feat(builder): repair existing templates, a weekly check that queues repairs, and eval
The builder could only create games; nothing noticed when a live template stopped working, and there was no way to tell whether a builder change made it better or worse. - repair -id ID: boots an existing template as nodes run it; if it fails, the same investigation as for a draft runs and the model changes only what the diagnosis says. The worker handles "repair" jobs (gameId from the panel) and opens "fix(games): repair ..." on draft/<id>; a template that boots as it is reports "nothing to repair" and opens no PR. The boot and fix loop is now shared by builds and repairs. - validate.yml runs weekly and boot-tests every game; failed games are sent to the panel (X-GBR-Event: games_boot_failed) to queue repairs. - eval -ids all: builds games that already have a template from scratch, blind, and scores each draft against it (boots, same image and app, ports, ready line, settings found) into eval.md and eval.json. - research skips our own sites, so neither research nor eval reads our templates back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f11c5eaf2f
commit
f1271341ef
12 files changed
+535
-20
No files matched your search
+52
-7
@@ -32,7 +32,8 @@ import (
|
||||
|
||||
type builderJob struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"` // "research" or "build"
|
||||
Kind string `json:"kind"` // "research", "build" or "repair"
|
||||
GameID string `json:"gameId"` // repair: the template to fix
|
||||
GameName string `json:"gameName"`
|
||||
Notes string `json:"notes"`
|
||||
Variant json.RawMessage `json:"variant"`
|
||||
@@ -43,6 +44,8 @@ type jobResult struct {
|
||||
Build *buildResult `json:"build,omitempty"`
|
||||
PRURL string `json:"prUrl,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
// A repair whose template already boots: no pull request
|
||||
NoChange bool `json:"noChange,omitempty"`
|
||||
}
|
||||
|
||||
type workerConfig struct {
|
||||
@@ -258,6 +261,30 @@ func (c *workerConfig) handle(job *builderJob) *jobResult {
|
||||
return &jobResult{Build: res, Error: "opening the pull request: " + err.Error()}
|
||||
}
|
||||
return &jobResult{Build: res, PRURL: pr}
|
||||
|
||||
case "repair":
|
||||
if job.GameID == "" {
|
||||
return &jobResult{Error: "a repair job needs gameId"}
|
||||
}
|
||||
args := append([]string{"repair", "-id", job.GameID, "-reason", job.Notes, "-out", resultFile}, modelArgs...)
|
||||
runErr := c.runStreamed(job.ID, dir, self, args, "BOOT_TEST")
|
||||
res := &buildResult{}
|
||||
if err := readJSON(resultFile, res); err != nil {
|
||||
msg := "the repair produced no result"
|
||||
if runErr != nil {
|
||||
msg = runErr.Error()
|
||||
}
|
||||
return &jobResult{Error: msg}
|
||||
}
|
||||
if res.NoChange {
|
||||
return &jobResult{Build: res, NoChange: true}
|
||||
}
|
||||
c.progress(job.ID, "OPENING_PR", []string{"[worker] opening the pull request"})
|
||||
pr, err := c.openPR(dir, job, res)
|
||||
if err != nil {
|
||||
return &jobResult{Build: res, Error: "opening the pull request: " + err.Error()}
|
||||
}
|
||||
return &jobResult{Build: res, PRURL: pr}
|
||||
}
|
||||
return &jobResult{Error: "unknown job kind " + job.Kind}
|
||||
}
|
||||
@@ -369,14 +396,22 @@ func (c *workerConfig) openPR(dir string, job *builderJob, res *buildResult) (st
|
||||
summary += fmt.Sprintf("\nReplaces #%d (closed, branch deleted).\n", n)
|
||||
}
|
||||
auth := "http.extraHeader=Authorization: token " + c.botToken
|
||||
commit, title := prText(job.GameName, job.ID, res)
|
||||
if res.Repair {
|
||||
// A repair that changed nothing has nothing to propose
|
||||
diff := exec.Command("git", "status", "--porcelain", "--", filepath.ToSlash(res.Dir))
|
||||
diff.Dir = dir
|
||||
if out, _ := diff.Output(); len(strings.TrimSpace(string(out))) == 0 {
|
||||
return "", errors.New("the repair changed nothing in the template: " + strings.Join(res.Problems, "; "))
|
||||
}
|
||||
}
|
||||
steps := []struct {
|
||||
name string
|
||||
args []string
|
||||
}{
|
||||
{"checkout", []string{"checkout", "-q", "-b", branch}},
|
||||
{"add", []string{"add", filepath.ToSlash(res.Dir)}},
|
||||
{"commit", []string{"-c", "user.name=" + c.botLogin, "-c", "user.email=" + c.botEmail, "commit", "-q", "-m",
|
||||
fmt.Sprintf("feat(games): draft %s\n\nDrafted and boot-tested by the GBR builder (job %s).", job.GameName, job.ID)}},
|
||||
{"commit", []string{"-c", "user.name=" + c.botLogin, "-c", "user.email=" + c.botEmail, "commit", "-q", "-m", commit}},
|
||||
// Forced: the branch is the bot's own, and whatever was on it was the draft being replaced
|
||||
{"push", []string{"-c", auth, "push", "-q", "--force", "origin", branch}},
|
||||
}
|
||||
@@ -388,10 +423,6 @@ func (c *workerConfig) openPR(dir string, job *builderJob, res *buildResult) (st
|
||||
return "", fmt.Errorf("git %s: %s", s.name, strings.ReplaceAll(string(out), c.botToken, "***"))
|
||||
}
|
||||
}
|
||||
title := "feat(games): add " + job.GameName
|
||||
if !res.Passed {
|
||||
title = "WIP: " + title + " (did not boot yet)"
|
||||
}
|
||||
var pr struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
@@ -489,3 +520,17 @@ func readJSON(p string, out any) error {
|
||||
}
|
||||
return json.Unmarshal(raw, out)
|
||||
}
|
||||
|
||||
// prText is the commit message and pull request title for a new game or a repair.
|
||||
func prText(gameName, jobID string, res *buildResult) (commit, title string) {
|
||||
commit = fmt.Sprintf("feat(games): draft %s\n\nDrafted and boot-tested by the GBR builder (job %s).", gameName, jobID)
|
||||
title = "feat(games): add " + gameName
|
||||
if res.Repair {
|
||||
commit = fmt.Sprintf("fix(games): repair %s\n\nRepaired and boot-tested by the GBR builder (job %s).", gameName, jobID)
|
||||
title = "fix(games): repair " + gameName
|
||||
}
|
||||
if !res.Passed {
|
||||
title = "WIP: " + title + " (did not boot yet)"
|
||||
}
|
||||
return commit, title
|
||||
}
|
||||
Reference in new issue
Block a user