From 7214bf436828ea6a48fdd3302b11ac812f66e7a2 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 21:50:30 +0200 Subject: [PATCH] Fail a candidate's build when its fetch answers files in conflict or no merge, so the trunk's build is not stored under the candidate's fingerprint (review D4) --- .../mesh-delivery-builder/integration_test.go | 6 +++++- .../internal/build/candidates_test.go | 18 ++++++++++++++++++ .../internal/build/run.go | 16 +++++++++++++++- .../internal/build/run_test.go | 14 ++++++++++++-- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index d7b8fab..7affcfc 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -109,7 +109,11 @@ func (f *fakeBuildModules) serve(t *testing.T, nc *nats.Conn, module string) { step.Ended = time.Now().UTC() switch { case strings.HasSuffix(tool, "_fetch"): - step.State, step.Result = delivery.StepPassed, json.RawMessage(`{"workspace":"w`+id+`","commit":"`+commit+`"}`) + merge := "" + if args["base"] != nil { // a fetch with a base answers the merge commit (mesh-build-go FetchResult) + merge = `,"merge":"` + strings.Repeat("9", 40) + `"` + } + step.State, step.Result = delivery.StepPassed, json.RawMessage(`{"workspace":"w`+id+`","commit":"`+commit+`"`+merge+`}`) case f.failBuild.Load() && node == "n1": step.State, step.Reason = delivery.StepFailed, fmt.Sprintf("go build of %v: boom", args["artifact"]) default: diff --git a/modules/mesh-delivery-builder/internal/build/candidates_test.go b/modules/mesh-delivery-builder/internal/build/candidates_test.go index acc5976..7c26902 100644 --- a/modules/mesh-delivery-builder/internal/build/candidates_test.go +++ b/modules/mesh-delivery-builder/internal/build/candidates_test.go @@ -2,6 +2,7 @@ package build import ( "context" + "strings" "testing" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -94,3 +95,20 @@ func TestAFingerprintTheTrunkCarriesIsBuiltFromTheTrunksCommit(t *testing.T) { / t.Errorf("build = %+v", v) } } + +func TestACandidateWhoseHeadDoesNotMergeIsFailedAndNoTrunkBuildIsStoredUnderIt(t *testing.T) { // review D4 + for name, set := range map[string]func(*rig){ + "conflict": func(r *rig) { r.bm.conflicts = "go.mod" }, + "no merge": func(r *rig) { r.bm.noMerge = true }, + } { + r := newRig(t, goBundle) + trunkModule(t, r) + set(r) + r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) + r.b.Wait() + v := valueOf(t, r.st, r.key) + if v.State != delivery.BuildFailed || (name == "conflict" && !strings.Contains(v.Error, "go.mod")) || len(r.bm.callsOf("build_go_build")) != 0 { + t.Errorf("%s: build = %+v, builds %d, want failed and nothing built", name, v, len(r.bm.callsOf("build_go_build"))) + } + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 495027f..5f7782d 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -524,12 +524,26 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m del return } var fetched struct { - Workspace string `json:"workspace"` + Workspace string `json:"workspace"` + Merge string `json:"merge"` + Conflicts []string `json:"conflicts"` } if err := json.Unmarshal(raw, &fetched); err != nil || fetched.Workspace == "" { l.err = fmt.Errorf("the fetch on %s answered no workspace: %s", l.node, raw) return } + // A candidate is built from its head merged onto the trunk. A head that does not merge ends the fetch passed with the tree at + // the trunk, and building that would store the trunk's build under the candidate's fingerprint. + if _, candidate := fetchArgs["base"]; candidate { + if len(fetched.Conflicts) > 0 { + l.err = fmt.Errorf("the head does not merge into the trunk on %s: files in conflict: %s", l.node, strings.Join(fetched.Conflicts, ", ")) + return + } + if fetched.Merge == "" { + l.err = fmt.Errorf("the fetch with a base on %s answered no merge: %s", l.node, raw) + return + } + } for _, t := range l.tasks { raw, err := b.step(ctx, l.module, l.node, "build", map[string]any{"workspace": fetched.Workspace, "module": m.Path, "artifact": t.Artifact}) if err != nil { diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index 9764790..73812c5 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -24,8 +24,11 @@ type buildModules struct { digests map[string]string // `/` -> digest; the default is `digest` fails map[string]string // `/` -> the reason the step fails with // hold, when set before the builds start, blocks every call until it is closed; entered is sent to as a call arrives. - hold chan struct{} - entered chan struct{} + // conflicts, when set, is a file a fetch with a base reports in conflict; noMerge makes it answer neither a merge nor a conflict. + conflicts string + noMerge bool + hold chan struct{} + entered chan struct{} } func newBuildModules() *buildModules { @@ -52,6 +55,13 @@ func (b *buildModules) CallOn(node, key string, body any) (json.RawMessage, erro step.State, step.Reason = delivery.StepFailed, reason } else if strings.HasSuffix(tool, "_fetch") { step.Result = json.RawMessage(`{"workspace":"w-` + node + `"}`) + if args["base"] != nil { // a fetch with a base answers the merge, or the files in conflict (mesh-build-go FetchResult) + if b.conflicts != "" { + step.Result = json.RawMessage(`{"workspace":"w-` + node + `","base":"` + fmt.Sprint(args["base"]) + `","conflicts":["` + b.conflicts + `"]}`) + } else if !b.noMerge { + step.Result = json.RawMessage(`{"workspace":"w-` + node + `","base":"` + fmt.Sprint(args["base"]) + `","merge":"` + strings.Repeat("9", 40) + `"}`) + } + } } else { d := digest if v, ok := b.digests[node+"/"+fmt.Sprint(args["artifact"])]; ok {