Keep a first merge whose manifest word the running build-agent does not read, and name it (issue 1173)

ADR 0482 section 5: a merged manifest the running reader refuses waits for the newer reader, never failed.
This commit is contained in:
2026-10-22 16:18:39 +02:00
parent b8f77577fe
commit 980d396d40
7 changed files with 203 additions and 1 deletions
+92 -1
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
@@ -102,7 +103,7 @@ func (f following) resumeFirstBuilds(ctx context.Context, d deliveryMerge) error
}
for _, w := range waiting {
k := announcedOf(w)
if !k.FirstBuild || !strings.EqualFold(k.Owner+"/"+k.Repo, d.Owner+"/"+d.Repo) || k.Number != d.Number ||
if !k.FirstBuild || k.Held != nil || !strings.EqualFold(k.Owner+"/"+k.Repo, d.Owner+"/"+d.Repo) || k.Number != d.Number ||
k.HeadSHA != d.HeadSHA {
continue
}
@@ -172,12 +173,79 @@ func firstBuildsOf(ctx context.Context, inv catalogueReader, m link.SourceMoved,
return nil, "", err
}
// heldFirstBuild says a first build was refused because its manifest uses a word the running build-agent does not read,
// and its merge is kept waiting for a build-agent that reads it (novox/hq ADR 0482 §5, issue 1173).
type heldFirstBuild struct {
Module string `json:"module"`
Word string `json:"word"`
At time.Time `json:"at"`
}
// judgeRefusedFirstBuild is the hold a first build's outcome puts its merge under: nil unless the build failed on a word
// of its manifest the running build-agent does not know.
func judgeRefusedFirstBuild(result link.BuildResult, req inventory.BuildRequest, merge inventory.BatchedMerge) *heldFirstBuild {
word := refusedWord(result.Failed)
if word == "" || req.For != "merge" || merge.Plan != inventory.WaitingRecord {
return nil
}
return &heldFirstBuild{Module: req.Name(), Word: word, At: time.Now().UTC()}
}
// unknownWord is how the strict reading of a manifest says a word is not one it knows: encoding/json's own words, which
// the build-agent's answer carries whole. The build's answer has no typed field for it (Failed is the builder's words).
var unknownWord = regexp.MustCompile(`json: unknown field "([^"]+)"`)
// refusedWord is the word a build's failure says the reader did not know; empty when it says none.
func refusedWord(failed string) string {
if m := unknownWord.FindStringSubmatch(failed); m != nil {
return m[1]
}
return ""
}
// holdRefusedFirstBuild keeps a first build's merge waiting when the build was refused for a word the running build-agent
// does not read (novox/hq ADR 0482 §5, issue 1173): no failed build is recorded, nothing expires the merge, and it is
// named by a condition until a delivery of build-agent asks the build again. Answers whether the outcome was dealt with.
func holdRefusedFirstBuild(ctx context.Context, inv *inventory.Inventory, result link.BuildResult) (bool, error) {
if result.Failed == "" || refusedWord(result.Failed) == "" {
return false, nil
}
req, found, err := inv.BuildRequestByID(ctx, result.ID)
if err != nil || !found {
return false, err
}
merge, found, err := inv.MergeOf(ctx, req.Repository, req.Commit)
if err != nil || !found {
return false, err
}
held := judgeRefusedFirstBuild(result, req, merge)
if held == nil {
return false, nil
}
k := announcedOf(merge)
k.Held = held
event, err := json.Marshal(k)
if err != nil {
return false, err
}
if err := inv.HoldMerge(ctx, merge.Repository, merge.Commit, event); err != nil {
return false, err
}
fmt.Printf("%s: the first build of %s was refused for the word %q, which the running build-agent does not read; the "+
"merge %.8s of %s is kept waiting for a build-agent that reads it, and the build is asked again when one is delivered\n",
result.ID, held.Module, held.Word, merge.Commit, merge.Repository)
return true, nil
}
// probeFirstBuildsID is the probe that reads the first merges waiting for mesh-delivery's word.
const probeFirstBuildsID = "D18"
// kindFirstBuildUnasked is what a first merge waiting too long for mesh-delivery's word raises.
const kindFirstBuildUnasked = "first-build-unasked"
// kindFirstBuildHeld is what a first merge held for a build-agent that reads its manifest raises.
const kindFirstBuildHeld = "first-build-held"
// 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).
@@ -189,6 +257,10 @@ func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []c
var out []conditions.Observation
for _, w := range waiting {
k := announcedOf(w)
if k.Held != nil {
out = append(out, heldFirstBuildObservation(k))
continue
}
if !k.FirstBuild || now.Sub(w.Heard) <= firstBuildWaitBound {
continue
}
@@ -211,6 +283,25 @@ func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []c
return out
}
// heldFirstBuildObservation names a first merge held for a build-agent that reads its manifest: at once, since nothing
// is lost and nothing times out, but the operator can see what the merge waits for.
func heldFirstBuildObservation(k keptMerge) conditions.Observation {
repository := k.Owner + "/" + k.Repo
return conditions.Observation{Scope: conditions.ScopeMerge, ID: fmt.Sprintf("%s_%.8s", repository, k.Commit),
Kind: kindFirstBuildHeld, Severity: conditions.Warning,
Summary: fmt.Sprintf("the merge %.8s of %s adds the module %s, whose manifest uses the word %q: the running "+
"build-agent does not read it, so the merge waits for a build-agent that reads it", k.Commit, repository,
k.Held.Module, k.Held.Word),
Said: fmt.Sprintf("%s at %s waits for a build-agent that reads %q (module %s)", repository, k.Commit, k.Held.Word,
k.Held.Module),
Headline: fmt.Sprintf("The first build of %s waits for a newer build-agent", k.Held.Module),
Explanation: fmt.Sprintf("The merge %.8s into %s adds the module %s, and its manifest uses the word %q, which "+
"the build-agent now running does not know. The merge is kept, not failed: the first build is asked again "+
"when a delivery of build-agent ends.", k.Commit, repository, k.Held.Module, k.Held.Word),
Resolved: "a delivery of build-agent ends and the first build registers the module, or a hand `build` does",
}
}
// probeFirstBuilds is D18: 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)