DLV-B6: a check's build of a head a kept tag ask names numbers its packages tagged at its claim

The builder reads tag-asks for the candidate's repository and head (versions.TagFor), numbers only its packages under
the tag below the next version, keeps each with its repository for the branch's end, and says numbered with the head;
the same fingerprint claimed again keeps its number (design 51 *Tagged versions*).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 13:00:17 +01:00
parent 0dc0cf9c9a
commit 5fbd84091e
9 changed files with 130 additions and 14 deletions
@@ -12,6 +12,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"mesh-delivery-builder/internal/build"
"mesh-delivery-builder/internal/versions"
)
@@ -140,3 +141,30 @@ func tagWanted(ctx context.Context, kv jetstream.KeyValue, m jetstream.Msg, by s
}
_ = m.Ack()
}
// tagsVia is build.Builder.Tags over the state tag-asks: the asks of the repository, read when a check's build is
// looked at; none while the state is not bound.
func tagsVia(kv func() jetstream.KeyValue, logf func(string, ...any)) build.TagOf {
return func(ctx context.Context, repository, head string) (string, bool) {
store := kv()
if store == nil {
return "", false
}
lister, err := store.ListKeysFiltered(ctx, versions.EscapeToken(repository)+".>")
if err != nil {
logf("the tag asks of %s cannot be read, so its check builds no tagged version: %v", repository, err)
return "", false
}
var asks []versions.TagAsk
for key := range lister.Keys() {
e, err := store.Get(ctx, key)
if err != nil {
continue
}
if t, err := versions.DecodeTagAsk(e.Value()); err == nil {
asks = append(asks, t)
}
}
return versions.TagFor(asks, repository, head)
}
}
@@ -142,6 +142,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
Node: cred.Node,
Numbers: build.VersionNumbers{N: numberer},
}
tagAsksKV := process.AwaitBucket(ctx, js, conn, versions.TagAsksBucket, "builder_tag and builder_untag are not answered", logger.Printf)
// A check's build of a head a kept tag ask names numbers its packages tagged (design 51 *Tagged versions*).
b.Tags = tagsVia(tagAsksKV.KV, logger.Printf)
// A state bound later numbers the builds looked at without it.
versionsKV.Ready(func(jetstream.KeyValue) { b.Reconsider(ctx) })
// The versions' hand acts (builder_publish_again, builder_floor_again) come through reasks, as builder_build_again.
@@ -322,7 +325,6 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
})
// A tag ask or a branch's removal by hand (builder_tag, builder_untag) is written to tag-asks and answered there
// (design 51 *Tagged versions*); the checker's tag-wanted and the branch's end join it on their consumers (DLV-B6).
tagAsksKV := process.AwaitBucket(ctx, js, conn, versions.TagAsksBucket, "builder_tag and builder_untag are not answered", logger.Printf)
tagAsksKV.Ready(func(kv jetstream.KeyValue) {
each := func(e jetstream.KeyValueEntry) { tagAsked(ctx, acts, kv, e, logger.Printf) }
if tw, err := settle(ctx, kv, each); err != nil {
@@ -13,7 +13,8 @@ import (
// Numbers numbers a trunk build at its claim (novox/hq design 51 *The versions*, ADR 0606, ADR 0617 §1) and records the
// digests it was built with. Nil numbers nothing.
type Numbers interface {
Number(ctx context.Context, module string, m delivery.Module, plan Plan) (versions.Numbering, error)
// tag is a check's build's tag (TagOf), empty for a trunk build.
Number(ctx context.Context, module string, m delivery.Module, plan Plan, tag string) (versions.Numbering, error)
Built(ctx context.Context, module string, m delivery.Module, plan Plan, got versions.Numbering, made []delivery.BuiltArtifact) error
}
@@ -34,8 +35,13 @@ func ClaimOf(module string, m delivery.Module, plan Plan) versions.BuildClaim {
return c
}
func (v VersionNumbers) Number(ctx context.Context, module string, m delivery.Module, plan Plan) (versions.Numbering, error) {
return v.N.Number(ctx, ClaimOf(module, m, plan))
func (v VersionNumbers) Number(ctx context.Context, module string, m delivery.Module, plan Plan, tag string) (versions.Numbering, error) {
c := ClaimOf(module, m, plan)
if tag != "" {
// A check's build of a head a tag ask names: its packages tagged, below the next version (design 51 *Tagged versions*).
c.Tag, c.Head, c.Revision, c.Merge = tag, m.Commit, "", 0
}
return v.N.Number(ctx, c)
}
func (v VersionNumbers) Built(ctx context.Context, module string, m delivery.Module, plan Plan, got versions.Numbering, made []delivery.BuiltArtifact) error {
@@ -46,13 +52,18 @@ func (v VersionNumbers) Built(ctx context.Context, module string, m delivery.Mod
return v.N.Built(ctx, ClaimOf(module, m, plan), got, digests)
}
// number numbers a trunk build before its claim: false when it waits for its floor (a package, design 51 *The floor*),
// and then nothing is claimed; the floor's wake looks at the module again.
func (b *Builder) number(ctx context.Context, module string, m delivery.Module, plan Plan) (versions.Numbering, bool) {
// TagOf answers the cleaned tag a kept tag ask names for a repository's head (the builder's state tag-asks, design 51
// *Tagged versions*): a check's build of that head numbers its packages tagged at its claim.
type TagOf func(ctx context.Context, repository, head string) (tag string, ok bool)
// number numbers a build before its claim, a trunk build's artifacts or a check's build's packages under its tag: false
// when it waits for its floor (a package, design 51 *The floor*), and then nothing is claimed; the floor's wake looks at
// the module again.
func (b *Builder) number(ctx context.Context, module string, m delivery.Module, plan Plan, tag string) (versions.Numbering, bool) {
if b.Numbers == nil {
return versions.Numbering{}, true
}
got, err := b.Numbers.Number(ctx, module, m, plan)
got, err := b.Numbers.Number(ctx, module, m, plan, tag)
for _, s := range got.Said {
b.logf("%s at %s: %s", module, m.FP2, s)
}
@@ -85,6 +85,8 @@ type Builder struct {
// Numbers numbers each trunk build at its claim and records what it was built with (design 51 *The versions*); nil
// numbers nothing.
Numbers Numbers
// Tags answers a check's build's tag from the kept tag asks; nil numbers no check's build.
Tags TagOf
// Forms answers what each node's engine states it reads (ADR 0618 §2, ADR 0624 §1): present, the nodes whose engine
// answered when last asked, and counted, every node the placement names, present or away, a node with no statement
// stating nothing (the baseline's first forms). A done key names each artifact in the forms these keep, the oldest in
@@ -442,9 +444,13 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) {
b.tried[key] = true
b.mu.Unlock()
var numbering versions.Numbering
if trunk && planErr == nil && !nothing {
tag := ""
if !trunk && b.Tags != nil && b.refOf(module, m.FP2) != "" {
tag, _ = b.Tags(ctx, delivery.RepositoryName(m.Repository), m.Commit)
}
if (trunk || tag != "") && planErr == nil && !nothing {
var goOn bool
if numbering, goOn = b.number(ctx, module, m, plan); !goOn {
if numbering, goOn = b.number(ctx, module, m, plan, tag); !goOn {
return
}
}
@@ -428,8 +428,19 @@ func TestACandidatesImageCallCarriesNoPublishAndItsKeyNoReference(t *testing.T)
r := newRig(t, imageArt)
r.puller = "ace"
trunkModule(t, r)
// DLV-B6: a check's build of a head a kept tag ask names is numbered under that tag at its claim.
nums := &fakeNumbers{}
var tagAsked []string
r.b.Numbers = nums
r.b.Tags = func(_ context.Context, repository, head string) (string, bool) {
tagAsked = append(tagAsked, repository+"@"+head)
return "feat-x", head == headX
}
r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX))
r.b.Wait()
if !slices.Equal(nums.tags, []string{"feat-x"}) || len(tagAsked) != 1 || !strings.HasSuffix(tagAsked[0], "@"+headX) {
t.Errorf("the check's build numbered with tags %v (asked %v), want feat-x for its head", nums.tags, tagAsked)
}
v := valueOf(t, r.st, r.key)
if v.State != delivery.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Reference != "" {
t.Fatalf("build = %+v, want done with no reference", v)
@@ -470,9 +481,11 @@ type fakeNumbers struct {
versions map[string]string
numbered int
built map[string]string
tags []string
}
func (f *fakeNumbers) Number(context.Context, string, delivery.Module, Plan) (versions.Numbering, error) {
func (f *fakeNumbers) Number(_ context.Context, _ string, _ delivery.Module, _ Plan, tag string) (versions.Numbering, error) {
f.tags = append(f.tags, tag)
f.numbered++
return versions.Numbering{Wait: f.wait, Versions: f.versions}, nil
}
@@ -315,3 +315,17 @@ func (a Acts) FloorAgain(ctx context.Context, name string) (string, error) {
a.Reconsider(ctx)
return answer + "; builder_versions shows its version once numbered", nil
}
// TagFor is the cleaned tag a kept tag ask names for repository's head: the ask a check's build of that head is
// numbered under (design 51 *Tagged versions*). An untag, or an ask whose branch ended, names none.
func TagFor(asks []TagAsk, repository, head string) (string, bool) {
for _, t := range asks {
if t.Untag || t.State == TagRemoving || t.State == TagRemoved || t.Head == "" {
continue
}
if t.Repository == repository && t.Head == head {
return t.Tag, t.Tag != ""
}
}
return "", false
}
@@ -61,6 +61,9 @@ type BuildClaim struct {
Revision string
Merge uint64
Artifacts []ClaimArtifact
// Tag numbers a check's build of a head a tag ask names (design 51 *Tagged versions*): its packages only, each a
// tagged version kept with Repository for its branch's end; Head is the head, said in numbered. Empty for a trunk build.
Tag, Head string
}
// Numbering is what the claim got.
@@ -97,7 +100,7 @@ func (n *Numberer) Number(ctx context.Context, c BuildClaim) (Numbering, error)
sort.Slice(arts, func(i, j int) bool { return arts[i].Artifact < arts[j].Artifact })
for _, a := range arts {
seat, kind, ok := SeatOf(a.Kind, a.Language)
if !ok {
if !ok || (c.Tag != "" && kind != KindPackage) {
continue
}
name := NameOf(c.Module, a.Artifact)
@@ -111,7 +114,7 @@ func (n *Numberer) Number(ctx context.Context, c BuildClaim) (Numbering, error)
floor, why = n.askFloor(ctx, seat, name, c)
}
d, e, err := n.Versions.Number(ctx, Claim{Name: name, Kind: kind, Series: c.Series, Module: c.Module, Fingerprint: c.Fingerprint,
Commit: c.Commit, Revision: c.Revision, Seat: seat, Merge: c.Merge, Floor: floor})
Commit: c.Commit, Revision: c.Revision, Seat: seat, Merge: c.Merge, Floor: floor, Tag: c.Tag, Repository: c.Repository})
if err != nil {
return out, err
}
@@ -131,7 +134,7 @@ func (n *Numberer) Number(ctx context.Context, c BuildClaim) (Numbering, error)
}
out.Versions[a.Artifact] = d.Version
if !d.Keep && n.Said != nil {
n.Said(NumberedOf(e, ""))
n.Said(NumberedOf(e, c.Head))
}
}
return out, nil
@@ -85,6 +85,13 @@ func (v Versions) Number(ctx context.Context, c Claim) (Decision, Entry, error)
}
ver := Tagged(entries, c.Series, c.Tag, c.Floor)
d = Decision{Version: ver, Publish: true, Why: "tagged " + ver}
for _, e := range entries {
// The same fingerprint claimed again for its tag (a check's build looked at again) keeps its number.
if e.Tag == CleanTag(c.Tag) && e.Fingerprint == c.Fingerprint && e.State != Removed {
d = Decision{Version: e.Version, Keep: true, Publish: true, Why: "tagged " + e.Version + ", kept for this fingerprint"}
break
}
}
} else {
d = Decide(fresh(entries, c), c.Kind, c.Series, c.Fingerprint, c.Revision, c.Floor)
}
@@ -398,6 +398,25 @@ func TestTheBuildersVersions(t *testing.T) {
if err != nil || got.Versions["program"] != "0.1.14" || len(said) != 1 || said[0].Version != "0.1.14" || len(fw.Waiting()) != 1 {
t.Fatalf("floor answered: %+v, %v, said %v, waits %+v; want 0.1.14 said once, lib still waiting", got, err, said, fw.Waiting())
}
// DLV-B6: a check's build of a head a tag ask names numbers only its packages, tagged below the next version, kept
// with its repository for the branch's end, said with the head; claimed again it keeps its number.
tagged := c
tagged.Tag, tagged.Head, tagged.Revision, tagged.Merge = "feat-x", "h1", "", 0
tagged.Artifacts = append(tagged.Artifacts, ClaimArtifact{Artifact: "tlib", Kind: KindPackage, Language: "go"})
saidBefore, askedBefore := said, asked
said = nil
got, err = n.Number(ctx, tagged)
if err != nil || len(got.Versions) != 1 || !strings.Contains(got.Versions["tlib"], "-feat-x.1") || len(said) != 1 || said[0].Head != "h1" || said[0].Tag != "feat-x" {
t.Fatalf("tagged claim: %+v, %v, said %+v; want tlib alone tagged, said with its head", got, err, said)
}
if es, _ := n.Versions.Of(ctx, "m/tlib"); len(es) != 1 || es[0].Repository != "novox/m" {
t.Errorf("the tagged version kept = %+v, want its repository", es)
}
if again, _ := n.Number(ctx, tagged); again.Versions["tlib"] != got.Versions["tlib"] || len(said) != 1 {
t.Errorf("claimed again: %+v, said %d; want the same number, said once", again, len(said))
}
said, asked = saidBefore, askedBefore
got, _ = n.Number(ctx, c)
if err := n.Built(ctx, c, got, map[string]string{"program": "sha256:aa"}); err != nil {
t.Fatal(err)
}
@@ -562,6 +581,19 @@ func TestTheBuildersVersions(t *testing.T) {
es := []Entry{{Name: "n", Version: "0.1.2-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/r"}, {Name: "n", Version: "0.1.2-feat-x.2", Tag: "feat-x", Series: "0.1", State: Removed, Repository: "o/r"},
{Name: "n2", Version: "0.1.2-feat-x.1", Tag: "feat-x", Series: "0.1", Repository: "o/other"},
{Name: "n", Version: "0.1.1", Series: "0.1"}, {Name: "n", Version: "0.1.2-feat-y.1", Tag: "feat-y", Series: "0.1"}}
for _, c := range []struct {
asks []TagAsk
repo, head string
want string
}{
{[]TagAsk{a}, "novox/mesh-sdk", "abc", "feat-x-y"},
{[]TagAsk{a}, "novox/mesh-sdk", "other", ""},
{[]TagAsk{{Repository: "novox/mesh-sdk", Head: "abc", Tag: "t", State: TagRemoved}}, "novox/mesh-sdk", "abc", ""},
} {
if got, _ := TagFor(c.asks, c.repo, c.head); got != c.want {
t.Errorf("TagFor(%s, %s) = %q, want %q", c.repo, c.head, got, c.want)
}
}
if got := Untagged(es, "o/r", "feat/x"); len(got) != 1 || got[0].Version != "0.1.2-feat-x.1" {
t.Errorf("Untagged = %+v", got)
}