From 9da79ef8c375358a6bb8e9aaf5d782e79062ffe0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 17:58:21 +0200 Subject: [PATCH] Move no module whose fingerprint is unchanged, and let a key's build stand on the held build-agent build of the same digests at the switch (issue 1851, A9) --- cmd/mesh-controller/check_builds_test.go | 15 -- .../key_takein_integration_test.go | 30 ++++ cmd/mesh-controller/release_plan.go | 23 --- cmd/mesh-controller/same_program.go | 133 ++++++++---------- .../same_program_integration_test.go | 35 ++--- cmd/mesh-controller/same_program_test.go | 64 ++------- internal/inventory/source.go | 48 ++++++- internal/inventory/standing_switch_test.go | 49 +++++++ 8 files changed, 208 insertions(+), 189 deletions(-) create mode 100644 internal/inventory/standing_switch_test.go diff --git a/cmd/mesh-controller/check_builds_test.go b/cmd/mesh-controller/check_builds_test.go index 1e670644..f0219db1 100644 --- a/cmd/mesh-controller/check_builds_test.go +++ b/cmd/mesh-controller/check_builds_test.go @@ -132,18 +132,3 @@ func TestACheckIsHandedOnlyItsOwnHeadsBuilds(t *testing.T) { t.Fatalf("verdicts that could not be read named %+v", got) } } - -// A walk's merged heads are the forge's word for each merge; a merge announced without its head, or a pull request -// merged at two heads in one walk, has none, and its walk takes no check's build (novox/hq issue 703). -func TestAWalksMergedHeadsAreTheForgesWord(t *testing.T) { - merge := func(number int, head string) inventory.PlanMerge { - return inventory.PlanMerge{Repository: "novox/shop", Number: number, Head: head} - } - p := &inventory.Plan{Delivery: &inventory.PlanDelivery{Merges: []inventory.PlanMerge{ - merge(7, "aaaaaaaaaaaa1"), merge(8, ""), merge(9, "bbbbbbbbbbbb"), merge(9, "cccccccccccc")}}} - heads := mergedHeadsOf(p) - shop := func(n int) link.PullRef { return link.PullRef{Owner: "novox", Repo: "shop", Number: n} } - if heads[shop(7)] != "aaaaaaaaaaaa1" || heads[shop(8)] != "" || heads[shop(9)] != "" { - t.Fatalf("merged heads %v", heads) - } -} diff --git a/cmd/mesh-controller/key_takein_integration_test.go b/cmd/mesh-controller/key_takein_integration_test.go index 39df46e7..24ba2d98 100644 --- a/cmd/mesh-controller/key_takein_integration_test.go +++ b/cmd/mesh-controller/key_takein_integration_test.go @@ -190,3 +190,33 @@ func TestAKeysRecordKeepsTheKeepSetAndReadsItsManifest(t *testing.T) { t.Fatalf("the key's record does not read its manifest's context: %s", text) } } + +// A9 (ADR 0453 §6, ADR 0499 consequences): at the switch, a key's build whose every artifact has the digest of the +// build-agent build the mesh holds (`src1:`) is no move: it is registered with that build's artifacts, and a walk sends +// nothing for it. +func TestAKeyWithTheHeldBuildsDigestsIsNoMove(t *testing.T) { + open := theCatalogue(t) + ctx := t.Context() + inv := open.inventory + withTrunkList(t, true) + keys, trunk := aKeyFor("sudo", "6", fpOf('6'), "novox/mesh-catalog", "modules/sudo", "c0ffee0123456789") + k := keys.keys["sudo"] + held := inventory.Build{ID: "build-sudo-old", Module: "sudo", Repository: "novox/mesh-catalog", Path: "modules/sudo", + Commit: "0ld0ld0ld0ld0ld0", SourceFingerprint: "src1:" + strings.Repeat("9", 64), + Made: []inventory.Artifact{{Name: "a", Kind: "archive", Reference: k.Build.Artifacts[0].Reference}}} + held.Manifest, _ = json.Marshal(catalogue.Manifest{Module: "sudo", Version: "held"}) + if err := inv.RecordBuild(ctx, held); err != nil { + t.Fatal(err) + } + _, kept, err := takeInKey(ctx, inv, keys, keyTake{Module: "sudo", Trunk: trunk, Ask: registrationAsk{Merge: trunk.Commit}}) + if err != nil { + t.Fatal(err) + } + same, err := inv.SameSourceCommits(ctx, "sudo", kept.ID) + if err != nil || !same["0ld0ld0ld0ld0ld0"] { + t.Fatalf("A9: the key's build does not stand on the held build of the same digests: %v %v", same, err) + } + if shelf, _ := inv.Catalogue(ctx); shelf["sudo"].Version != "held" { + t.Fatalf("A9: sudo is registered as %q, not with the held build's artifacts", shelf["sudo"].Version) + } +} diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index 57e43e81..8cc11981 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -451,29 +451,6 @@ var checkedBuildsOf = func(ctx context.Context, pulls []link.PullRef) ([]link.Ch return link.ReadCheckBuilds(ctx, js, buildSeatHeld(ctx), pulls, time.Now().Add(-builder.CheckBuildsKept)) } -// mergedHeadsOf are the head commits the walk's merges merged, by pull request (novox/hq issue 703): a pull request -// merged twice in one walk, or whose head the announcement did not say, has none, and takes no check's build. -func mergedHeadsOf(p *inventory.Plan) map[link.PullRef]string { - heads := map[link.PullRef]string{} - if p.Delivery == nil { - return heads - } - seen := map[link.PullRef]bool{} - for _, m := range p.Delivery.Merges { - pull, ok := pullOf(m) - if !ok { - continue - } - if seen[pull] && heads[pull] != m.Head { - heads[pull] = "" - continue - } - seen[pull] = true - heads[pull] = m.Head - } - return heads -} - // pullOf is a merge's pull request: owner and repository, the last two parts of however the repository is spelled // (owner/repo, a path on the git seat, a URL), and its number. False when the merge names none. func pullOf(m inventory.PlanMerge) (link.PullRef, bool) { diff --git a/cmd/mesh-controller/same_program.go b/cmd/mesh-controller/same_program.go index fa1260d2..00892b95 100644 --- a/cmd/mesh-controller/same_program.go +++ b/cmd/mesh-controller/same_program.go @@ -8,45 +8,22 @@ import ( "strings" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" ) -// **A module moves only when its program changes** (novox/hq issue 721). A merge check builds every module the -// delivery plan moves and compares each with the build the mesh holds of it; a walk is cut without a module whose -// check found its whole image the same as that build, while the mesh still holds it, and says so among the modules -// it left unchanged. A merge of one sentence in a package a program imports but never reaches moves no build of it. +// **A module moves only when its source changes** (novox/hq issue 721; from R4e by its fingerprint, ADR 0453 §6, ADR +// 0499; issue 1851, A9). A walk is cut without a module whose fingerprint at the commit the walk carries is the +// fingerprint of the build the mesh holds of it, and says so among the modules it left unchanged: a merge of a file no +// module reads moves nothing. A build the mesh holds of the old form (build-agent's `src1:`) has no fingerprint to +// compare: the module stays in the walk, and the rule of the standing build (equal digests, no move) judges it once its +// key is taken. -// sameAsRunning are the modules of a walk its checks found the same as the build the mesh holds, each with what its -// check said. **Only when every pull request the walk answers was checked at the head it merged, and every one of -// those checks built the module and found it the same as the build the mesh still holds**: a merge the walk answers -// without a pull request, a check of another head, a check that built it differently, or a held build that moved -// since, leaves the module in the walk. -func sameAsRunning(plan *inventory.Plan, builds []link.CheckedBuild, held map[string]string) map[string]string { - if plan.Delivery == nil || len(plan.Delivery.Merges) == 0 { - return nil - } - for _, m := range plan.Delivery.Merges { - if _, ok := pullOf(m); !ok { - return nil - } - } - pulls := pullsOfWalk(plan) - heads := mergedHeadsOf(plan) +// sameByFingerprint are the modules whose fingerprint now (`src2:`, the indexer's trunk value at the walk's commit) is +// the fingerprint of the build the mesh holds, each with why; one whose either is unknown, or of the old form, is not. +func sameByFingerprint(now, held map[string]string) map[string]string { out := map[string]string{} - for name := range plan.Modules { - said := "" - for _, pull := range pulls { - b, ok := checkedBuildOf(builds, pull, heads[pull], name) - if !ok || !stillHeld(b.Built, name, held) { - said = "" - break - } - if said == "" { - said = b.Built.Same - } - } - if said != "" { - out[name] = said + for m, fp := range now { + if strings.HasPrefix(fp, "src2:") && held[m] == fp { + out[m] = fmt.Sprintf("unchanged: its fingerprint %s is the build the mesh holds", short(fp)) } } if len(out) == 0 { @@ -55,65 +32,67 @@ func sameAsRunning(plan *inventory.Plan, builds []link.CheckedBuild, held map[st return out } -// checkedBuildOf is the newest build a check of the pull request at head made of the module; false when none did. -func checkedBuildOf(builds []link.CheckedBuild, pull link.PullRef, head, module string) (link.CheckedBuild, bool) { - if head == "" { - return link.CheckedBuild{}, false +// heldFingerprint is the source fingerprint of the build the mesh holds of a module: the newest build of the commit +// it is registered at. +func heldFingerprint(ctx context.Context, inv *inventory.Inventory, module string) (string, error) { + src, err := inv.SourceOf(ctx, module) + if err != nil { + return "", err } - for _, b := range builds { - if b.Pull == pull && b.Built.Module == module && link.SameHead(b.Head, head) { - return b, true + fps, err := inv.BuildSourceFingerprints(ctx, module) + if err != nil { + return "", err + } + for commit, fp := range fps { + if sameCommitID(commit, src.BuiltFrom) { + return fp, nil } } - return link.CheckedBuild{}, false + return "", nil } -// stillHeld is whether a check found every artifact of the module the same as a build the mesh still holds. -func stillHeld(b link.CheckBuilt, module string, held map[string]string) bool { - if b.Same == "" || b.NotBuilt != "" || len(b.Made) == 0 || len(b.SameAs) != len(b.Made) { - return false - } - for _, m := range b.Made { - was := pinnedDigest(b.SameAs[m.Name]) - if was == "" || was != pinnedDigest(held[module+"/"+m.Name]) { - return false - } - } - return true -} - -// pinnedDigest is a reference's pinned digest, empty when it is not pinned. -func pinnedDigest(ref string) string { - if i := strings.LastIndex(ref, "@"); i >= 0 { - return ref[i+1:] - } - return "" -} - -// leaveSameAsRunning takes from a walk being cut every module its checks found the same as the build the mesh holds -// (sameAsRunning), and every dependent in the walk only for one of those (withDependentsOf): what was taken, with why. **Anything not read leaves the walk as it was**, said. +// leaveSameAsRunning takes from a walk being cut every module whose fingerprint is unchanged (sameByFingerprint), and +// every dependent in the walk only for one of those (withDependentsOf): what was taken, with why. **Anything not read +// leaves the walk as it was**, said. func leaveSameAsRunning(ctx context.Context, inv *inventory.Inventory, plan *inventory.Plan, moved []string, edges []inventory.Edge) map[string]string { - if plan.Delivery == nil || len(plan.Modules) == 0 { + if len(plan.Modules) == 0 { return nil } - pulls := pullsOfWalk(plan) - if len(pulls) == 0 { - return nil - } - builds, err := checkedBuildsOf(ctx, pulls) + entries, err := inv.Catalogued(ctx) if err != nil { - fmt.Printf(" the checks' builds could not be read, so every module moves: %v\n", err) + fmt.Printf(" the catalogue could not be read, so every module moves: %v\n", err) // empty-on-error: said above; nothing left out of the walk is the safe answer, every module moves as before return nil } - held, err := inv.RegisteredHeld(ctx) + keys, closeKeys, err := openPlanKeys() if err != nil { - fmt.Printf(" what the mesh holds could not be read, so every module moves: %v\n", err) + fmt.Printf(" the indexer's fingerprints could not be read, so every module moves: %v\n", err) // empty-on-error: said above; nothing left out of the walk is the safe answer, every module moves as before return nil } - same := withDependentsOf(sameAsRunning(plan, builds, held), plan, moved, edges) + defer closeKeys() + byName := map[string]inventory.Entry{} + for _, e := range entries { + byName[e.Manifest.Module] = e + } + now, held := map[string]string{}, map[string]string{} + for m := range plan.Modules { + e, known := byName[m] + if !known || isBuildersOwn(m) { + continue + } + v, err := keys.ModuleAt(ctx, m, plan.CommitOn(e.Source.Repository, followedBranch(e.Source.Ref))) + if err != nil || v.FP2 == "" { + continue // not written yet, or none: the module moves, and its key is waited for in the walk + } + fp, err := heldFingerprint(ctx, inv, m) + if err != nil { + continue + } + now[m], held[m] = v.FP2, fp + } + same := withDependentsOf(sameByFingerprint(now, held), plan, moved, edges) if len(same) == 0 { return nil } diff --git a/cmd/mesh-controller/same_program_integration_test.go b/cmd/mesh-controller/same_program_integration_test.go index 9e608dbd..0a4f5131 100644 --- a/cmd/mesh-controller/same_program_integration_test.go +++ b/cmd/mesh-controller/same_program_integration_test.go @@ -3,45 +3,38 @@ package main import ( - "context" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/catalogue" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" ) -func TestAWalkWhoseEveryBuildCameOutTheSameIsDone(t *testing.T) { +// R4e A9 (ADR 0453 §6, ADR 0499; issue 1851), against the store and a real bus: a merge of a file no module reads moves +// nothing. Its module's fingerprint at the merge, as the indexer wrote it, is the fingerprint of the build the mesh holds, +// so the walk is cut done with nothing to walk, and its record names the module unchanged and why — the walk +// mesh-delivery reads as delivered, never stranded; no build is asked. +func TestAWalkWhoseEveryFingerprintIsUnchangedIsDone(t *testing.T) { open := windowed(t) endsItsLine(t) - asksRecorded(t) + noBuildAsked(t) + b := aBuilderOnTheBus(t) ctx := t.Context() inv := open.inventory - if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-one", Repository: "novox/one", Ref: "main", - Module: "one", Commit: "c0", On: "builder", Asked: t0.Add(-time.Hour), - Made: []inventory.Artifact{{Name: "one", Kind: "image", Reference: "store/one@sha256:aaaa"}}, - Sources: []inventory.BuildSource{{Paths: []string{"main.go", "go.mod"}}}}); err != nil { + fp := fpOf('5') + if err := inv.RecordBuild(ctx, inventory.Build{ID: "key-held", Repository: "novox/one", Ref: "main", + Module: "one", Commit: "c0", On: "ace", SourceFingerprint: fp, + Made: []inventory.Artifact{{Name: "one", Kind: "image", Reference: "store/one@sha256:aaaa"}}}); err != nil { t.Fatal(err) } - // What runs: the module registered with that build's image (RegisteredHeld reads the module table). if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "one", Version: "1", Resources: []map[string]any{ {"id": "app", "type": "container", "name": "one", "image": "store/one@sha256:aaaa"}}}, inventory.Source{Repository: "novox/one", Ref: "main", BuiltFrom: "c0", Asked: time.Now()}); err != nil { t.Fatal(err) } - pull := link.PullRef{Owner: "novox", Repo: "one", Number: 7} - was := checkedBuildsOf - checkedBuildsOf = func(context.Context, []link.PullRef) ([]link.CheckedBuild, error) { - return []link.CheckedBuild{{Pull: pull, Head: "headaaaaaaaa", Built: link.CheckBuilt{Module: "one", Key: "k", - Made: []link.MadeArtifact{{Name: "one", Kind: "image", Reference: "scratch/one@sha256:bbbb"}}, - Same: "unchanged: its program is the same as running build one aaaa", - SameAs: map[string]string{"one": "10.0.0.1:5000/one@sha256:aaaa"}}}}, nil - } - t.Cleanup(func() { checkedBuildsOf = was }) code := repoMerge("one", "one1aaaaaaaa", t0) - code.Number, code.HeadSHA = 7, "headaaaaaaaa1111" + b.indexed(t, "one", "1", "novox/one", "", code.Commit, fp) hear(t, open, code, t0.Add(time.Second)) cutAt(t, open, t0.Add(2*time.Minute)) ws, _ := walks(t, open) @@ -52,7 +45,7 @@ func TestAWalkWhoseEveryBuildCameOutTheSameIsDone(t *testing.T) { if w.State != inventory.PlanDone || len(w.Tiers) != 0 || len(w.Modules) != 0 { t.Fatalf("the walk reads %s, tiers %v, modules %v; want done with nothing to walk", w.State, w.Tiers, w.Modules) } - if w.Delivery == nil || !strings.Contains(w.Delivery.Unchanged["one"], "its program is the same as running build") { - t.Fatalf("the walk does not say one matched what runs: %+v", w.Delivery) + if w.Delivery == nil || !strings.Contains(w.Delivery.Unchanged["one"], "is the build the mesh holds") { + t.Fatalf("the walk does not say one's fingerprint is unchanged: %+v", w.Delivery) } } diff --git a/cmd/mesh-controller/same_program_test.go b/cmd/mesh-controller/same_program_test.go index 11afe41d..39b4a467 100644 --- a/cmd/mesh-controller/same_program_test.go +++ b/cmd/mesh-controller/same_program_test.go @@ -5,66 +5,26 @@ import ( "testing" "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" ) -// TestAWalkLeavesOutAModuleWhoseProgramDidNotChange is novox/hq issue 721: mesh-controller #335 changed one sentence -// that mesh-builder never reaches; its check found build-agent the same as the build the mesh holds, so the walk moves -// mesh-controller alone and says build-agent is unchanged. -func TestAWalkLeavesOutAModuleWhoseProgramDidNotChange(t *testing.T) { - pull := link.PullRef{Owner: "novox", Repo: "mesh-controller", Number: 335} - head := "34050dc6aaaabbbbccccdddd" - aPlan := func() *inventory.Plan { - return &inventory.Plan{Tiers: [][]string{{"mesh-controller", "build-agent"}}, - Modules: map[string]*inventory.PlanModule{"mesh-controller": {}, "build-agent": {}}, - Delivery: &inventory.PlanDelivery{Merges: []inventory.PlanMerge{{Repository: "novox/mesh-controller", - Commit: "c2ec0a60", Number: 335, Head: head}}}} - } - held := map[string]string{"build-agent/agent": "store/build-agent@sha256:aaaa", - "mesh-controller/controller": "store/mesh-controller@sha256:cccc"} - builds := []link.CheckedBuild{ - {Pull: pull, Head: head[:12], Built: link.CheckBuilt{Module: "build-agent", Key: "k1", - Made: []link.MadeArtifact{{Name: "agent", Kind: "image", Reference: "scratch/build-agent@sha256:bbbb"}}, - Same: "unchanged: its program is the same as running build agent aaaa", - SameAs: map[string]string{"agent": "10.0.0.1:5000/build-agent@sha256:aaaa"}}}, - {Pull: pull, Head: head[:12], Built: link.CheckBuilt{Module: "mesh-controller", Key: "k2", - Made: []link.MadeArtifact{{Name: "controller", Kind: "image", Reference: "scratch/mesh-controller@sha256:dddd"}}}}, - } - - p := aPlan() - same := sameAsRunning(p, builds, held) - if len(same) != 1 || same["build-agent"] == "" { +// TestAWalkLeavesOutAModuleWhoseFingerprintDidNotChange is novox/hq issue 721 on fingerprints (R4e A9, ADR 0453 §6): +// mesh-controller #335 changed one sentence that mesh-builder never reaches; the indexer's fingerprint of build-agent at +// the merge is the fingerprint of the build the mesh holds, so the walk moves mesh-controller alone and says build-agent +// is unchanged. A held build of the old form, or a fingerprint not written, leaves the module in the walk. +func TestAWalkLeavesOutAModuleWhoseFingerprintDidNotChange(t *testing.T) { + a, b := "src2:"+strings.Repeat("a", 64), "src2:"+strings.Repeat("b", 64) + now := map[string]string{"build-agent": a, "mesh-controller": b, "old": a, "unwritten": ""} + held := map[string]string{"build-agent": a, "mesh-controller": a, "old": "src1:" + strings.Repeat("a", 64)} + same := sameByFingerprint(now, held) + if len(same) != 1 || !strings.Contains(same["build-agent"], "is the build the mesh holds") { t.Fatalf("the walk leaves out %v, want build-agent alone", same) } + p := &inventory.Plan{Tiers: [][]string{{"mesh-controller", "build-agent"}}, + Modules: map[string]*inventory.PlanModule{"mesh-controller": {}, "build-agent": {}}} withhold(p, []string{"build-agent"}, nil) if len(p.Tiers) != 1 || len(p.Tiers[0]) != 1 || p.Tiers[0][0] != "mesh-controller" { t.Fatalf("the walk moves %v, want mesh-controller alone", p.Tiers) } - - // **The held build moved since the check**: what it was compared with no longer runs. - moved := map[string]string{"build-agent/agent": "store/build-agent@sha256:eeee"} - if same := sameAsRunning(aPlan(), builds, moved); len(same) != 0 { - t.Fatalf("a module compared with a build the mesh no longer holds is left out: %v", same) - } - // **A check of another head** says nothing of this merge. - other := aPlan() - other.Delivery.Merges[0].Head = "ffffffffffffffff" - if same := sameAsRunning(other, builds, held); len(same) != 0 { - t.Fatalf("a check of another head left out %v", same) - } - // **Another pull request the walk answers** that did not find it the same leaves it in. - two := aPlan() - two.Delivery.Merges = append(two.Delivery.Merges, inventory.PlanMerge{Repository: "novox/mesh-catalog", - Commit: "1234", Number: 9, Head: "abcdefabcdefabcdef"}) - if same := sameAsRunning(two, builds, held); len(same) != 0 { - t.Fatalf("a module another merge moves is left out: %v", same) - } - // **A merge with no pull request** leaves the walk as it is. - direct := aPlan() - direct.Delivery.Merges[0].Number = 0 - if same := sameAsRunning(direct, builds, held); len(same) != 0 { - t.Fatalf("a walk answering a merge without a pull request left out %v", same) - } } // TestADependentOfABaseThatCameOutTheSameIsLeftOut is ADR 0267 rule 3 (novox/hq issue 607): a dependent is in a walk diff --git a/internal/inventory/source.go b/internal/inventory/source.go index f58714b8..a498b07b 100644 --- a/internal/inventory/source.go +++ b/internal/inventory/source.go @@ -2,6 +2,8 @@ package inventory import ( "context" + "encoding/json" + "strings" "github.com/novox/mesh-controller/internal/chain" ) @@ -43,6 +45,11 @@ func (i *Inventory) sourceRows(ctx context.Context, module string) (map[string][ // standing is, of a module's builds newest first, the index of the build that stands for the build at // `from`: the oldest of the unbroken run of builds behind it with its source fingerprint. +// +// **Across the switch to the new form** (novox/hq ADR 0453 §6, ADR 0499; R4e A9): a build of the new form (`src2:`, taken +// from the builder's key) continues the run of the build before it of the old form (`src1:` or none, build-agent's) +// when every artifact of the two has the same digest: the fingerprints cannot be compared, the programs can, and equal +// digests are no move. Past it the run goes on by the old fingerprint. func standing(builds []sourceRow, from int) int { at := from fp := builds[from].fingerprint @@ -50,14 +57,53 @@ func standing(builds []sourceRow, from int) int { return at } for j := from + 1; j < len(builds); j++ { - if builds[j].fingerprint != fp || builds[j].failedGate { + if builds[j].failedGate { break } + if builds[j].fingerprint != fp { + if !newForm(fp) || newForm(builds[j].fingerprint) || !sameDigests(builds[at].made, builds[j].made) { + break + } + fp = builds[j].fingerprint + } at = j } return at } +// newForm says a source fingerprint is of the new form (ADR 0453 §6). +func newForm(fp string) bool { return strings.HasPrefix(fp, "src2:") } + +// sameDigests says two builds' artifacts, as recorded (JSON of []Artifact), are the same programs: the same names, each +// with the same digest, wherever each was pushed. A build naming an artifact with no digest is never the same. +func sameDigests(a, b []byte) bool { + digests := func(raw []byte) map[string]string { + var made []Artifact + if json.Unmarshal(raw, &made) != nil || len(made) == 0 { + return nil + } + out := map[string]string{} + for _, m := range made { + d := digestIn(m.Reference) + if d == "" { + return nil + } + out[m.Name] = d + } + return out + } + da, db := digests(a), digests(b) + if da == nil || db == nil || len(da) != len(db) { + return false + } + for name, d := range da { + if db[name] != d { + return false + } + } + return true +} + // StandingBuild is the build that stands for a module's build (novox/hq issue 280): the oldest build of // the unbroken run of builds with its source fingerprint, at or before it — itself when its fingerprint // is empty or it starts the run. Answers its id and the manifest it was recorded with; empty when the diff --git a/internal/inventory/standing_switch_test.go b/internal/inventory/standing_switch_test.go new file mode 100644 index 00000000..5d1aa7b8 --- /dev/null +++ b/internal/inventory/standing_switch_test.go @@ -0,0 +1,49 @@ +package inventory + +import ( + "encoding/json" + "testing" +) + +func madeOf(t *testing.T, refs map[string]string) []byte { + t.Helper() + var made []Artifact + for name, ref := range refs { + made = append(made, Artifact{Name: name, Kind: "bundle", Reference: ref}) + } + raw, err := json.Marshal(made) + if err != nil { + t.Fatal(err) + } + return raw +} + +// R4e A9 (ADR 0453 §6, ADR 0499): at the switch, a build taken from the builder's key (`src2:`) whose every artifact has +// the digest of the build-agent build before it (`src1:`) is no move: it stands on that build, wherever each was pushed. +// One digest that differs is a move, as is a build of the old form after one of the new. +func TestAKeysBuildWithTheOldBuildsDigestsIsNoMove(t *testing.T) { + const d1, d2 = "sha256:1111111111111111111111111111111111111111111111111111111111111111", + "sha256:2222222222222222222222222222222222222222222222222222222222222222" + old := sourceRow{id: "build-1", fingerprint: "src1:aa", made: madeOf(t, map[string]string{ + "tools": "artifact-store://x/tools/blobs/" + d1, "img": "artifact-store://x/img@" + d2})} + key := sourceRow{id: "key-bb-1", fingerprint: "src2:bb", made: madeOf(t, map[string]string{ + "tools": "artifact-store://x/tools/blobs/" + d1, "img": "registry.internal:5000/x/img@" + d2})} + if at := standing([]sourceRow{key, old}, 0); at != 1 { + t.Fatalf("the key's build with the same digests stands on %d, want the old build", at) + } + moved := key + moved.made = madeOf(t, map[string]string{"tools": "artifact-store://x/tools/blobs/" + d2, "img": "x/img@" + d2}) + if at := standing([]sourceRow{moved, old}, 0); at != 0 { + t.Fatalf("a key's build with another digest stands on %d, want itself (a move)", at) + } + // Two key builds of one fingerprint are one run, as two builds of one fingerprint always were. + again := key + again.id = "key-bb-2" + if at := standing([]sourceRow{again, key, old}, 0); at != 2 { + t.Fatalf("a run of one new fingerprint on an old build of the same digests stands on %d", at) + } + // An old build after a new one never continues the new one's run. + if at := standing([]sourceRow{old, key}, 0); at != 0 { + t.Fatalf("an old build continues a new one's run: %d", at) + } +}