Run each part of a repository's own check in the toolchain it declares (hq issue 302)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery held for a person: merged, and the controller opened no walk for it within 10m0s — nothing it holds follows that branch, or the merge was…
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery held for a person: merged, and the controller opened no walk for it within 10m0s — nothing it holds follows that branch, or the merge was…
mesh-lab's merge-check.sh runs in the TypeScript toolchain, which holds no Go compiler, so its replays register was never compiled before a merge and a broken one would have merged green. A script now declares a further part with '# mesh-check-also: <toolchain> <script>'; the build seat runs it in that toolchain's own container, and mesh/repo-check passes only when every part does.
This commit is contained in:
+95
-25
@@ -39,6 +39,11 @@ import (
|
||||
// quality, run when present in the toolchain it declares (`# mesh-check-toolchain: go|typescript`
|
||||
// among its first lines; go when it declares none). A repository that reaches the build seat with
|
||||
// none is said as a warning: it is the mesh's, and nothing of its own is tested before it merges.
|
||||
// **A repository in two languages declares the other part too** (`# mesh-check-also: <toolchain>
|
||||
// <script>`, one line each, among the same first lines): each named script is run from the root in
|
||||
// its own toolchain's container, after merge-check.sh, and the layer passes only when every part
|
||||
// does (novox/hq issue 302 — the lab's Go replays went unchecked because its script runs in the
|
||||
// TypeScript toolchain, which holds no Go compiler).
|
||||
//
|
||||
// One check:
|
||||
//
|
||||
@@ -178,6 +183,32 @@ const noScript = "the repository declares no " + CheckScript + ": none of its ow
|
||||
// toolchainLine is how a merge-check.sh declares the toolchain it runs in.
|
||||
var toolchainLine = regexp.MustCompile(`^#\s*mesh-check-toolchain:\s*([a-z0-9-]+)\s*$`)
|
||||
|
||||
// alsoLine is how a merge-check.sh declares a further part of the repository's check, run in a toolchain
|
||||
// of its own: `# mesh-check-also: <toolchain> <script>`.
|
||||
var alsoLine = regexp.MustCompile(`^#\s*mesh-check-also:\s*([a-z0-9-]+)\s+(\S+)\s*$`)
|
||||
|
||||
// ScriptPart is one part of a repository's own check: a script, run from the repository's root, and the
|
||||
// toolchain it runs in.
|
||||
type ScriptPart struct {
|
||||
Toolchain string
|
||||
Script string
|
||||
}
|
||||
|
||||
// ScriptParts is every part of a repository's own check: its merge-check.sh in the toolchain it declares,
|
||||
// then each further part it declares among its first twenty lines (`# mesh-check-also: <toolchain>
|
||||
// <script>`), in the order declared. A declared script that is not a path inside the repository is
|
||||
// answered as a part all the same, for the check to refuse by name rather than to drop.
|
||||
func ScriptParts(script []byte) []ScriptPart {
|
||||
parts := []ScriptPart{{Toolchain: ScriptToolchain(script), Script: CheckScript}}
|
||||
lines := bufio.NewScanner(bytes.NewReader(script))
|
||||
for i := 0; i < 20 && lines.Scan(); i++ {
|
||||
if m := alsoLine.FindStringSubmatch(strings.TrimSpace(lines.Text())); m != nil {
|
||||
parts = append(parts, ScriptPart{Toolchain: m[1], Script: m[2]})
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
// ScriptToolchain is the language a merge-check.sh declares it runs in, among its first twenty lines;
|
||||
// go when it declares none.
|
||||
func ScriptToolchain(script []byte) string {
|
||||
@@ -416,32 +447,11 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
case timedOut():
|
||||
v.Repo = &Layer{Verdict: "error", Summary: fmt.Sprintf("the check ran past %s before its %s ran", CheckTimeout, CheckScript)}
|
||||
default:
|
||||
language := ScriptToolchain(script)
|
||||
image := spec.Toolchains[language]
|
||||
if language == "go" && image == "" {
|
||||
image = spec.Toolchain
|
||||
}
|
||||
if image == "" {
|
||||
v.Repo = &Layer{Verdict: "error", Summary: fmt.Sprintf("its %s runs in the %s toolchain, which the mesh does "+
|
||||
"not hold", CheckScript, language)}
|
||||
break
|
||||
}
|
||||
say("check", "running its %s in the mesh's %s toolchain", CheckScript, language)
|
||||
fmt.Fprintf(&out, "--- its %s (%s toolchain)\n", CheckScript, language)
|
||||
var own tail
|
||||
cmd := exec.CommandContext(ctx, "docker", LabelledArgs("docker", in(image, tree, env, "sh", CheckScript), spec.ID)...)
|
||||
inItsOwnGroup(cmd)
|
||||
w := io.MultiWriter(&out, &own)
|
||||
cmd.Stdout, cmd.Stderr = w, w
|
||||
switch err := cmd.Run(); {
|
||||
case timedOut():
|
||||
v.Repo = &Layer{Verdict: "error", Summary: fmt.Sprintf("its %s ran past %s and was ended", CheckScript, CheckTimeout)}
|
||||
case ctx.Err() != nil:
|
||||
v.Repo = ownCheck(ctx, spec, ScriptParts(script), tree, &out, timedOut, func(image string, script string) *exec.Cmd {
|
||||
return exec.CommandContext(ctx, "docker", LabelledArgs("docker", in(image, tree, env, "sh", script), spec.ID)...)
|
||||
}, say)
|
||||
if v.Repo == nil {
|
||||
return v, ctx.Err()
|
||||
case err != nil:
|
||||
v.Repo = &Layer{Verdict: "fail", Summary: "its " + CheckScript + " failed: " + whatFailed(own.String())}
|
||||
default:
|
||||
v.Repo = &Layer{Verdict: "pass", Summary: "its " + CheckScript + " passed"}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +462,66 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ownCheck runs the repository's own check, part by part, each in its toolchain's own container: the
|
||||
// layer's verdict. Nil when the check was ended from outside (not past its bound), which the caller
|
||||
// answers as the context's error.
|
||||
func ownCheck(ctx context.Context, spec CheckSpec, parts []ScriptPart, tree string, out *tail, timedOut func() bool,
|
||||
command func(image, script string) *exec.Cmd, say func(step, format string, args ...any)) *Layer {
|
||||
var ran []string
|
||||
for i, part := range parts {
|
||||
named := part.Script
|
||||
if i > 0 {
|
||||
named = CheckScript + "'s part " + part.Script
|
||||
}
|
||||
image := spec.Toolchains[part.Toolchain]
|
||||
if part.Toolchain == "go" && image == "" {
|
||||
image = spec.Toolchain
|
||||
}
|
||||
if image == "" {
|
||||
return &Layer{Verdict: "error", Summary: fmt.Sprintf("%sits %s runs in the %s toolchain, which the mesh does "+
|
||||
"not hold", passedSoFar(ran), named, part.Toolchain)}
|
||||
}
|
||||
if i > 0 && !filepath.IsLocal(part.Script) {
|
||||
return &Layer{Verdict: "fail", Summary: fmt.Sprintf("%s declares a part %q that is not a path inside the "+
|
||||
"repository", CheckScript, part.Script)}
|
||||
}
|
||||
if i > 0 {
|
||||
if info, err := os.Stat(filepath.Join(tree, part.Script)); err != nil || info.IsDir() {
|
||||
return &Layer{Verdict: "fail", Summary: fmt.Sprintf("%s declares a part %s, which the repository does "+
|
||||
"not hold", CheckScript, part.Script)}
|
||||
}
|
||||
}
|
||||
say("check", "running its %s in the mesh's %s toolchain", part.Script, part.Toolchain)
|
||||
fmt.Fprintf(out, "--- its %s (%s toolchain)\n", part.Script, part.Toolchain)
|
||||
var own tail
|
||||
cmd := command(image, part.Script)
|
||||
inItsOwnGroup(cmd)
|
||||
w := io.MultiWriter(out, &own)
|
||||
cmd.Stdout, cmd.Stderr = w, w
|
||||
switch err := cmd.Run(); {
|
||||
case timedOut():
|
||||
return &Layer{Verdict: "error", Summary: fmt.Sprintf("its %s ran past %s and was ended", part.Script, CheckTimeout)}
|
||||
case ctx.Err() != nil:
|
||||
return nil
|
||||
case err != nil:
|
||||
return &Layer{Verdict: "fail", Summary: passedSoFar(ran) + "its " + part.Script + " failed: " + whatFailed(own.String())}
|
||||
}
|
||||
ran = append(ran, fmt.Sprintf("%s (%s)", part.Script, part.Toolchain))
|
||||
}
|
||||
if len(ran) == 1 {
|
||||
return &Layer{Verdict: "pass", Summary: "its " + CheckScript + " passed"}
|
||||
}
|
||||
return &Layer{Verdict: "pass", Summary: "its " + CheckScript + " passed, each part in its toolchain: " + strings.Join(ran, ", ")}
|
||||
}
|
||||
|
||||
// passedSoFar is what of a check's parts passed before the one that did not, said first.
|
||||
func passedSoFar(ran []string) string {
|
||||
if len(ran) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(ran, ", ") + " passed; "
|
||||
}
|
||||
|
||||
// gateLayer runs the gate: the touched manifests through `module check`, every machine composed with the
|
||||
// change, and the replays of what the mesh runs. It answers the gate's verdict and summary.
|
||||
func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFile string, env []string,
|
||||
|
||||
Reference in New Issue
Block a user