From 2178db5a1d316638f216969f11ebf02670bc375c Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:43:51 +0100 Subject: [PATCH] DLV-B7: a failed trunk build is the builder's finding build-failed, cleared by a later done build Read from the builder's own build log entries at their end; a candidate's build is the check's to say. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- .../cmd/mesh-delivery-builder/findings.go | 45 +++++++++++++++-- .../cmd/mesh-delivery-builder/main.go | 4 +- .../cmd/mesh-delivery-builder/refused_test.go | 50 +++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go index 4b1d4536..dc45246d 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go @@ -21,6 +21,15 @@ type findings struct { kv *process.AwaitedBucket logf func(string, ...any) now func() time.Time + // store stands in for kv in a test. + store findingStore +} + +// findingStore is the part of the findings bucket written. +type findingStore interface { + Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error) + Put(ctx context.Context, key string, value []byte) (uint64, error) + Delete(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error } // finding is one of the builder's findings: its kind, what it is about, and its words. @@ -30,11 +39,41 @@ type finding struct { severity health.Severity } -func (f *findings) bucket() jetstream.KeyValue { - if f == nil || f.kv == nil { +func (f *findings) bucket() findingStore { + switch { + case f == nil: + return nil + case f.store != nil: + return f.store + case f.kv == nil: return nil } - return f.kv.KV() + if kv := f.kv.KV(); kv != nil { + return kv + } + return nil +} + +// BuildFailed is the finding of a failed trunk build (DLV-B7, design 25), about its module. +const BuildFailed = "build-failed" + +// buildEnded raises the module's build-failed for a trunk build that failed and clears it at a later done one; a +// candidate's build (its ref named) is the check's to say. +func (f *findings) buildEnded(ctx context.Context, e delivery.BuildLogEntry) { + if e.Event != delivery.BuildEndedEvent || e.Ref != "" { + return + } + switch e.Outcome { + case delivery.BuildFailed: + f.raise(ctx, finding{kind: BuildFailed, scope: health.ScopeModule, id: e.Module, + summary: "the trunk build of " + e.Module + " at " + e.FP + " (commit " + e.Commit + ") failed", + headline: "A module's trunk build failed", + explanation: "The builder built " + e.Module + " from the trunk and the build failed, so its new version is not delivered.", + resolved: "A later trunk build of " + e.Module + " is done.", + needs: "builder_builds_since shows the build; builder_build_again builds it again."}) + case delivery.BuildDone: + f.clear(ctx, BuildFailed, health.ScopeModule, e.Module) + } } // raise writes fd; it is written again only when its words changed, so the keeper's condition moves only then. diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index 2cf8ea8f..1e53357d 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -168,10 +168,13 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro b.Trunk = trunks.head // The builder's log (ADR 0571 ยง1): each build's start and end in its module log `build-log`, read by the view, the // indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since. + found := &findings{kv: process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings are said in its log only", logger.Printf), logf: logger.Printf, now: time.Now} b.Log = func(e delivery.BuildLogEntry) { if err := appendLog(context.WithoutCancel(ctx), js, cred, e); err != nil { logger.Printf("%v", err) } + // A failed trunk build is the builder's finding build-failed, cleared by a later done one (DLV-B7). + found.buildEnded(context.WithoutCancel(ctx), e) } b.Said = func(e build.AskedAgain) { if err := emitAskedAgain(context.WithoutCancel(ctx), js, cred, e); err != nil { @@ -314,7 +317,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // A branch's end removes its tagged versions (design 51 *Tagged versions*, DLV-B6): the forge bridge's merged, a pull's // closed or branch-deleted on the builder's durable consumer of its stream; each forge message also wakes a floor whose // bridge_tags was silent. The checker's tag-wanted is kept in tag-asks. A consumer not there yet is said. - found := &findings{kv: process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings are said in its log only", logger.Printf), logf: logger.Printf, now: time.Now} ends := &branchEnds{acts: acts, findings: found, tagAsks: tagAsksKV.KV, logf: logger.Printf, now: time.Now, wake: func(ctx context.Context) { (&floorWake{b: b, waits: floorWaits, logf: logger.Printf}).wake(ctx, versions.WakeForge, "", "") diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go index 43a26a25..067e26aa 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/process" "github.com/nats-io/nats.go" @@ -27,6 +28,7 @@ import ( // issue 774). func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) { t.Run("a floor's wake is heard on the events consumer (design 51 The floor)", aFloorsWakeIsHeard) + t.Run("a failed trunk build is a finding until a later done one (DLV-B7)", aFailedTrunkBuildIsAFinding) t.Run("a branch's end is acknowledged once its tagged versions are removed (design 51 Tagged versions)", aBranchsEndIsHeard) set := &buildrun.ToolchainSet{} var logs bytes.Buffer @@ -194,3 +196,51 @@ type failingVersions struct{ noVersions } func (failingVersions) KeysOf(context.Context, string) ([]string, error) { return nil, errors.New("down") } + +func aFailedTrunkBuildIsAFinding(t *testing.T) { + st := &memFindings{vals: map[string][]byte{}} + f := &findings{store: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }} + ended := func(outcome, ref string) delivery.BuildLogEntry { + return delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Ref: ref, Outcome: outcome} + } + key := health.FindingKey(health.ScopeModule, "m", BuildFailed) + f.buildEnded(context.Background(), ended(delivery.BuildFailed, "refs/pull/3/head")) + if len(st.vals) != 0 { + t.Fatalf("a candidate's failed build is a finding: %v", st.vals) + } + f.buildEnded(context.Background(), ended(delivery.BuildFailed, "")) + got, err := health.DecodeFinding(st.vals[key]) + if err != nil || got.Observer != delivery.BuilderModule || !strings.Contains(got.Summary, "trunk build of m at f1") { + t.Fatalf("the finding = %+v, %v", got, err) + } + f.buildEnded(context.Background(), ended(delivery.BuildDone, "")) + if _, ok := st.vals[key]; ok { + t.Error("a later done trunk build leaves build-failed") + } +} + +// memFindings is a findings bucket in memory. +type memFindings struct{ vals map[string][]byte } + +type memEntry struct { + jetstream.KeyValueEntry + v []byte +} + +func (e memEntry) Value() []byte { return e.v } + +func (m *memFindings) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) { + v, ok := m.vals[key] + if !ok { + return nil, jetstream.ErrKeyNotFound + } + return memEntry{v: v}, nil +} +func (m *memFindings) Put(_ context.Context, key string, v []byte) (uint64, error) { + m.vals[key] = v + return 1, nil +} +func (m *memFindings) Delete(_ context.Context, key string, _ ...jetstream.KVDeleteOpt) error { + delete(m.vals, key) + return nil +}