From 39ae7de6d30edc2537cf1e0c7bcfb0218aece0ed Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 19 Oct 2026 00:19:03 +0200 Subject: [PATCH] Say a first merge that waits past ten minutes for mesh-delivery's word, with probe D17 (issue 1146, AC8) --- cmd/mesh-controller/doctor.go | 5 +++ cmd/mesh-controller/first_builds.go | 49 ++++++++++++++++++++++++ cmd/mesh-controller/first_builds_test.go | 32 ++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/cmd/mesh-controller/doctor.go b/cmd/mesh-controller/doctor.go index 4358b135..d9e436c4 100644 --- a/cmd/mesh-controller/doctor.go +++ b/cmd/mesh-controller/doctor.go @@ -120,6 +120,11 @@ var probeRegistry = []probe{ {ID: probeDeliveriesID, Asserts: "no delivery is held past its state's bound unsaid: mesh-delivery's " + "`stalled`, each with the transition its table lets healer H2 take", From: "ADR 0239", Kind: kindDeliveryStalled, Phase: 3, run: probeDeliveries}, + // A first merge into a repository the mesh builds nothing from, kept waiting for mesh-delivery's word on the + // operator's Approve (novox/hq ADR 0432, issue 1146): said past a bound, never only in a log line (ADR 0426 §3, §8). + {ID: probeFirstBuildsID, Asserts: "no first merge into a repository the mesh builds nothing from waits more than ten " + + "minutes for mesh-delivery's announcement of the operator's Approve of its head", From: "ADR 0432, issue 1146", + Kind: kindFirstBuildUnasked, Phase: 3, run: probeFirstBuilds}, // The delivery budgets (novox/hq ADR 0282 decision 7): the newest delivery of each class within its budget, // read from mesh-delivery's `times`; a measurement said, never a delivery held. {ID: probeBudgetsID, Asserts: "the newest delivery of a leaf module ran on every machine within three minutes of " + diff --git a/cmd/mesh-controller/first_builds.go b/cmd/mesh-controller/first_builds.go index 31624282..aeda9d21 100644 --- a/cmd/mesh-controller/first_builds.go +++ b/cmd/mesh-controller/first_builds.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -161,3 +162,51 @@ func firstBuildsOf(ctx context.Context, inv catalogueReader, m link.SourceMoved, // Not announced yet, or the bus or the store could not be asked: the caller waits or tries again, and asks nothing. return nil, "", err } + +// probeFirstBuildsID is the probe that reads the first merges waiting for mesh-delivery's word. +const probeFirstBuildsID = "D17" + +// kindFirstBuildUnasked is what a first merge waiting too long for mesh-delivery's word raises. +const kindFirstBuildUnasked = "first-build-unasked" + +// firstBuildWaitBound is how long a first merge waits for mesh-delivery's announcement of the operator's Approve before +// it is said: a bound in the sense of ADR 0426 §3, not a delay. Nothing waits on it: the word's arrival asks the builds +// at once, and the merge is said, never dropped, once it is past this (issue 1146). +const firstBuildWaitBound = 10 * time.Minute + +// firstBuildObservations are the conditions of the first merges waiting past the bound, one for each; a merge answered is +// no longer listed as waiting, so its condition clears with the next look. +func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []conditions.Observation { + var out []conditions.Observation + for _, w := range waiting { + k := announcedOf(w) + if !k.FirstBuild || now.Sub(w.Heard) <= firstBuildWaitBound { + continue + } + repository := k.Owner + "/" + k.Repo + for_ := now.Sub(w.Heard).Round(time.Minute) + out = append(out, conditions.Observation{Scope: conditions.ScopeMerge, + ID: fmt.Sprintf("%s_%.8s", repository, k.Commit), Kind: kindFirstBuildUnasked, Severity: conditions.Warning, + Summary: fmt.Sprintf("the merge %.8s of %s adds modules the mesh builds nothing from yet, and has waited %s for "+ + "mesh-delivery's announcement of the operator's Approve of its head: no build is asked", k.Commit, repository, for_), + Said: fmt.Sprintf("%s at %s waits for mesh-delivery's word on pull request %d (head %.8s)", repository, k.Commit, + k.Number, k.HeadSHA), + Headline: fmt.Sprintf("The first build of %s is not asked: no Approve was announced", repository), + Explanation: fmt.Sprintf("The merge %.8s into %s adds modules the mesh builds nothing from yet. It registers them "+ + "only when mesh-delivery announces that it merged the pull request on your Approve of that head, and no such "+ + "announcement has come in %s. The merge is kept, and the builds are asked the moment the announcement is kept.", + k.Commit, repository, for_), + Resolved: "mesh-delivery announces the merge, or a hand `build` registers the modules", + }) + } + return out +} + +// probeFirstBuilds is D17: no first merge waits past its bound for mesh-delivery's word unsaid. +func probeFirstBuilds(ctx context.Context, d *doctor) ([]conditions.Observation, error) { + waiting, err := d.open.inventory.WaitingMerges(ctx) + if err != nil { + return nil, err + } + return firstBuildObservations(waiting, time.Now().UTC()), nil +} diff --git a/cmd/mesh-controller/first_builds_test.go b/cmd/mesh-controller/first_builds_test.go index fa22e3ed..7a6b2d14 100644 --- a/cmd/mesh-controller/first_builds_test.go +++ b/cmd/mesh-controller/first_builds_test.go @@ -2,12 +2,15 @@ package main import ( "context" + "encoding/json" "errors" "slices" + "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/asks" + "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -72,3 +75,32 @@ func TestAFirstMergeIntoANewRepositoryAsksItsModulesBuildsOnlyOnTheApprove(t *te }) } } + +// 1146/AC8: a first merge waiting past the bound for mesh-delivery's word is said, naming the repository, the commit and +// the missing word; one inside the bound, one that is no first merge, and one answered (no longer waiting) are not. +func TestAFirstMergeWaitingPastItsBoundIsSaidAndClearsWhenAnswered(t *testing.T) { + now := time.Date(2026, 10, 19, 1, 0, 0, 0, time.UTC) + waiting := func(commit string, first bool, heard time.Time) inventory.BatchedMerge { + event, _ := json.Marshal(keptMerge{SourceMoved: link.SourceMoved{Owner: "novox", Repo: "mesh-delivery", Base: "main", + Commit: commit, Number: 1, HeadSHA: "a1a1a1a1a1a1"}, FirstBuild: first}) + return inventory.BatchedMerge{Repository: "novox/mesh-delivery", Branch: "main", Commit: commit, Heard: heard, Event: event} + } + got := firstBuildObservations([]inventory.BatchedMerge{ + waiting("0177ea7a00", true, now.Add(-firstBuildWaitBound-time.Minute)), + waiting("1111111100", true, now.Add(-time.Minute)), + waiting("2222222200", false, now.Add(-time.Hour)), + }, now) + if len(got) != 1 { + t.Fatalf("1146/AC8: %d conditions raised, want the one merge past the bound: %+v", len(got), got) + } + o := got[0] + if o.Kind != kindFirstBuildUnasked || o.Scope != conditions.ScopeMerge || o.Severity != conditions.Warning || + !strings.Contains(o.Summary, "novox/mesh-delivery") || !strings.Contains(o.Summary, "0177ea7a") || + !strings.Contains(o.Summary, "announcement of the operator's Approve") { + t.Fatalf("1146/AC8: the condition is %+v", o) + } + // Answered, the merge is no longer listed as waiting: nothing is observed, and the condition clears. + if cleared := firstBuildObservations(nil, now); len(cleared) != 0 { + t.Fatalf("1146/AC8: an answered merge still raises %+v", cleared) + } +}