Keep judge builds warm in the build agent's judge cache, and leave nothing on the host
mesh/delivery-group group fix/704-release-branch-judge rejected: its order contradicts itself: novox/mesh-catalog@47ef2644aca1 and novox/mesh-controller@2bee29…
mesh/merge-gate head 2bee290eea56 merged into trunk 76304d9db817 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 2bee290eea56 merged into trunk 76304d9db817 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
mesh/delivery-group group fix/704-release-branch-judge rejected: its order contradicts itself: novox/mesh-catalog@47ef2644aca1 and novox/mesh-controller@2bee29…
mesh/merge-gate head 2bee290eea56 merged into trunk 76304d9db817 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 2bee290eea56 merged into trunk 76304d9db817 — pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Review of #329: a judge's home made with a temporary directory inside the build agent is resolved on the host by the container runtime, leaving a cold cache behind on every check, and a kept judge was lost on every redeploy. Judge builds now use the judge cache the build agent declares (MESH_JUDGE_CACHE), mounted into judge builds only: one warm Go cache, the kept judges, and a fresh home per check removed after it (hq ADR 0358 §6, issue 704, issue 482). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+65
-19
@@ -94,9 +94,12 @@ type CheckSpec struct {
|
||||
// the release branch too; a change to the controller still judges itself and a change to the node-engine brings
|
||||
// its own validator. The verdict names the branch, the judge's commit and the branch each beside was read at.
|
||||
Release string
|
||||
// JudgeCache is a directory outside the workspace where a release branch's judge is kept by the commits it is
|
||||
// built from, so it is built once per release-branch head (ADR 0358 §2). Outside: the check's containers mount
|
||||
// the workspace and a pull request's own script must not plant a judge for the next check. Empty keeps none.
|
||||
// JudgeCache is the judge cache (novox/hq ADR 0358 §6): a directory of the build agent's own, outside the
|
||||
// workspace, that only judge builds mount — never a check's containers, so no pull request's script can write it.
|
||||
// It holds the Go build cache every judge is built with (judges stay warm), the release-branch judges kept by the
|
||||
// commits they are built from (judges/, one per release-branch head), and each judge build's fresh home (homes/,
|
||||
// removed after the build). Empty: each judge is built cold under the check's own root, which every check empties
|
||||
// before it clones, and nothing is kept.
|
||||
JudgeCache string
|
||||
// Toolchain is the image a check's Go runs in: the mesh's own Go toolchain, as it holds it.
|
||||
// Toolchains is every toolchain the mesh holds, by language, for a script that declares another.
|
||||
@@ -598,20 +601,21 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
// **Every judge is built where no pull request can reach** (novox/hq issue 482, ADR 0358 review): the workspace's
|
||||
// Go cache, HOME and git configuration are written by pull requests' own scripts, and git runs during a Go build
|
||||
// to stamp its version — a planted hook would rewrite the judge's sources before they compile. So a judge is built
|
||||
// with a HOME, configuration and Go cache of its own, outside the workspace and made fresh for this check, no
|
||||
// global or system git configuration, no Go environment file, and no version stamping from git.
|
||||
judgeHome, err := os.MkdirTemp("", "mesh-check-judge-")
|
||||
// with a HOME and configuration made fresh for this check and a Go cache only judge builds mount (judgeBuildDirs),
|
||||
// no global or system git configuration, no Go environment file, and no version stamping from git.
|
||||
judgeHome, judgeGoCache, err := judgeBuildDirs(spec.JudgeCache, workspace, root, spec.ID)
|
||||
if err != nil {
|
||||
return CheckVerdict{}, err
|
||||
}
|
||||
defer os.RemoveAll(judgeHome)
|
||||
if withinDir(workspace, judgeHome) {
|
||||
return CheckVerdict{}, fmt.Errorf("the temporary directory %s is inside the workspace %s, which a check's "+
|
||||
"containers mount: no judge is built where a pull request could reach it", judgeHome, workspace)
|
||||
}
|
||||
inJudgeBuild := func(dir string, _ []string, command ...string) []string {
|
||||
args := in(spec.Toolchain, dir, JudgeBuildEnv(judgeHome), command...)
|
||||
return append([]string{args[0], "--volume", judgeHome + ":" + judgeHome}, args[1:]...)
|
||||
args := in(spec.Toolchain, dir, JudgeBuildEnv(judgeHome, judgeGoCache), command...)
|
||||
if spec.JudgeCache == "" {
|
||||
return args // under the check's root, in the workspace mounted already
|
||||
}
|
||||
// The judge cache's parts this build needs, mounted into judge builds alone (ADR 0358 §6).
|
||||
return append([]string{args[0], "--volume", judgeHome + ":" + judgeHome, "--volume",
|
||||
judgeGoCache + ":" + judgeGoCache}, args[1:]...)
|
||||
}
|
||||
var out tail
|
||||
// running runs one container of the check, its output into the report, in a process group of its own.
|
||||
@@ -1153,7 +1157,7 @@ func releaseJudge(ctx context.Context, run, plain Runner, spec CheckSpec, root s
|
||||
with = ", with mesh-host's validator of " + spec.Release + " at " + twelve(host)
|
||||
}
|
||||
bin := filepath.Join(root, "bin", "mesh-controller")
|
||||
if took := judgeKept(spec.JudgeCache, key, bin); took {
|
||||
if took := judgeKept(keptJudges(spec.JudgeCache), key, bin); took {
|
||||
say("check", "judged by %s's controller at %s%s, built for an earlier check of that head", spec.Release,
|
||||
twelve(key), with)
|
||||
return bin, dir, nil
|
||||
@@ -1172,21 +1176,63 @@ func releaseJudge(ctx context.Context, run, plain Runner, spec CheckSpec, root s
|
||||
return "", "", fmt.Errorf("the controller of %s does not build%s: %w", spec.Release, with, err)
|
||||
}
|
||||
// Kept before anything of the change runs: nothing a pull request's script writes can reach the cache.
|
||||
keepJudge(spec.JudgeCache, key, bin, say)
|
||||
keepJudge(keptJudges(spec.JudgeCache), key, bin, say)
|
||||
say("check", "judged by %s's controller at %s%s", spec.Release, twelve(key), with)
|
||||
return bin, dir, nil
|
||||
}
|
||||
|
||||
// JudgeBuildEnv is the environment every judge is built in (novox/hq issue 482): home, a directory outside the
|
||||
// workspace made fresh for the check, as HOME, configuration and Go cache; no global or system git configuration; no
|
||||
// Go environment file; and no version stamped from git, so no git runs during the build at all.
|
||||
func JudgeBuildEnv(home string) []string {
|
||||
// JudgeBuildEnv is the environment every judge is built in (novox/hq ADR 0358 §6, issue 482): home, made fresh for the
|
||||
// build, as HOME and configuration; goCache, the judge cache's Go cache that no check's container mounts; no global
|
||||
// or system git configuration; no Go environment file; and no version stamped from git, so no git runs at all.
|
||||
func JudgeBuildEnv(home, goCache string) []string {
|
||||
return []string{"HOME=" + home, "XDG_CONFIG_HOME=" + filepath.Join(home, ".config"),
|
||||
"GIT_CONFIG_GLOBAL=/dev/null", "GIT_CONFIG_NOSYSTEM=1", "GOENV=off",
|
||||
"GOCACHE=" + filepath.Join(home, "go-cache"), "GOPATH=" + filepath.Join(home, "go"),
|
||||
"GOCACHE=" + goCache, "GOPATH=" + filepath.Join(home, "go"),
|
||||
"CGO_ENABLED=0", "GOFLAGS=-mod=vendor -buildvcs=false", "GOPROXY=off"}
|
||||
}
|
||||
|
||||
// judgeBuildDirs are the home and the Go cache a check's judge builds use (ADR 0358 §6). With a judge cache: a home
|
||||
// made fresh under its homes/ for this check, and its go-cache/, kept warm between checks — both on the host path the
|
||||
// build agent declares, so the container runtime mounts what the agent sees, and nothing is left in a temporary
|
||||
// directory of the host. Without one: both under the check's root, which every check empties before it clones. A
|
||||
// judge cache inside the workspace is refused: a check's containers mount the workspace.
|
||||
func judgeBuildDirs(cache, workspace, root, id string) (string, string, error) {
|
||||
if cache == "" {
|
||||
home, goCache := filepath.Join(root, "judge-home"), filepath.Join(root, "judge-go-cache")
|
||||
for _, d := range []string{home, goCache} {
|
||||
if err := os.MkdirAll(d, 0o700); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return home, goCache, nil
|
||||
}
|
||||
if withinDir(workspace, cache) {
|
||||
return "", "", fmt.Errorf("the judge cache %s is inside the workspace %s, which a check's containers mount: "+
|
||||
"no judge is built where a pull request could reach it", cache, workspace)
|
||||
}
|
||||
if !safeName.MatchString(id) {
|
||||
id = "check"
|
||||
}
|
||||
home, goCache := filepath.Join(cache, "homes", id), filepath.Join(cache, "go-cache")
|
||||
if err := os.RemoveAll(home); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
for _, d := range []string{home, goCache} {
|
||||
if err := os.MkdirAll(d, 0o700); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return home, goCache, nil
|
||||
}
|
||||
|
||||
// keptJudges is where a judge cache keeps the release-branch judges, empty when there is no judge cache.
|
||||
func keptJudges(cache string) string {
|
||||
if cache == "" {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(cache, "judges")
|
||||
}
|
||||
|
||||
// judgesKept is how many release-branch judges a judge cache keeps: a head moved past is not built from again.
|
||||
const judgesKept = 4
|
||||
|
||||
|
||||
Reference in New Issue
Block a user