Let a walk wait for its delivery's word, and serve the delivery's owner (hq ADR 0239)
mesh/merge-gate pass: every machine composes with the change as it did without (0 of 4 compose)
mesh/delivery delivered
mesh/delivery-group group feat/mesh-delivery delivered: every member is delivered

While the mesh-delivery seat has a holder on record, a merge that moves no
core module opens its walk and asks nothing until mesh-delivery or a person
says go; nothing of it is registered before its turn, so no other send
carries it. The controller keeps the planner, the gate, sending and the
walk, and gains the verbs the owner asks with: delivery-plan, -order,
-check (a group composed as one future state), deliver, delivery-stop,
delivery-walks; every walk kept is said as plan-moved.
This commit is contained in:
jochen
2026-10-07 00:01:42 +02:00
parent ba26ba2772
commit 487aa040de
29 changed files with 1562 additions and 39 deletions
+49 -1
View File
@@ -81,6 +81,19 @@ type CheckSpec struct {
// Toolchains is every toolchain the mesh holds, by language, for a script that declares another.
Toolchain string
Toolchains map[string]string
// Group are a delivery group's other heads (novox/hq ADR 0239), each cloned beside this one at its head
// and composed with it by the gate as one future state. The repository's own check is not run for a
// group: each member's pull request runs its own.
Group []GroupHead
}
// GroupHead is one other head of a delivery group's composed check.
type GroupHead struct {
Owner string
Repo string
Repository string
Ref string
Paths []string
}
// Gated is whether the change touches the mesh's graph, and so whether the gate runs.
@@ -141,6 +154,9 @@ const (
EnvVerdict = "MESH_CHECK_VERDICT"
EnvBeside = "MESH_CHECK_BESIDE"
EnvModules = "MESH_CHECK_MODULES"
// EnvGroup is a delivery group's other heads, as JSON, for the gate (novox/hq ADR 0239); empty for a
// pull request checked alone.
EnvGroup = "MESH_CHECK_GROUP"
)
// CheckTimeout bounds one check; a check that runs past it is an error, not a pass.
@@ -234,6 +250,32 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
say("check", "beside it %s at %s", dir, short(b.Ref))
}
// A delivery group's other heads (novox/hq ADR 0239), each at its head, for the gate to compose with this.
groupFile := ""
if len(spec.Group) > 0 {
var heads []map[string]any
for i, g := range spec.Group {
dir := fmt.Sprintf("group-%d", i)
if g.Repo != "" && safeName.MatchString(g.Repo) {
dir = "group-" + g.Repo
}
if err := clone(g.Repository, g.Ref, dir); err != nil {
return CheckVerdict{}, fmt.Errorf("a head of the group, %w", err)
}
say("check", "with it, of its group, %s/%s at %s", g.Owner, g.Repo, short(g.Ref))
heads = append(heads, map[string]any{"repository": g.Owner + "/" + g.Repo,
"tree": filepath.Join(root, dir), "changed": g.Paths})
}
raw, err := json.Marshal(heads)
if err != nil {
return CheckVerdict{}, err
}
groupFile = filepath.Join(root, "group.json")
if err := os.WriteFile(groupFile, raw, 0o644); err != nil {
return CheckVerdict{}, err
}
}
// The facts, and the versions they say the mesh runs.
if registry == "" {
return CheckVerdict{}, errors.New("no artifact store to read the facts snapshot from")
@@ -316,6 +358,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
EnvTestStore + "=" + storeURL, EnvTestBus + "=nats://" + bus, EnvRepository + "=" + spec.Owner + "/" + spec.Repo,
EnvChanged + "=" + strings.Join(spec.Paths, ","), EnvBeside + "=" + root,
EnvModules + "=" + strings.Join(append(append([]string{}, spec.Modules...), spec.New...), ","),
EnvGroup + "=" + groupFile,
"GOCACHE=" + filepath.Join(workspace, "go-cache"), "GOMODCACHE=" + filepath.Join(workspace, "go-modules")}
v := CheckVerdict{}
@@ -346,6 +389,10 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
// **The repository's own check**, in the toolchain it declares.
switch {
case len(spec.Group) > 0:
// A group's composed check judges the heads together; each member's own tests are its pull request's.
v.Repo = &Layer{Verdict: "pass", Summary: "a group's composed check: each member's own " + CheckScript +
" runs on its pull request"}
case !hasScript:
v.Repo = &Layer{Verdict: "warning", Summary: noScript}
case timedOut():
@@ -447,7 +494,8 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
// 2. Every machine composed with the change.
if err := running(inToolchain(tree, append(env, EnvVerdict+"="+verdictFile), "sh", "-c",
`"$MESH_GATE" merge-gate --facts "$MESH_FACTS" --store "$MESH_GATE_POSTGRES" `+
`--repository "$MESH_CHECK_REPOSITORY" --tree . --changed "$MESH_CHECK_CHANGED" --json > "$MESH_CHECK_VERDICT"`)); err != nil {
`--repository "$MESH_CHECK_REPOSITORY" --tree . --changed "$MESH_CHECK_CHANGED" `+
`${MESH_CHECK_GROUP:+--group "$MESH_CHECK_GROUP"} --json > "$MESH_CHECK_VERDICT"`)); err != nil {
if ctx.Err() != nil {
return "error", "the check was ended during the merge gate"
}
+40
View File
@@ -232,6 +232,7 @@ var aJudge = map[string]string{
"cmd/mesh-controller/main.go": `package main
import (
"encoding/json"
"fmt"
"os"
"strings"
@@ -252,6 +253,24 @@ func main() {
fmt.Println(m + ": ok")
}
case os.Args[1] == "merge-gate":
for i, a := range os.Args {
if a == "--group" && i+1 < len(os.Args) {
body, _ := os.ReadFile(os.Args[i+1])
var heads []struct {
Repository string ` + "`json:\"repository\"`" + `
Tree string ` + "`json:\"tree\"`" + `
}
_ = json.Unmarshal(body, &heads)
var said []string
for _, h := range heads {
if _, err := os.Stat(h.Tree + "/module.json"); err == nil {
said = append(said, h.Repository)
}
}
fmt.Printf("{\"verdict\":\"pass\",\"summary\":\"composed with %s\"}\n", strings.Join(said, ","))
return
}
}
fmt.Println(` + "`" + `{"verdict":"warning","summary":"a merge rebuilds 14 module(s)"}` + "`" + `)
}
}
@@ -323,6 +342,27 @@ func TestTheGateRunsWhenTheGraphIsTouchedBesideTheRepositorysOwnCheck(t *testing
t.Fatalf("a fault the base already had failed the change: %+v\n%s", v.Gate, v.Report)
}
// A delivery group (novox/hq ADR 0239): the other heads cloned beside it at their heads and handed to the
// gate to compose with this one; the repository's own check is each pull request's, not the group's.
app, appHead := aCheckedRepository(t, map[string]string{"module.json": `{"module":"app"}`})
repo2, head2 := aCheckedRepository(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`,
CheckScript: "exit 1\n"})
gid := fmt.Sprintf("check-group-%d", time.Now().UnixNano())
v, err = Check(t.Context(), Command, CheckSpec{ID: gid, Repository: repo2, Ref: head2, Owner: "novox",
Repo: "mesh-catalog", Paths: []string{"modules/gitea/module.json"}, Beside: beside, Modules: []string{"gitea"},
Manifests: []string{"modules/gitea/module.json"}, Toolchain: goToolchain,
Group: []GroupHead{{Owner: "novox", Repo: "app", Repository: app, Ref: appHead, Paths: []string{"module.json"}}}},
t.TempDir(), registry, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if v.Gate.Verdict != "pass" || v.Gate.Summary != "composed with novox/app" {
t.Fatalf("the group's other head was not composed: %+v\n%s", v.Gate, v.Report)
}
if v.Repo == nil || v.Repo.Verdict != "pass" || !strings.Contains(v.Repo.Summary, "group") {
t.Fatalf("a group's check ran a member's own script: %+v", v.Repo)
}
// A change to the controller judges itself: one that does not build fails its own gate.
v = check(map[string]string{"go.mod": "module x\n\ngo 1.22\n", "cmd/mesh-controller/main.go": "package main\nfunc main() { nope }\n",
"modules/gitea/module.json": "{}"}, "self")