BL17d: builder_build answers a module's build keys whole; builder_builds_since takes module; builder_stuck's fingerprint pinned whole

builder_build reads the builds and asked states, and the tools bundle follows the catalog half itself (registrations for the
watched branches, revision per branch) for `newest`; the manifest names the tool and the two seat verbs.

Builds: design 47 G §589, A-87
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 23:28:08 +01:00
parent 50cca4309f
commit 12e3f2dc48
10 changed files with 482 additions and 29 deletions
@@ -0,0 +1,79 @@
package main
import (
"context"
"encoding/json"
"fmt"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"mesh-delivery-builder/internal/build"
)
// catalogWithin is how long a call waits for the catalog half's first view of one branch.
var catalogWithin = 30 * time.Second
// buildTool is builder_build (design 47 G §589): a module's build keys whole.
func buildTool(r *build.BuildReader) stdio.Tool {
return stdio.Tool{Name: build.BuildTool,
Description: "A module's build keys, whole, newest first (at most 20): per key its fingerprint (whole, as the build-input fingerprint the catalog graph " +
"entry carries it), state (queued, building, done or failed), revision and since when, the step and its node, the error whole, event-builds " +
"(the revisions events started since the last a person started; after three the operator is asked) and the open ask; and newest, the watched " +
"branches whose newest catalog graph entry of the module names that fingerprint, read from the catalog half. A module none of whose keys the newest " +
"entry names says so, naming the entry's fingerprint. With fingerprint, that key alone. Which node wants which fingerprint is the assigner's " +
"to answer (the seat mesh-wants). Read-only.",
Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{
"module": map[string]any{"type": "string", "description": "the module"},
"fingerprint": map[string]any{"type": "string", "description": "one key: its fingerprint, src2:<hex>"}}},
Run: func(args map[string]any) (any, error) {
module, _ := args["module"].(string)
fp, _ := args["fingerprint"].(string)
return r.Build(module, fp)
}}
}
// newestVia reads the fingerprint of a module's newest catalog graph entry on each of its watched branches, from the catalog
// half through the seat mesh-catalog (the verb registrations for the branches, the verb revision for each branch's view), as
// the delivery view's tools read it: once per call (design 47 G §532, §589, A-87).
func newestVia(ask meshcatalogclient.Call) func(module string) (map[string]string, error) {
return func(module string) (map[string]string, error) {
raw, err := ask(meshcatalogclient.RegistrationsVerb, meshcatalog.RegistrationsInput{Module: module})
if err != nil {
return nil, fmt.Errorf("the registrations of %s: %w", module, err)
}
answer, said, ok := buscalls.Unwrap(raw)
if !ok {
return nil, fmt.Errorf("%s answered: %s", meshcatalogclient.RegistrationsVerb, said)
}
var regs meshcatalog.RegistrationsAnswer
if err := json.Unmarshal(answer, &regs); err != nil {
return nil, fmt.Errorf("%s answered no registrations: %w", meshcatalogclient.RegistrationsVerb, err)
}
out := map[string]string{}
for _, reg := range regs.Registrations {
if reg.Module != module {
continue
}
for _, branch := range reg.Branches {
c, err := meshcatalogclient.NewCatalog(ask, branch)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), catalogWithin)
err = c.Start(ctx)
cancel()
if err != nil {
return nil, fmt.Errorf("the catalog half of %s has no first view: %w", branch, err)
}
if e, ok := c.Entry(module); ok && e.Fp != "" {
out[branch] = e.Fp
}
}
}
return out, nil
}
}
@@ -0,0 +1,32 @@
package main
import (
"encoding/json"
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// Design 47 G §589, A-87: the tools bundle reads each watched branch's newest catalog graph entry of the module from the catalog
// half itself: the verb registrations for the branches, the verb revision for each branch's view.
func TestNewestIsReadFromTheCatalogHalfOfEachWatchedBranch(t *testing.T) {
fp := map[string]string{"main": "src2:" + strings.Repeat("1", 64), "feature": "src2:" + strings.Repeat("2", 64)}
ask := func(key string, body any) (json.RawMessage, error) {
switch key {
case meshcatalogclient.RegistrationsVerb:
return json.Marshal(meshcatalog.RegistrationsAnswer{Registrations: []meshcatalog.Registration{{Module: "m", Branches: []string{"main", "feature"}}}})
case meshcatalogclient.RevisionVerb:
in := body.(meshcatalog.RevisionInput)
return json.Marshal(meshcatalog.RevisionAnswer{Revision: &meshcatalog.CatalogRevision{Revision: 3, Kind: meshcatalogclient.KindTrunk},
Entries: []meshcatalog.CatalogEntry{{Module: "m", Fp: fp[in.Branch]}, {Module: "z", Fp: "src2:" + strings.Repeat("9", 64)}}})
}
t.Fatalf("asked %s", key)
return nil, nil
}
got, err := newestVia(ask)("m")
if err != nil || len(got) != 2 || got["main"] != fp["main"] || got["feature"] != fp["feature"] {
t.Fatalf("newest = %v, %v, want each branch's entry fingerprint whole", got, err)
}
}
@@ -156,15 +156,16 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q
"fingerprint, source revision, repository, directory, commit and ref, the build node, when it ended, its outcome (done, failed or " +
"queued), the manifest built, the bases it was built against and each artifact made with its digest and reference; a failed " +
"build carries its error and the step it was at. more says the " +
"log held more than one answer carries: ask again from the last build's time. What the operator's command nox builds reads. Read-only.",
"log held more than one answer carries: ask again from the last build's time. With module, that module's builds only. What the operator's command nox builds reads. Read-only.",
Input: map[string]any{"type": "object", "required": []string{"since"}, "properties": map[string]any{
"since": map[string]any{"type": "string", "description": "RFC 3339 time; the builds that ended at or after it"}}},
"since": map[string]any{"type": "string", "description": "RFC 3339 time; the builds that ended at or after it"},
"module": map[string]any{"type": "string", "description": "only this module's builds"}}},
Run: func(args map[string]any) (any, error) {
at, err := time.Parse(time.RFC3339Nano, str(args, "since"))
if err != nil {
return nil, fmt.Errorf("since %q is not an RFC 3339 time: %w", str(args, "since"), err)
}
return build.BuildsSince(since, at)
return build.BuildsSince(since, at, str(args, "module"))
}},
{Name: build.AnnounceAgainTool,
Description: "Announce a done build again (take in again, compose again): the builder writes the done key of module and fingerprint " +
@@ -363,6 +364,8 @@ func main() {
return e.Value, true, nil
}}
tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr)
tools = append(tools, buildTool(&build.BuildReader{Builds: read(stdio.State("builds")), Entry: entryOf(stdio.State("builds")), Asked: read(stdio.State("asked")),
Newest: newestVia(stdio.Ask), Steps: r}))
tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
qs, as := stdio.State(versions.PublishQueueState), stdio.State(versions.ActedState)
tools = append(tools, republishTools(readerOf(qs), readerOf(as), buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...)
@@ -394,3 +397,14 @@ func main() {
os.Exit(1)
}
}
// entryOf reads a key with its revision and when it was written.
func entryOf(s stdio.KeptState) func(key string) (json.RawMessage, uint64, time.Time, error) {
return func(key string) (json.RawMessage, uint64, time.Time, error) {
e, err := s.Get(key)
if err != nil || e == nil {
return nil, 0, time.Time{}, err
}
return e.Value, e.Revision, written(s, key, e.Revision), nil
}
}
@@ -0,0 +1,222 @@
package build
import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
// **builder_build answers a module's build keys whole** (design 47 G §589, A-87): with `fingerprint` that key whole, with
// `module` alone the module's keys newest first, at most MostBuilds, each with its fingerprint whole, its state, revision and
// since when, its step and node, its error whole, event-builds and the open ask (§568), and `newest` naming each watched branch
// whose newest catalog graph entry of the module names that fingerprint, read from the catalog half by the tools bundle.
// BuildTool is the builder's read tool of a module's build keys.
const BuildTool = "builder_build"
// MostBuilds is how many keys of a module builder_build answers by module alone.
const MostBuilds = 20
// BuildRow is one build key as builder_build answers it.
type BuildRow struct {
Key string `json:"key"`
Fingerprint string `json:"fingerprint"`
State string `json:"state"`
Revision uint64 `json:"revision,omitempty"`
Since time.Time `json:"since,omitzero"`
Step string `json:"step,omitempty"`
Node string `json:"node,omitempty"`
Error string `json:"error,omitempty"`
EventBuilds int `json:"event-builds"`
// Ask is the open ask of the operator about the key (§568).
Ask *OpenAsk `json:"ask,omitempty"`
// Newest names each watched branch whose newest catalog graph entry of the module names this fingerprint.
Newest []string `json:"newest,omitempty"`
}
// OpenAsk is the ask the operator has not answered.
type OpenAsk struct {
ID string `json:"id"`
Asked string `json:"asked"`
Expires string `json:"expires"`
}
// BuildAnswer is what builder_build answers.
type BuildAnswer struct {
Module string `json:"module"`
Builds []BuildRow `json:"builds"`
// Newest is the fingerprint of the module's newest catalog graph entry on each watched branch.
Newest map[string]string `json:"newest-entries,omitempty"`
// Note says what no key shows: that none of the keys is the newest entry's, naming its fingerprint.
Note string `json:"note,omitempty"`
}
// BuildReader reads what builder_build answers.
type BuildReader struct {
Builds StateRead
// Entry is a key's value with its revision and when it was written; nil leaves both out.
Entry func(key string) (json.RawMessage, uint64, time.Time, error)
// Asked is the builder's state asked, which holds the asks the operator was asked.
Asked StateRead
// Newest is the fingerprint of the module's newest catalog graph entry on each watched branch, read from the catalog half.
Newest func(module string) (map[string]string, error)
Steps StuckReader
index map[string]map[string]string
}
// Build answers for module, and for one fingerprint when fingerprint is not empty.
func (r *BuildReader) Build(module, fingerprint string) (BuildAnswer, error) {
out := BuildAnswer{Module: module, Builds: []BuildRow{}}
if module == "" {
return out, errors.New("name the module (module)")
}
all, err := r.Builds.Keys()
if err != nil {
return out, err
}
var rows []BuildRow
for _, key := range all {
mod, fp, err := builds.ParseBuilderBuildKey(key)
if err != nil || mod != module || (fingerprint != "" && fp != fingerprint) {
continue
}
raw, rev, at, err := r.read(key)
if err != nil {
return out, fmt.Errorf("%s cannot be read: %w", key, err)
}
if raw == nil {
continue
}
v, err := builds.DecodeBuilderBuild(raw)
if err != nil {
return out, fmt.Errorf("%s holds what is not a build: %w", key, err)
}
row := BuildRow{Key: key, Fingerprint: fp, State: v.State, Revision: rev, Since: at, Step: v.Step, Error: v.Error, EventBuilds: v.EventBuilds}
if _, _, node, _, ok := builds.TransportFaultOf(v.Error); ok && node != "no node" {
row.Node = node
}
if row.Node == "" && v.State == builds.BuildBuilding {
row.Node = r.stepNode(v)
}
rows = append(rows, row)
}
if fingerprint != "" && len(rows) == 0 {
return out, fmt.Errorf("the builder holds no build key of %s at %s", module, fingerprint)
}
sort.Slice(rows, func(i, j int) bool {
if !rows[i].Since.Equal(rows[j].Since) {
return rows[i].Since.After(rows[j].Since)
}
return rows[i].Key > rows[j].Key
})
if len(rows) > MostBuilds {
rows = rows[:MostBuilds]
}
asks := r.openAsks()
for i := range rows {
rows[i].Ask = asks[rows[i].Key]
}
if r.Newest != nil {
newest, err := r.Newest(module)
if err != nil {
return out, fmt.Errorf("the catalog half's newest entry of %s cannot be read: %w", module, err)
}
out.Newest = newest
named := false
for i := range rows {
for branch, fp := range newest {
if fp == rows[i].Fingerprint {
rows[i].Newest, named = append(rows[i].Newest, branch), true
}
}
slices.Sort(rows[i].Newest)
}
if !named && len(newest) > 0 {
var says []string
for branch, fp := range newest {
says = append(says, branch+" names "+fp)
}
slices.Sort(says)
out.Note = "no build key of " + module + " is the newest entry's: " + strings.Join(says, "; ")
}
}
out.Builds = append(out.Builds, rows...)
return out, nil
}
// stepNode is the node a building key's step runs on, read from its asks' steps in the build modules' steps; empty when none runs.
func (r *BuildReader) stepNode(v builds.BuilderBuild) string {
if len(v.Asks) == 0 || len(r.Steps.Steps) == 0 {
return ""
}
if r.index == nil {
idx, err := r.Steps.stepIndex()
if err != nil {
return ""
}
r.index = idx
}
languages := make([]string, 0, len(v.Asks))
for l := range v.Asks {
languages = append(languages, l)
}
slices.Sort(languages)
for _, l := range languages {
bm, _ := buildstep.BuildModuleOf(l)
st, ok := r.Steps.Steps[bm]
k, found := r.index[bm][v.Asks[l]]
if !ok || !found {
continue
}
if raw, _, err := st.Get(k); err == nil && raw != nil {
if step, err := buildstep.DecodeStep(raw); err == nil && step.Node != "" {
return step.Node
}
}
}
return ""
}
func (r *BuildReader) read(key string) (json.RawMessage, uint64, time.Time, error) {
if r.Entry != nil {
return r.Entry(key)
}
raw, at, err := r.Builds.Get(key)
return raw, 0, at, err
}
// openAsks are the asks kept in the state asked that nobody answered yet, by the key each is about.
func (r *BuildReader) openAsks() map[string]*OpenAsk {
out := map[string]*OpenAsk{}
if r.Asked.Keys == nil {
return out
}
keys, err := r.Asked.Keys()
if err != nil {
return out
}
for _, k := range keys {
raw, _, err := r.Asked.Get(k)
if err != nil || raw == nil {
continue
}
var kept occ.Kept
if json.Unmarshal(raw, &kept) != nil || kept.Ended != "" || kept.Acted || kept.Ask.About == "" {
continue
}
if t, err := occ.Time(kept.Ask.Expires); err == nil && !t.IsZero() && time.Now().After(t) {
continue
}
out[kept.Ask.About] = &OpenAsk{ID: kept.Ask.ID, Asked: kept.Asked.UTC().Format(time.RFC3339), Expires: kept.Ask.Expires}
}
return out
}
@@ -0,0 +1,83 @@
package build
import (
"encoding/json"
"sort"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
const fp2 = "src2:2222222222222222222222222222222222222222222222222222222222222222"
func stateOf(vals map[string][]byte) StateRead {
return StateRead{
Keys: func() ([]string, error) {
var ks []string
for k := range vals {
ks = append(ks, k)
}
sort.Strings(ks)
return ks, nil
},
Get: func(k string) (json.RawMessage, time.Time, error) { return vals[k], time.Time{}, nil }}
}
// Design 47 G §589: builder_build by module answers its keys newest first with whole fingerprints, event-builds and the open
// ask, and names the watched branches whose newest entry names a key's fingerprint; a module none of whose keys the newest
// entry names says so; with a fingerprint, that key alone.
func TestBuilderBuildAnswersAModulesKeysWhole(t *testing.T) {
failed, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildFailed, EventBuilds: 3, Manifest: mdigest('a'),
Error: "transport fault: connection, node-build-go ace, dial tcp: connection refused"})
done, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: mdigest('a'), Artifacts: []builds.BuiltArtifact{{Name: "x", Kind: "file", Digest: "sha256:" + strings.Repeat("a", 64)}}})
k1, _ := builds.BuilderBuildKey("m", fp1)
k2, _ := builds.BuilderBuildKey("m", fp2)
other, _ := builds.BuilderBuildKey("other", fp1)
vals := map[string][]byte{k1: failed, k2: done, other: done}
ask := occ.Kept{Ask: oc.Ask{ID: "retake-1", About: k1, Expires: occ.FormatTime(time.Now().Add(time.Hour))}, Asked: time.Now()}
askRaw, _ := json.Marshal(ask)
r := &BuildReader{Builds: stateOf(vals), Asked: stateOf(map[string][]byte{"ask.retake-1": askRaw}),
Newest: func(string) (map[string]string, error) { return map[string]string{"main": fp1, "feature": fp2}, nil }}
a, err := r.Build("m", "")
if err != nil || len(a.Builds) != 2 {
t.Fatalf("builds = %+v, %v, want the module's two keys", a, err)
}
var row BuildRow
for _, b := range a.Builds {
if b.Fingerprint == fp1 {
row = b
}
}
if row.Fingerprint != fp1 || len(row.Fingerprint) != len("src2:")+64 || row.State != builds.BuildFailed || row.EventBuilds != 3 || row.Node != "ace" ||
!strings.HasPrefix(row.Error, "transport fault: connection, node-build-go ace, dial tcp") || row.Ask == nil || row.Ask.ID != "retake-1" || len(row.Newest) != 1 || row.Newest[0] != "main" {
t.Errorf("the failed key = %+v, want its fingerprint whole, its error whole, event-builds 3, the open ask and newest main", row)
}
if one, err := r.Build("m", fp2); err != nil || len(one.Builds) != 1 || one.Builds[0].Fingerprint != fp2 || one.Builds[0].Newest[0] != "feature" {
t.Errorf("one key = %+v, %v", one, err)
}
r.Newest = func(string) (map[string]string, error) {
return map[string]string{"main": "src2:" + strings.Repeat("c", 64)}, nil
}
if n, err := r.Build("m", ""); err != nil || !strings.Contains(n.Note, "no build key of m is the newest entry's") || !strings.Contains(n.Note, "main names src2:"+strings.Repeat("c", 64)) {
t.Errorf("a module none of whose keys the newest entry names: %+v, %v", n, err)
}
}
// Design 47 G §589: builder_stuck answers each key's fingerprint whole, as the build-input fingerprint the catalog graph entry carries it.
func TestBuilderStuckAnswersTheFingerprintWhole(t *testing.T) {
building, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, Step: "asked", Manifest: mdigest('a')})
k, _ := builds.BuilderBuildKey("m", fp1)
out, err := StuckReader{Builds: stateOf(map[string][]byte{k: building})}.Stuck("m")
if err != nil {
t.Fatal(err)
}
rows := out["stuck"].([]Stuck)
if len(rows) != 1 || rows[0].FP != fp1 {
t.Fatalf("stuck = %+v, want the fingerprint %s whole", rows, fp1)
}
}
@@ -270,12 +270,19 @@ func TestABundleAndAnArchiveAreBuiltOnOneNodeAndDone(t *testing.T) { // 1607/AC7
page.Entries = append(page.Entries, stdio.LogEntry{Sequence: uint64(i + 1), Key: e.Module, Value: raw, At: e.At})
}
page.Next = 3
since, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At)
since, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, "")
if err != nil || len(since.Builds) != 1 || since.Builds[0].Event != builds.BuildEndedEvent || len(since.Builds[0].Made) != 3 || !since.More {
t.Errorf("builds since = %+v, %v, want the one end and more", since, err)
}
// With module (design 47 G §589) the builds of that module alone.
if other, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, "another"); err != nil || len(other.Builds) != 0 {
t.Errorf("builds since of another module = %+v, %v, want none", other, err)
}
if mine, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, logged[0].Module); err != nil || len(mine.Builds) != 1 {
t.Errorf("builds since of the module = %+v, %v, want its one end", mine, err)
}
page.Entries[0].Value = []byte(`{"v":0}`)
if _, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At); err == nil {
if _, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At, ""); err == nil {
t.Error("an entry that does not read was skipped, want it refused")
}
if v.State != builds.BuildDone || v.By != "novox.builder" || v.Manifest != manifestD || len(v.Artifacts) != 3 {
@@ -2,6 +2,7 @@ package build
import (
"fmt"
"slices"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
@@ -16,9 +17,9 @@ type LogSince func(at time.Time, keys string, limit int) (*stdio.LogPage, error)
// BuildsSince answers builds.BuildsSinceTool (ADR 0571 clause 9): every ended build in the builder's log from since on,
// oldest first, read through the runtime's log verbs; More when the page was not the log's last, so the caller asks again
// from the last build's time. An entry the log holds that does not read is refused, naming its sequence: a reader that
// from the last build's time. With module (design 47 G §589) only that module's builds are answered. An entry the log holds that does not read is refused, naming its sequence: a reader that
// skipped it would register too little.
func BuildsSince(read LogSince, since time.Time) (builds.BuildsSince, error) {
func BuildsSince(read LogSince, since time.Time, module string) (builds.BuildsSince, error) {
out := builds.BuildsSince{Since: since.UTC(), Builds: []builds.BuildLogEntry{}}
page, err := read(since, "", BuildsSinceLimit)
if err != nil {
@@ -38,6 +39,9 @@ func BuildsSince(read LogSince, since time.Time) (builds.BuildsSince, error) {
if ended := builds.EndedSince(entries, since); ended != nil {
out.Builds = ended
}
if module != "" {
out.Builds = slices.DeleteFunc(out.Builds, func(e builds.BuildLogEntry) bool { return e.Module != module })
}
out.More = page.Next != 0
return out, nil
}
@@ -313,26 +313,9 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) {
if r.Now != nil {
now = r.Now
}
// Steps are found by the ask's id, which their key ends in: one index per build module, no value read until an ask names it.
stepKeys := map[string]map[string]string{}
for bm, s := range r.Steps {
ks, err := s.Keys()
if err != nil {
return nil, fmt.Errorf("the steps of %s: %w", bm, err)
}
byID := make(map[string]string, len(ks))
for _, k := range ks {
_, _, id, err := buildstep.ParseStepKey(k)
if err != nil {
// A key neither form parses is named, never passed over in silence (design 47 G §484).
if r.Logf != nil {
r.Logf("builder_stuck: the key %q of the steps of %s is a step key of neither form, <asker>.<node>.<id> or <node>.<id>: %v", k, bm, err)
}
continue
}
byID[id] = k
}
stepKeys[bm] = byID
stepKeys, err := r.stepIndex()
if err != nil {
return nil, err
}
// Narrowed by the key (the module) before any value is read; the state is in the value, so every matching key is read, in
// parallel, at most MaxReads of them.
@@ -429,6 +412,32 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) {
return answer, nil
}
// stepIndex finds the build modules' steps by the ask's id, which their key ends in: one index per build module, no value read
// until an ask names it.
func (r StuckReader) stepIndex() (map[string]map[string]string, error) {
stepKeys := map[string]map[string]string{}
for bm, s := range r.Steps {
ks, err := s.Keys()
if err != nil {
return nil, fmt.Errorf("the steps of %s: %w", bm, err)
}
byID := make(map[string]string, len(ks))
for _, k := range ks {
_, _, id, err := buildstep.ParseStepKey(k)
if err != nil {
// A key neither form parses is named, never passed over in silence (design 47 G §484).
if r.Logf != nil {
r.Logf("builder_stuck: the key %q of the steps of %s is a step key of neither form, <asker>.<node>.<id> or <node>.<id>: %v", k, bm, err)
}
continue
}
byID[id] = k
}
stepKeys[bm] = byID
}
return stepKeys, nil
}
// StuckBuildModules are the build modules whose steps builder_stuck reads.
var StuckBuildModules = []string{buildrun.GoBuildModule, buildrun.NodeBuildModule, ImageBuildModule}
@@ -144,7 +144,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.publish", "seat:container-image-registry.remove", "seat:container-image-registry.versions",
"seat:git.tag",
"seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.entry", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove",
"seat:go-package-registry.publish", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-module-assigner.assignments", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.publish", "seat:npm-package-registry.remove",
"seat:npm-package-registry.versions", "seat:oci-artifact-registry.publish", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
t.Errorf("invokes = %v, want %v", m.Invokes, want)
}
@@ -171,7 +171,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
t.Errorf("secrets-owner = %q", m.SecretsOwner)
}
// 1945: the builder's tools (builder_seat_queue: note 62), served by its tools bundle, and the event it says when it asks an ask again.
if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views",
if want := []string{"builder_announce_again", "builder_ask_again", "builder_bounds", "builder_build", "builder_build_again", "builder_builds_since", "builder_engine_reads", "builder_first_view_again", "builder_first_views",
"builder_floor_again", "builder_floor_waits", "builder_old_form", "builder_publish_again", "builder_publish_queue", "builder_release", "builder_reserved", "builder_reserved_drop", "builder_seat_queue", "builder_stuck", "builder_toolchains", "builder_toolchains_drop", "builder_versions", "builder_waits", "reserved"}; !reflect.DeepEqual(sorted(m.Tools), want) {
t.Errorf("tools = %v, want %v", m.Tools, want)
}
@@ -15,6 +15,7 @@
"builder_announce_again",
"builder_ask_again",
"builder_bounds",
"builder_build",
"builder_build_again",
"builder_builds_since",
"builder_engine_reads",
@@ -85,6 +86,8 @@
"seat:go-package-registry.versions",
"seat:mesh-artifact-sweep.held",
"seat:mesh-catalog.entry",
"seat:mesh-catalog.registrations",
"seat:mesh-catalog.revision",
"seat:mesh-catalog.versions",
"seat:mesh-hand-acts.hand-act",
"seat:mesh-module-assigner.assignments",