235 lines
5.2 KiB
Go
235 lines
5.2 KiB
Go
package buildqueue
|
|
|
|
import (
|
|
"brunel/config"
|
|
"brunel/domain"
|
|
"brunel/packages"
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
func StartPackageQueueWorker(ctx context.Context) {
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
err := packages.ProcessPackages()
|
|
if err != nil {
|
|
slog.Error("unable to process packages: " + err.Error())
|
|
}
|
|
packs := packages.GetPackages()
|
|
packs.Iter(func(k string, v domain.SourcePackage) bool {
|
|
needsBuild := false
|
|
buildVersion := ""
|
|
buildAttempt := 0
|
|
errPreviously := false
|
|
v.Packages.Iter(func(k string, v domain.PackageInfo) bool {
|
|
if v.Status == domain.Current {
|
|
return true
|
|
}
|
|
if v.LastBuildStatus == domain.Error {
|
|
errPreviously = true
|
|
buildAttempt = 1
|
|
}
|
|
if v.Status == domain.Missing {
|
|
needsBuild = true
|
|
buildVersion = v.NewVersion
|
|
return false
|
|
}
|
|
if v.Status == domain.Stale {
|
|
needsBuild = true
|
|
buildVersion = v.NewVersion
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
if needsBuild {
|
|
typ := domain.BuildTypeNormal
|
|
if errPreviously {
|
|
typ = domain.BuildTypeLTO
|
|
}
|
|
buildItem := domain.BuildQueueItem{
|
|
Source: v,
|
|
Status: domain.Queued,
|
|
Type: typ,
|
|
Patch: false,
|
|
Rebuild: false,
|
|
BuildNumber: buildAttempt,
|
|
BuildVersion: buildVersion,
|
|
}
|
|
err := Add(buildItem)
|
|
if err != nil {
|
|
slog.Info("unable to add package to queue: " + err.Error())
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
time.Sleep(1 * time.Hour)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func StartQueueWorker(ctx context.Context) {
|
|
go processQueue(ctx)
|
|
}
|
|
|
|
func StartStatusWorker(ctx context.Context) {
|
|
go processStatus(ctx)
|
|
}
|
|
|
|
func processStatus(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
q := GetQueue()
|
|
itemsToRemove := make([]string, 0)
|
|
q.Iter(func(k string, item domain.BuildQueueItem) bool {
|
|
if item.Status == domain.Building {
|
|
complete, err := CheckIfBuildComplete(ctx, item)
|
|
if err != nil && !complete {
|
|
slog.Error("unable to check if build is complete: " + err.Error())
|
|
}
|
|
if complete {
|
|
if err != nil {
|
|
item.Source.Packages.Iter(func(k string, v domain.PackageInfo) bool {
|
|
v.Status = domain.Error
|
|
v.LastBuildStatus = domain.Error
|
|
item.Source.Packages.Set(k, v)
|
|
return true
|
|
})
|
|
packages.UpdateSourcePackage(item.Source)
|
|
itemsToRemove = append(itemsToRemove, k)
|
|
return true
|
|
}
|
|
item.Source.Packages.Iter(func(k string, v domain.PackageInfo) bool {
|
|
v.Status = domain.Built
|
|
v.LastBuildStatus = domain.Built
|
|
v.Version = item.BuildVersion
|
|
v.NewVersion = ""
|
|
item.Source.Packages.Set(k, v)
|
|
return true
|
|
})
|
|
packages.UpdateSourcePackage(item.Source)
|
|
itemsToRemove = append(itemsToRemove, k)
|
|
return true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
for _, item := range itemsToRemove {
|
|
Remove(item)
|
|
}
|
|
time.Sleep(5 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
func processQueue(ctx context.Context) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
err := ProcessNext()
|
|
if err != nil {
|
|
slog.Error("unable to process queue: " + err.Error())
|
|
}
|
|
}
|
|
time.Sleep(30 * time.Second)
|
|
}
|
|
}
|
|
|
|
func CheckIfBuildComplete(ctx context.Context, item domain.BuildQueueItem) (bool, error) {
|
|
resp, err := http.Get(config.Configs.ActionsUrl)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
doc, err := html.Parse(resp.Body)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
buildName := item.Source.Name + "=" + item.BuildVersion
|
|
var f func(*html.Node) (bool, error)
|
|
f = func(n *html.Node) (bool, error) {
|
|
if n.Type == html.ElementNode && n.Data == "div" {
|
|
for _, a := range n.Attr {
|
|
if a.Key == "class" && strings.Contains(a.Val, "flex-item") {
|
|
isMatch, status := checkBuildBlock(n, buildName)
|
|
if isMatch {
|
|
switch status {
|
|
case "Success":
|
|
return true, nil
|
|
case "Failure":
|
|
return true, fmt.Errorf("build failed")
|
|
default:
|
|
// Build found but status unknown, keep searching
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
isComplete, err := f(c)
|
|
if isComplete || err != nil {
|
|
return isComplete, err
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
return f(doc)
|
|
}
|
|
|
|
func checkBuildBlock(n *html.Node, buildName string) (bool, string) {
|
|
var title string
|
|
var status string
|
|
|
|
var f func(*html.Node)
|
|
f = func(n *html.Node) {
|
|
if n.Type == html.ElementNode {
|
|
switch n.Data {
|
|
case "a":
|
|
for _, a := range n.Attr {
|
|
if a.Key == "class" && a.Val == "flex-item-title" {
|
|
for _, attr := range n.Attr {
|
|
if attr.Key == "title" {
|
|
title = attr.Val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
case "span":
|
|
for _, a := range n.Attr {
|
|
if a.Key == "data-tooltip-content" {
|
|
if a.Val == "Success" || a.Val == "Failure" {
|
|
status = a.Val
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
f(c)
|
|
}
|
|
}
|
|
|
|
f(n)
|
|
|
|
return title == buildName, status
|
|
}
|