diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go index b2a5aa3f..de22d90a 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go @@ -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) + } +} 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 52fd9342..66b2178d 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -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 { diff --git a/modules/mesh-delivery-builder/internal/build/numbers.go b/modules/mesh-delivery-builder/internal/build/numbers.go index 22ac25ef..52a5dc24 100644 --- a/modules/mesh-delivery-builder/internal/build/numbers.go +++ b/modules/mesh-delivery-builder/internal/build/numbers.go @@ -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) } diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 0b5aad8d..95c6e127 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -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 } } diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index 509c093f..5558afeb 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -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 } diff --git a/modules/mesh-delivery-builder/internal/versions/again.go b/modules/mesh-delivery-builder/internal/versions/again.go index c4ab1402..c298beac 100644 --- a/modules/mesh-delivery-builder/internal/versions/again.go +++ b/modules/mesh-delivery-builder/internal/versions/again.go @@ -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 +} diff --git a/modules/mesh-delivery-builder/internal/versions/claim.go b/modules/mesh-delivery-builder/internal/versions/claim.go index 04b3bb27..e758d927 100644 --- a/modules/mesh-delivery-builder/internal/versions/claim.go +++ b/modules/mesh-delivery-builder/internal/versions/claim.go @@ -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 diff --git a/modules/mesh-delivery-builder/internal/versions/store.go b/modules/mesh-delivery-builder/internal/versions/store.go index b29eff1a..4f064420 100644 --- a/modules/mesh-delivery-builder/internal/versions/store.go +++ b/modules/mesh-delivery-builder/internal/versions/store.go @@ -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) } diff --git a/modules/mesh-delivery-builder/internal/versions/versions_test.go b/modules/mesh-delivery-builder/internal/versions/versions_test.go index 9f142678..e9d5e1b6 100644 --- a/modules/mesh-delivery-builder/internal/versions/versions_test.go +++ b/modules/mesh-delivery-builder/internal/versions/versions_test.go @@ -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) }