From a20af30d54d6cadc7ad5e90333967f175a7c2bcc Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 21:34:35 +0200 Subject: [PATCH] A group's Go member naming a mesh-sdk commit that is neither on the trunk nor the group's head fails the check naming the member and the commit; a pin only a head carries names issue 1148 (issue 1624) --- .../cmd/mesh-delivery-checker/forge.go | 61 ++++++++ .../cmd/mesh-delivery-checker/forge_test.go | 55 +++++++ .../mesh-delivery-checker/integration_test.go | 64 ++++++++ .../cmd/mesh-delivery-checker/judge.go | 3 +- .../cmd/mesh-delivery-checker/main.go | 2 +- .../internal/check/judge.go | 16 +- .../internal/check/pins.go | 104 +++++++++++++ .../internal/check/pins_test.go | 147 ++++++++++++++++++ .../internal/check/testdata/captured/README | 6 + .../captured/forge-compare-branch.json | 31 ++++ .../captured/forge-compare-trunk.json | 12 ++ .../captured/forge-get-file-go-mod.json | 9 ++ .../mesh-delivery-checker/manifest_test.go | 4 +- modules/mesh-delivery-checker/module.json | 2 + 14 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go create mode 100644 modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go create mode 100644 modules/mesh-delivery-checker/internal/check/pins.go create mode 100644 modules/mesh-delivery-checker/internal/check/pins_test.go create mode 100644 modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-branch.json create mode 100644 modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-trunk.json create mode 100644 modules/mesh-delivery-checker/internal/check/testdata/captured/forge-get-file-go-mod.json diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go new file mode 100644 index 0000000..9245ad5 --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +// The forge's tools the checker reads with (mesh-catalog modules/gitea): gitea_get_file answers {file: {content}}, the text +// of a file at a ref, and gitea_compare answers {total_commits, commits}, what head holds that base does not. The checker +// writes nothing to the forge: the forge bridge sets the statuses (ADR 0484 clause 2). +const ( + getFileTool = "gitea.gitea_get_file" + compareTool = "gitea.gitea_compare" +) + +// forgeReads reads the forge through call (mesh-sdk process.Conn.Call as the checker's own account). +type forgeReads struct { + call func(key string, body any) (json.RawMessage, error) +} + +func ownerRepo(repository string) (string, string) { + owner, repo, _ := strings.Cut(repository, "/") + return owner, repo +} + +// File is the text of a file of a repository at a ref. +func (f forgeReads) File(_ context.Context, repository, path, ref string) (string, error) { + owner, repo := ownerRepo(repository) + raw, err := f.call(getFileTool, map[string]string{"owner": owner, "repo": repo, "path": path, "ref": ref}) + if err != nil { + return "", err + } + var a struct { + File *struct { + Content *string `json:"content"` + } `json:"file"` + } + if err := json.Unmarshal(raw, &a); err != nil || a.File == nil || a.File.Content == nil { + return "", fmt.Errorf("the answer of %s is not {file: {content}}: %v", getFileTool, err) + } + return *a.File.Content, nil +} + +// OnTrunk says whether the commit is already on the repository's main: main holds it when the commit holds nothing main does +// not. +func (f forgeReads) OnTrunk(_ context.Context, repository, commit string) (bool, error) { + owner, repo := ownerRepo(repository) + raw, err := f.call(compareTool, map[string]string{"owner": owner, "repo": repo, "base": "main", "head": commit}) + if err != nil { + return false, err + } + var a struct { + Total *int `json:"total_commits"` + } + if err := json.Unmarshal(raw, &a); err != nil || a.Total == nil { + return false, fmt.Errorf("the answer of %s is not {total_commits}: %v", compareTool, err) + } + return *a.Total == 0, nil +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go new file mode 100644 index 0000000..fbb84d8 --- /dev/null +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/forge_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +// capturedAnswer is what the forge's tool answered (internal/check/testdata/captured/README). +func capturedAnswer(t *testing.T, name string) json.RawMessage { + t.Helper() + raw, err := os.ReadFile(filepath.Join("..", "..", "internal", "check", "testdata", "captured", name+".json")) + if err != nil { + t.Fatal(err) + } + return raw +} + +func TestTheForgeReadsParseWhatTheForgeAnswered(t *testing.T) { // 1624/AC4 + var asked []string + f := forgeReads{call: func(key string, body any) (json.RawMessage, error) { + b, _ := json.Marshal(body) + asked = append(asked, key+" "+string(b)) + switch { + case key == getFileTool: + return capturedAnswer(t, "forge-get-file-go-mod"), nil + case strings.Contains(string(b), "cb54756c3865"): + return capturedAnswer(t, "forge-compare-trunk"), nil + } + return capturedAnswer(t, "forge-compare-branch"), nil + }} + text, err := f.File(context.Background(), "novox/mesh-delivery", "modules/mesh-delivery-builder/go.mod", "main") + if err != nil || !strings.Contains(text, "git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865") { + t.Errorf("file = %q, %v", text, err) + } + if on, err := f.OnTrunk(context.Background(), "novox/mesh-sdk", "cb54756c3865"); err != nil || !on { + t.Errorf("a commit on the trunk: %v, %v", on, err) + } + if on, err := f.OnTrunk(context.Background(), "novox/mesh-sdk", "9aaa5c80ac5c"); err != nil || on { + t.Errorf("a commit of the branch: %v, %v", on, err) + } + if len(asked) != 3 || !strings.Contains(asked[0], `"owner":"novox"`) || !strings.Contains(asked[2], `"base":"main"`) { + t.Errorf("asked %v", asked) + } + bad := forgeReads{call: func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }} + if _, err := bad.File(context.Background(), "novox/x", "go.mod", "main"); err == nil { + t.Error("an answer with no file was taken") + } + if _, err := bad.OnTrunk(context.Background(), "novox/x", "abc"); err == nil { + t.Error("an answer with no total was taken") + } +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index 5767b67..cbf005b 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -150,7 +150,54 @@ func (f *fakeBuildModules) serve(t *testing.T, nc *nats.Conn, module string) { t.Cleanup(func() { _ = sub.Unsubscribe() }) } +// serveForge answers the forge's two reads as its tools do, with the answers the forge gave (internal/check/testdata/captured): +// the go.mod of the captured file, whose mesh-sdk pin is cb54756c3865, and gitea_compare for the pin: on the trunk, unless the +// pin is the one the test set as stale. +func (b *bed) serveForge(t *testing.T, nc *nats.Conn) { + reply := func(m *nats.Msg, name string) { + var result map[string]any + _ = json.Unmarshal(mustRead(t, name), &result) + answer, _ := json.Marshal(map[string]any{"result": result, "error": "", "node": "n0"}) + _ = m.Respond(answer) + } + for _, tool := range []string{"gitea_get_file", "gitea_compare"} { + tool := tool + sub, err := nc.Subscribe("mesh.mod.gitea.tool."+tool, func(m *nats.Msg) { + var a map[string]string + _ = json.Unmarshal(m.Data, &a) + switch { + case tool == "gitea_get_file" && b.stalePin.Load(): + content := strings.Replace(string(mustRead(t, "forge-get-file-go-mod")), "cb54756c3865", "9aaa5c80ac5c", 1) + var result map[string]any + _ = json.Unmarshal([]byte(content), &result) + answer, _ := json.Marshal(map[string]any{"result": result, "error": "", "node": "n0"}) + _ = m.Respond(answer) + case tool == "gitea_get_file": + reply(m, "forge-get-file-go-mod") + case strings.HasPrefix(a["head"], "9aaa5c80ac5c"): + reply(m, "forge-compare-branch") + default: + reply(m, "forge-compare-trunk") + } + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = sub.Unsubscribe() }) + } +} + +func mustRead(t *testing.T, name string) []byte { + t.Helper() + raw, err := os.ReadFile(filepath.Join(captured, name+".json")) + if err != nil { + t.Fatal(err) + } + return raw +} + type bed struct { + stalePin atomic.Bool ctx context.Context js jetstream.JetStream buckets map[string]jetstream.KeyValue @@ -231,6 +278,7 @@ func startBed(t *testing.T) *bed { for _, module := range []string{"mesh-build-go", "mesh-build-node"} { b.setHealth(t, "n1", module) } + b.serveForge(t, ctl) b.fake = &fakeBuildModules{js: js, ctx: ctx, calls: map[string]int{}} b.fake.serve(t, ctl, "mesh-build-go") b.fake.serve(t, ctl, "mesh-build-node") @@ -526,3 +574,19 @@ func TestNoCheckStartsWhileADeliveryBuildsAndOneStartsWhenItEnds(t *testing.T) { t.Errorf("verdict = %+v", v) } } + +// TestAMemberNamingAMeshSDKCommitOffTheTrunkFailsNamingTheCommit: the go.mod the forge answers names a commit that main does not +// hold; the verdict fails naming the member and the commit and no test runs (ADR 0484 clause 9). +func TestAMemberNamingAMeshSDKCommitOffTheTrunkFailsNamingTheCommit(t *testing.T) { // 1624/AC2 + b := startBed(t) + b.stalePin.Store(true) + key := b.candidate(t, "feat/x", pull(repo, 7, head1)) + b.start(t, nil) + v := b.verdict(t, key, ended) + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "9aaa5c80ac5c") || !strings.Contains(v.Failed.Words, repo+"#7") { + t.Fatalf("verdict = %+v", v) + } + if n := b.fake.count("mesh-build-go.build_go_test"); n != 0 { + t.Errorf("%d test steps ran after a stale pin", n) + } +} diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index 61fe261..b2a909f 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -23,6 +23,7 @@ type judgeDeps struct { kept check.KV previews check.KV repos *repoModules + forge check.Forge announce check.Announcer builds check.BuildStates logf func(format string, args ...any) @@ -30,7 +31,7 @@ type judgeDeps struct { func newJudge(d judgeDeps) check.Judge { return &check.Examiner{Steps: d.runner, Toolchains: d.tools, Presence: d.presence, Health: d.have, Places: d.places, - Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Logf: d.logf} + Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Forge: d.forge, Builds: d.builds, Announce: d.announce, Trunks: d.trunks, Logf: d.logf} } // manifestKV reads a manifest by its digest. diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index 7f01c48..752a6ba 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -221,7 +221,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro ch.Judge = newJudge(judgeDeps{ runner: runner, tools: tools, presence: presence, have: have, places: places, trunks: trunks, kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]}, - repos: repos, builds: builds, logf: logger.Printf, + repos: repos, builds: builds, forge: forgeReads{call: caller.Call}, logf: logger.Printf, announce: announce, }) ch.Announce = announce diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 21dc121..49af8a4 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -13,6 +13,14 @@ import ( "git.novox.be/novox/mesh-sdk/go/delivery/buildrun" ) +// goModule is the build module the Go repositories are judged by. +const goModule = buildrun.GoBuildModule + +// verdictPtr is a verdict of failure a step of the check ends with. +type verdictPtr = *delivery.Verdict + +func failed(machine, words string) verdictPtr { return fail(machine, words) } + // Steps runs a build module's long step on a node and answers its result (buildrun.Runner). type Steps interface { Run(ctx context.Context, module, node, tool string, args map[string]any) (json.RawMessage, error) @@ -50,6 +58,9 @@ type Examiner struct { // Previews is the previews bucket; nil writes none. Previews KV Modules BuildModules + // Forge reads the go.mod files of the Go members and says whether a mesh-sdk commit is on its trunk (ADR 0484 clause 9); + // nil checks no pin. + Forge Forge // Builds says how the build of a fingerprint the candidate carries stands (the builder's builds bucket); nil says nothing // of the builds. Builds BuildStates @@ -120,6 +131,9 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) { if v := st.fetchAll(ctx); v != nil { return st.finish(ctx, *v), nil } + if v := st.checkPins(ctx); v != nil { + return st.finish(ctx, *v), nil + } for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { for _, priority := range []bool{true, false} { for _, t := range st.targets { @@ -230,7 +244,7 @@ func (st *state) fetchAll(ctx context.Context) *delivery.Verdict { } raw, err := st.step(ctx, t, "fetch", fmt.Sprintf("fetch %s#%d on %s", p.Repository, p.Number, node), args) if err != nil { - return fail(node, fmt.Sprintf("fetch of %s#%d: %v", p.Repository, p.Number, err)) + return fail(node, fmt.Sprintf("fetch of %s#%d: %v%s", p.Repository, p.Number, err, pinHint(st.c, err.Error()))) } if err := json.Unmarshal(raw, &t.fetched); err != nil || t.fetched.Workspace == "" { return fail(node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, raw)) diff --git a/modules/mesh-delivery-checker/internal/check/pins.go b/modules/mesh-delivery-checker/internal/check/pins.go new file mode 100644 index 0000000..592195d --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/pins.go @@ -0,0 +1,104 @@ +package check + +import ( + "context" + "fmt" + "path" + "regexp" + "strings" +) + +// The pin of mesh-sdk in a Go member's go.mod (novox/hq ADR 0484 clause 9, ADR 0482 clause 2, issue 1624): a delivery group +// has one verdict, and the mesh-sdk its Go members are built and tested with is the group's: a pin to the head of the group's +// mesh-sdk pull request, or to a commit already on mesh-sdk's trunk. Any other commit is a stale or a foreign pin, and the +// group fails naming the member and the commit. + +// SDKRepository is the repository of mesh-sdk, and SDKModulePath its Go module. +const ( + SDKRepository = "novox/mesh-sdk" + SDKModulePath = "git.novox.be/novox/mesh-sdk/go" +) + +// Issue1148 is why a pin that only a head carries fails the check. +const Issue1148 = "issue 1148" + +// Forge reads a file of a repository at a ref, and says whether a commit is on a repository's trunk. +type Forge interface { + // File is the text of a file at a ref (a branch, a tag or a commit). + File(ctx context.Context, repository, path, ref string) (string, error) + // OnTrunk says whether commit (a full or a 12-digit sha) is already merged into the trunk of the repository: it holds no + // commit the trunk does not. + OnTrunk(ctx context.Context, repository, commit string) (bool, error) +} + +var pseudoVersion = regexp.MustCompile(`-([0-9a-f]{12})$`) + +// SDKPin is the commit (12 hex digits) a go.mod names for mesh-sdk by a pseudo-version, or none for a go.mod that does not +// require mesh-sdk or requires a version that is no pseudo-version (a tag is a release the SDK itself names). +func SDKPin(goMod string) (commit string, ok bool) { + for _, line := range strings.Split(goMod, "\n") { + f := strings.Fields(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "require"))) + if len(f) >= 2 && f[0] == SDKModulePath { + if m := pseudoVersion.FindStringSubmatch(f[1]); m != nil { + return m[1], true + } + return "", false + } + } + return "", false +} + +// checkPins refuses a Go member whose go.mod names a mesh-sdk commit that is neither on mesh-sdk's trunk nor the head of the +// group's mesh-sdk pull request. It reads each go.mod the fetch named at the member's head from the forge. It answers nil +// when every pin is the group's. +func (st *state) checkPins(ctx context.Context) verdictPtr { + if st.j.Forge == nil { + return nil + } + sdk := st.c.member(SDKRepository) + for _, t := range st.targets { + if t.module != goModule || t.pull.Repository == SDKRepository { + continue + } + for _, dir := range t.fetched.Modules { + file := path.Join(dir, "go.mod") + st.c.Progress.Set(fmt.Sprintf("read %s of %s#%d", file, t.pull.Repository, t.pull.Number), t.node) + text, err := st.j.Forge.File(ctx, t.pull.Repository, file, t.pull.Head) + if err != nil { + return failed(t.node, fmt.Sprintf("%s#%d: %s cannot be read to check its mesh-sdk pin: %v", t.pull.Repository, t.pull.Number, file, err)) + } + pin, ok := SDKPin(text) + if !ok { + continue + } + if sdk != nil && strings.HasPrefix(sdk.Head, pin) { + continue // the group's own head + } + onTrunk, err := st.j.Forge.OnTrunk(ctx, SDKRepository, pin) + if err != nil { + return failed(t.node, fmt.Sprintf("%s#%d: whether mesh-sdk %s is on its trunk cannot be told: %v", t.pull.Repository, t.pull.Number, pin, err)) + } + if onTrunk { + continue + } + nor := "the group has no mesh-sdk pull request" + if sdk != nil { + nor = "the head of the group's mesh-sdk pull request is " + short12(sdk.Head) + } + return failed(t.node, fmt.Sprintf("%s#%d: %s names mesh-sdk %s, which is neither on mesh-sdk's trunk nor the group's head (%s)", t.pull.Repository, t.pull.Number, file, pin, nor)) + } + } + return nil +} + +// failureVerdict is a verdict of failure the check ends with. + +// pinHint is added to the failure of a Go member's fetch that names mesh-sdk while the group holds a mesh-sdk pull request: +// a pin to that pull request's head names a commit only the head carries, which go mod download cannot resolve before the +// pull request is merged (issue 1148), so such a check fails and never passes. +func pinHint(c *Case, fetchError string) string { + if c.member(SDKRepository) == nil || !strings.Contains(fetchError, "mesh-sdk") { + return "" + } + return " (a pin to the head of the group's mesh-sdk pull request names a commit only that head carries, which go mod download cannot resolve; " + Issue1148 + ")" +} diff --git a/modules/mesh-delivery-checker/internal/check/pins_test.go b/modules/mesh-delivery-checker/internal/check/pins_test.go new file mode 100644 index 0000000..3ffdc06 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/pins_test.go @@ -0,0 +1,147 @@ +package check + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// fakeForge answers the forge's two reads from files and commits it is given. +type fakeForge struct { + goMods map[string]string // ` ` -> text + onTrunk map[string]bool // by commit prefix + err error +} + +func (f *fakeForge) File(_ context.Context, repository, path, ref string) (string, error) { + if f.err != nil { + return "", f.err + } + if s, ok := f.goMods[repository+" "+path+" "+ref]; ok { + return s, nil + } + return "", fmt.Errorf("no file %s of %s at %s", path, repository, ref) +} + +func (f *fakeForge) OnTrunk(_ context.Context, _ string, commit string) (bool, error) { + if f.err != nil { + return false, f.err + } + return f.onTrunk[commit[:12]], nil +} + +// capturedGoMod is the go.mod the forge answered (testdata/captured/forge-get-file-go-mod.json), pinned to the commit given. +func capturedGoMod(t *testing.T, pin string) string { + t.Helper() + raw, err := os.ReadFile(filepath.Join("testdata", "captured", "forge-get-file-go-mod.json")) + if err != nil { + t.Fatal(err) + } + var a struct{ File struct{ Content string } } + if err := json.Unmarshal(raw, &a); err != nil { + t.Fatal(err) + } + return strings.Replace(a.File.Content, "cb54756c3865", pin, 1) +} + +func TestTheSDKPinIsTheCommitOfAPseudoVersion(t *testing.T) { // 1624 + if c, ok := SDKPin(capturedGoMod(t, "cb54756c3865")); !ok || c != "cb54756c3865" { + t.Errorf("pin = %q, %v", c, ok) + } + if _, ok := SDKPin("module x\n\nrequire git.novox.be/novox/mesh-sdk/go v0.2.0\n"); ok { + t.Error("a tagged version is no commit") + } + if c, ok := SDKPin("module x\n\nrequire git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865\n"); !ok || c != "cb54756c3865" { + t.Errorf("a one-line require: %q, %v", c, ok) + } + if _, ok := SDKPin("module x\n\ngo 1.26\n"); ok { + t.Error("a go.mod that does not require mesh-sdk has a pin") + } +} + +// groupBench is a group of mesh-delivery#7 and the SDK's pull request 12, mesh-delivery's go.mod at its head naming pin. +func groupBench(t *testing.T, pin string, trunk ...string) *bench { + b := newBench(t) + b.fetch.Modules = []string{"modules/m"} + forge := &fakeForge{goMods: map[string]string{repoA + " modules/m/go.mod " + head1: capturedGoMod(t, pin)}, onTrunk: map[string]bool{}} + for _, c := range trunk { + forge.onTrunk[c] = true + } + b.ex.Forge = forge + return b +} + +func TestAMemberPinnedToTheSDKMembersHeadPassesAndIsBuiltAgainstIt(t *testing.T) { // 1624/AC2, AC4 + b := groupBench(t, head2[:12]) + v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict + if v.State != delivery.VerdictPass { + t.Fatalf("verdict = %+v", v) + } + var besideSDK bool + for _, f := range b.steps.tool("fetch") { + if f.args["repository"] == repoA { + refs, _ := f.args["beside"].([]BesideRef) + for _, r := range refs { + besideSDK = besideSDK || (r.Repository == repoSDK && r.Commit == head2) + } + } + } + if !besideSDK { + t.Error("mesh-delivery's tests were not given the SDK member at its head") + } +} + +func TestAMemberPinnedToAnOlderHeadOfTheSDKMemberFailsNamingTheMemberAndTheCommit(t *testing.T) { // 1624/AC2, AC4 + b := groupBench(t, "9aaa5c80ac5c") // a commit of the SDK branch, not its head, not on the trunk + v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict + f := v.Failed + if v.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, repoA+"#7") || !strings.Contains(f.Words, "9aaa5c80ac5c") || + !strings.Contains(f.Words, "neither on mesh-sdk's trunk nor the group's head") || !strings.Contains(f.Words, short12(head2)) { + t.Fatalf("verdict = %+v", v) + } + if n := len(b.steps.tool("test")); n != 0 { + t.Errorf("%d test steps ran after a stale pin", n) + } +} + +func TestAMemberPinnedToACommitOnTheTrunkPasses(t *testing.T) { // 1624/AC2, AC4 + b := groupBench(t, "cb54756c3865", "cb54756c3865") + if v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict; v.State != delivery.VerdictPass { + t.Fatalf("verdict = %+v", v) + } + // A lone pull request of a Go member is held to the trunk alone. + b = groupBench(t, "9aaa5c80ac5c") + v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "the group has no mesh-sdk pull request") { + t.Errorf("a foreign pin with no SDK member: %+v", v) + } +} + +func TestAPinOnlyTheSDKHeadCarriesThatDownloadCannotResolveFailsNamingIssue1148(t *testing.T) { // 1624/AC5, ADR 0453 clause 12 + b := groupBench(t, head2[:12]) + b.steps.handlers["build_go_fetch"] = func(c stepCall) (any, error) { + if c.args["repository"] == repoA { + return nil, fmt.Errorf("go mod download: git.novox.be/novox/mesh-sdk/go@v0.1.14-0.20261021100209-%s: invalid version: unknown revision", head2[:12]) + } + return b.fetch, nil + } + v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "issue 1148") || !strings.Contains(v.Failed.Words, head2[:12]) { + t.Errorf("verdict = %+v, want a failure naming issue 1148 and the commit", v) + } +} + +func TestAPinThatCannotBeCheckedFailsAndNeverPasses(t *testing.T) { // ADR 0453 clause 12 + b := groupBench(t, "cb54756c3865") + b.ex.Forge.(*fakeForge).err = fmt.Errorf("the forge is down") + v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict + if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "the forge is down") { + t.Errorf("verdict = %+v", v) + } +} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/README b/modules/mesh-delivery-checker/internal/check/testdata/captured/README index b8023fe..9c809ea 100644 --- a/modules/mesh-delivery-checker/internal/check/testdata/captured/README +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/README @@ -11,3 +11,9 @@ into a throwaway test that wrote them out, at mesh-catalog feat/r4c5-checker d58 so the shas are its constants; the shape and the field names are the module's own marshalling). node-fetch-clean.json is the FetchResult json.Marshal of mesh-build-node's fetch (fetch.go, preview.go) in its test TestFetchWithABaseMergesAndAnswersThePreview (cmd/mesh-build-node/preview_test.go) at the same commit. Captured on 2026-10-21 for issue 1679. + +forge-get-file-go-mod.json is the answer of the forge's gitea_get_file (mesh-catalog modules/gitea, tools/index.ts) for modules/mesh-delivery-builder/go.mod of +novox/mesh-delivery at main, and forge-compare-trunk.json the answer of gitea_compare for novox/mesh-sdk base main, head cb54756c3865 (a commit on the +trunk: no commit main does not hold); both called through the mesh on 2026-10-21 for issue 1624. forge-compare-branch.json is the answer of the same call for a +head that is not on the trunk (novox/mesh-sdk base main, head afbe371003a5, the branch feat/r4c5-sdk), shortened to two of its seven commits and one of its +twenty-six files, with the head and the totals set to match. diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-branch.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-branch.json new file mode 100644 index 0000000..f8d5829 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-branch.json @@ -0,0 +1,31 @@ +{ + "base": "main", + "commits": [ + { + "author": "jochen", + "date": "2026-10-21T19:35:14+02:00", + "files": 2, + "sha": "9aaa5c80ac5c4820c4eddf488fbb90ac6981a624", + "subject": "Accept the gate's recheck status mesh/repo-check among the owed contexts, so a pinned recheck is not refused (issue 1673)" + }, + { + "author": "jochen", + "date": "2026-10-21T19:26:02+02:00", + "files": 3, + "sha": "0730ef609216798e787ee8b6542912f5ec613a77", + "subject": "Keep the unit level cheap: the merge's real-git tests start processes, so they are integration tests (issue 895)" + } + ], + "files": [ + { + "path": "go/delivery/gate.go", + "status": "modified" + } + ], + "head": "9aaa5c80ac5c", + "repository": "novox/mesh-sdk", + "said": [ + "9aaa5c80ac5c holds 2 commits main does not, touching 1 files" + ], + "total_commits": 2 +} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-trunk.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-trunk.json new file mode 100644 index 0000000..21dff8b --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-compare-trunk.json @@ -0,0 +1,12 @@ +{ + "base": "main", + "commits": [], + "files": [], + "head": "cb54756c3865", + "repository": "novox/mesh-sdk", + "said": [ + "cb54756c3865 holds 0 commits main does not, touching 0 files", + "a file's status is the last listed commit's change to it; for the lines, gitea_pull_request_diff or gitea_get_file" + ], + "total_commits": 0 +} diff --git a/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-get-file-go-mod.json b/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-get-file-go-mod.json new file mode 100644 index 0000000..a591e7e --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/testdata/captured/forge-get-file-go-mod.json @@ -0,0 +1,9 @@ +{ + "file": { + "content": "module mesh-delivery-builder\n\ngo 1.26.0\n\nrequire (\n\tgit.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865\n\tgithub.com/nats-io/nats-server/v2 v2.11.17\n\tgithub.com/nats-io/nats.go v1.54.0\n)\n\nrequire (\n\tgithub.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect\n\tgithub.com/google/go-tpm v0.9.8 // indirect\n\tgithub.com/klauspost/compress v1.20.0 // indirect\n\tgithub.com/minio/highwayhash v1.0.4 // indirect\n\tgithub.com/nats-io/jwt/v2 v2.8.1 // indirect\n\tgithub.com/nats-io/nkeys v0.4.16 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgolang.org/x/crypto v0.57.0 // indirect\n\tgolang.org/x/sys v0.48.0 // indirect\n\tgolang.org/x/time v0.15.0 // indirect\n)\n", + "path": "modules/mesh-delivery-builder/go.mod", + "ref": "main", + "sha": "4431597e2e4d2a26b59006f9f8c54659a964ec68", + "size": 691 + } +} diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index 9d7eebb..5370e5c 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -100,8 +100,8 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules", "mesh-delivery-watchman.presence", "node-engine.have"}; !reflect.DeepEqual(sorted(m.Reads), want) { t.Errorf("reads = %v, want %v", m.Reads, want) } - if want := []string{"mesh-build-go.build_go_digest", "mesh-build-go.build_go_fetch", "mesh-build-go.build_go_test", "mesh-build-node.build_node_fetch", "mesh-build-node.build_node_test"}; !reflect.DeepEqual(sorted(m.Invokes), want) { - t.Errorf("invokes = %v, want only the build modules' fetch, digest and test (no run step before issue 1309, ADR 0484 clause 5)", m.Invokes) + if want := []string{"gitea.gitea_compare", "gitea.gitea_get_file", "mesh-build-go.build_go_digest", "mesh-build-go.build_go_fetch", "mesh-build-go.build_go_test", "mesh-build-node.build_node_fetch", "mesh-build-node.build_node_test"}; !reflect.DeepEqual(sorted(m.Invokes), want) { + t.Errorf("invokes = %v, want only the build modules' fetch, digest and test (no run step before issue 1309, ADR 0484 clause 5) and the forge's two reads for a mesh-sdk pin (clause 9)", m.Invokes) } // The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts. if want := []string{"mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported"}; !reflect.DeepEqual(sorted(m.Consumes), want) { diff --git a/modules/mesh-delivery-checker/module.json b/modules/mesh-delivery-checker/module.json index 15e2cc7..80b7351 100644 --- a/modules/mesh-delivery-checker/module.json +++ b/modules/mesh-delivery-checker/module.json @@ -35,6 +35,8 @@ "checker_verdicts" ], "invokes": [ + "gitea.gitea_compare", + "gitea.gitea_get_file", "mesh-build-go.build_go_digest", "mesh-build-go.build_go_fetch", "mesh-build-go.build_go_test",