From 5dfc0cc794caf339c43723e65e581abbaeb8243e Mon Sep 17 00:00:00 2001 From: Greg Date: Sun, 25 Oct 2026 15:52:18 -0400 Subject: [PATCH] fix(builder): drafts don't use CI secrets, and a missing one is fed back The second Valheim build stopped before its boot test: told that test.yaml's password "looks like a credential; map it under 'secrets'", the model mapped it to a CI secret nobody created, and boottest-payload's missing-secret exit ended the build. - The validator now says to remove a test password first (boot tests fill password settings with a throwaway value); secrets only when the server can't start without a real one. - Drafts with test.yaml 'secrets' fail the draft checks. - build turns boottest-payload's missing-secret exit into feedback for the model instead of an error. Co-Authored-By: Claude Opus 5.5 --- cmd/gbrgames/build.go | 13 +++++++++++-- cmd/gbrgames/draft.go | 5 +++++ cmd/gbrgames/draft_test.go | 9 +++++++++ cmd/gbrgames/validate.go | 3 ++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/cmd/gbrgames/build.go b/cmd/gbrgames/build.go index 1985c28..2bfcf4e 100644 --- a/cmd/gbrgames/build.go +++ b/cmd/gbrgames/build.go @@ -235,9 +235,18 @@ func bootCheck(gamesDir, id, agentImage string, wantOutside bool) (problems, ver if err != nil { return nil, nil, err } - payload, err := exec.Command(self, "boottest-payload", "-games", gamesDir, id).Output() + payloadCmd := exec.Command(self, "boottest-payload", "-games", gamesDir, id) + var stderr bytes.Buffer + payloadCmd.Stderr = &stderr + payload, err := payloadCmd.Output() if err != nil { - return nil, nil, fmt.Errorf("boottest-payload: %w", err) + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == exitMissingSecret { + // The builder has no CI secrets: that's the template's problem to fix, not a reason to stop + return []string{"the boot test can't run: " + strings.TrimSpace(stderr.String()) + ". Remove 'secrets' from " + + "test.yaml; boot tests fill password settings with a throwaway value"}, nil, nil + } + return nil, nil, fmt.Errorf("boottest-payload: %v: %s", err, strings.TrimSpace(stderr.String())) } if out, err := runCmd("docker", "pull", "-q", agentImage); err != nil { return nil, nil, fmt.Errorf("docker pull %s: %v: %s", agentImage, err, out) diff --git a/cmd/gbrgames/draft.go b/cmd/gbrgames/draft.go index ecde53c..dc62d25 100644 --- a/cmd/gbrgames/draft.go +++ b/cmd/gbrgames/draft.go @@ -270,6 +270,11 @@ func draftCompleteness(game, test map[string]any) []string { } else if v, _ := n.Int64(); v < 1024 { problems = append(problems, "requiredMemoryMb is below 1024") } + // Secrets only exist as CI settings a person adds; a draft with one can't be boot-tested anywhere + if secrets, _ := test["secrets"].(map[string]any); len(secrets) > 0 { + problems = append(problems, "test.yaml has 'secrets' ("+strings.Join(sortedKeys(secrets), ", ")+"), which no boot test of a "+ + "new game has: remove them. Password settings get a throwaway value in boot tests automatically") + } if _, ok := game["readyPattern"]; !ok { if _, ok := test["settleSec"]; !ok { problems = append(problems, "no readyPattern and no test.settleSec: boot tests need one of them") diff --git a/cmd/gbrgames/draft_test.go b/cmd/gbrgames/draft_test.go index 81a8fe9..062fa7c 100644 --- a/cmd/gbrgames/draft_test.go +++ b/cmd/gbrgames/draft_test.go @@ -84,3 +84,12 @@ func TestImageUnusable(t *testing.T) { t.Errorf("long run flagged: %q", why) } } + +func TestDraftCompletenessRejectsSecrets(t *testing.T) { + // The Valheim draft that couldn't be boot-tested: the password mapped to a CI secret nobody created + test := map[string]any{"settleSec": json.Number("120"), "secrets": map[string]any{"Password": "VALHEIM_TEST_PASSWORD"}} + problems := strings.Join(draftCompleteness(map[string]any{}, test), "\n") + if !strings.Contains(problems, "test.yaml has 'secrets' (Password)") { + t.Errorf("got:\n%s", problems) + } +} diff --git a/cmd/gbrgames/validate.go b/cmd/gbrgames/validate.go index 394fce3..225b13e 100644 --- a/cmd/gbrgames/validate.go +++ b/cmd/gbrgames/validate.go @@ -168,7 +168,8 @@ func validateGame(dir, folder string, gameSchema, testSchema *jsonschema.Schema) add("test.yaml config.%s is not a key of this game", key) } if sensitiveKey.MatchString(key) { - add("test.yaml config.%s looks like a credential; map it under 'secrets' to a Gitea secret instead", key) + add("test.yaml config.%s looks like a credential; remove it (boot tests fill password settings with a throwaway "+ + "value), or, only if the server can't start without a real one, map it under 'secrets' to a CI secret", key) } } for _, key := range sortedKeys(secrets) {