Read a rebuild of an unchanged source as no move, whatever image digest it made (hq issue 280)
mesh/merge-gate error: the check could not run: a throwaway postgres:17-alpine could not be raised: docker run --label mesh.build=build-1791317509716888018…
mesh/delivery delivered

An image is not byte-reproducible, so ADR 0236's 'same artifacts is no move'
never held for one: a catalogue merge that did not touch the bus rebuilt it,
and every send to the control node waited for a planned bus upgrade.

The builder now records a source fingerprint per build (module tree, context
trees, bases and toolchains by digest). A rebuild with the fingerprint of the
build it repeats is registered with that build's artifacts, handed to modules
standing on it, holds no push, demands no bus step, and a plan sends and
gates nothing for it. Identical artifacts remain a second way to be no move.
This commit is contained in:
2026-10-06 22:09:11 +02:00
parent b9e0cd34c3
commit 792352dfad
15 changed files with 773 additions and 35 deletions
+1
View File
@@ -292,6 +292,7 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
})
}
result.Against = built.Against
result.SourceFingerprint = built.Source
for _, r := range built.Read {
result.Read = append(result.Read, link.ReadRepository{Repository: r.Repository, Ref: r.Ref})
}
+2
View File
@@ -105,6 +105,7 @@ func buildOnce(ctx context.Context, args []string) error {
Ref: *ref,
Manifest: built.Manifest,
Against: built.Against,
Source: built.Source,
}
for _, r := range built.Read {
out.Read = append(out.Read, readRepository{Repository: r.Repository, Ref: r.Ref})
@@ -135,6 +136,7 @@ type onceResult struct {
Made []madeArtifact `json:"made"`
Against []string `json:"against,omitempty"`
Read []readRepository `json:"read,omitempty"`
Source string `json:"source-fingerprint,omitempty"`
}
// readRepository is a repository this build read source from besides the module's own.
+19
View File
@@ -148,6 +148,8 @@ func buildFrom(result link.BuildResult) inventory.Build {
// be, for exactly the modules it needs most. Keeping them is what makes a replay able to
// rebuild the graph rather than a list of names.
Path: result.Path,
// What it was made from (novox/hq issue 280): two builds with one are one build.
SourceFingerprint: result.SourceFingerprint,
}
// When it was asked, which is what orders it against another build of the same module
// (novox/hq 04-ISSUES/219) — not when it was heard.
@@ -580,6 +582,23 @@ func takeIn(ctx context.Context, inv *inventory.Inventory, result link.BuildResu
"back: it is recorded and not registered again — a newer build is", result.On, manifest.Module,
short(result.Commit))
}
// **A build whose source is unchanged is never a move** (novox/hq issue 280): a rebuild made from
// what the build the mesh stands on was made from registers that build's artifacts at the new
// commit, so no machine is sent a new digest for a source nobody changed — an image is not
// byte-reproducible, and the bus rebuilt for another module's merge demanded a planned upgrade.
if kept.SourceFingerprint != "" {
stands, raw, err := inv.StandingBuild(ctx, manifest.Module, kept.ID)
if err != nil {
return manifest, kept, err
}
if stands != "" && stands != kept.ID && len(raw) > 0 {
if same, err := catalogue.ParseManifest(raw); err == nil && same.Module == manifest.Module {
fmt.Printf("%s at %s was made from the source %s was: registered with its artifacts, no move\n",
manifest.Module, short(result.Commit), stands)
manifest = same
}
}
}
if err := inv.RegisterModule(ctx, manifest, recorded); err != nil {
if errors.Is(err, inventory.ErrSuperseded) {
return manifest, kept, fmt.Errorf("%s built %s (%s), recorded and not registered: %w",
+14 -3
View File
@@ -54,13 +54,15 @@ type busPending struct {
machines []string
from map[string]string
to string
// same are the commits whose build made the same artifacts and manifest as the build the mesh holds.
// same are the commits whose build was made from the same source as the build the mesh holds, or
// made the same artifacts and manifest (novox/hq issue 280).
same map[string]bool
}
// moves is whether sending the machine would replace its bus: a build it was not last sent, unless the
// two builds made the same artifacts from the same manifest — a rebuild of the same source for another
// module's merge changes nothing the machine runs.
// two builds were made from the same source, or made the same artifacts from the same manifest — a
// rebuild of the same source for another module's merge changes nothing the machine runs, whatever
// image digest it made (novox/hq issue 280).
func (b busPending) moves(machine string) bool {
from, known := b.from[machine]
if b.module == "" || b.to == "" || (known && sameCommit(from, b.to)) {
@@ -100,6 +102,15 @@ func pendingBus(ctx context.Context, inv *inventory.Inventory) (busPending, erro
for commit, refs := range made {
b.same[commit] = refs != "" && refs == made[b.to]
}
sources, err := inv.BuildSourceFingerprints(ctx, b.module)
if err != nil {
return b, err
}
for commit, src := range sources {
if src != "" && src == sources[b.to] {
b.same[commit] = true
}
}
for _, n := range b.machines {
sent, known, err := inv.SentBuilds(ctx, n)
if err != nil {
+48 -4
View File
@@ -72,8 +72,10 @@ var errWalkedElsewhere = errors.New("a plan already walking a build there sends
type moveFacts struct {
current map[string]inventory.CurrentBuild
fps map[string]map[string]string
passed map[string]map[string]bool
plans []inventory.Plan
// srcs is, per module, per commit, its build's source fingerprint (novox/hq issue 280).
srcs map[string]map[string]string
passed map[string]map[string]bool
plans []inventory.Plan
}
func readMoveFacts(ctx context.Context, inv *inventory.Inventory) (moveFacts, error) {
@@ -85,6 +87,9 @@ func readMoveFacts(ctx context.Context, inv *inventory.Inventory) (moveFacts, er
if f.fps, err = inv.Fingerprints(ctx); err != nil {
return f, err
}
if f.srcs, err = inv.SourceFingerprints(ctx); err != nil {
return f, err
}
if f.passed, err = inv.PassedCommits(ctx); err != nil {
return f, err
}
@@ -92,12 +97,17 @@ func readMoveFacts(ctx context.Context, inv *inventory.Inventory) (moveFacts, er
return f, err
}
// identical is whether two builds of a module put the same thing on a machine: the same commit, or
// builds that made the same artifacts from the same manifest.
// identical is whether two builds of a module put the same thing on a machine: the same commit,
// builds made from the same source (novox/hq issue 280) — the module's tree, the contexts it read, its
// bases and toolchains, whatever digests an image rebuild made of them — or builds that made the same
// artifacts from the same manifest.
func (f moveFacts) identical(module, a, b string) bool {
if a == b || sameCommit(a, b) {
return true
}
if sa := f.srcs[module][a]; sa != "" && sa == f.srcs[module][b] {
return true
}
fa := f.fps[module][a]
return fa != "" && fa == f.fps[module][b]
}
@@ -112,6 +122,40 @@ func (f moveFacts) gated(module, commit string) bool {
return false
}
// unchangedOnEvery is whether a plan's build of a module was made from the source of the build every
// machine running it was last sent (novox/hq issue 280): no move on any of them. False when no machine
// runs it, when what one was sent is not known, or when the build stands for itself.
func unchangedOnEvery(ctx context.Context, inv *inventory.Inventory, module, build string, running []string) (bool, error) {
if build == "" || len(running) == 0 {
return false, nil
}
same, err := inv.SameSourceCommits(ctx, module, build)
if err != nil || len(same) == 0 {
return false, err
}
for _, n := range running {
sent, known, err := inv.SentBuilds(ctx, n)
if err != nil {
return false, err
}
was, carried := sent[module]
if !known || !carried || !inRun(same, was) {
return false, nil
}
}
return true, nil
}
// inRun is whether a commit is one of a run's, however either is abbreviated.
func inRun(run map[string]bool, commit string) bool {
for c := range run {
if sameCommit(c, commit) {
return true
}
}
return false
}
// moves is what a machine's next send would move that no gate has seen: modules whose policy rolls out
// (a recorded one is a person's push), that the machine was sent before, moving to a build not identical
// to the one it runs and that has passed no gate. A machine whose last send's builds are not known names
+14
View File
@@ -620,6 +620,20 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
if err != nil {
return false, err
}
// **A build whose source is unchanged is never a move** (novox/hq issue 280): made from the source
// of the build every machine running it was sent, it is registered with that build's artifacts —
// nothing to send, and nothing for a gate to judge.
if state.FirstAt == nil {
if same, err := unchangedOnEvery(ctx, inv, m, state.Build, running); err != nil {
return false, err
} else if same {
now := time.Now().UTC()
state.SentAt = &now
state.Why = "no move: made from the source every machine running it runs"
fmt.Printf("%s: %s built from the source every machine running it runs: no move, nothing sent\n", p.ID, m)
continue
}
}
policy, err := inv.UpgradeOf(ctx, m)
if err != nil {
return false, err
+209
View File
@@ -0,0 +1,209 @@
package main
import (
"context"
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// A build whose source is unchanged is never a move (novox/hq issue 280): an image is not
// byte-reproducible, so two builds of one source make two digests, and the rule that a rebuild with
// identical artifacts is no move (ADR 0236) never held for one.
// anImageBuild is a build outcome of an image module: its manifest names the image by the digest made.
func anImageBuild(t *testing.T, module, id, commit, digest, source string, asked time.Time) link.BuildResult {
t.Helper()
image := "registry.invalid:5000/" + module + "/server@sha256:" + digest
manifest, _ := json.Marshal(map[string]any{"module": module, "version": "1",
"resources": []any{map[string]any{"id": "svc", "type": "container", "name": module, "image": image}}})
return link.BuildResult{ID: link.NewBuildID(asked), Repository: "novox/mesh-catalog", Path: "modules/" + module,
On: "anchor", Module: module, Commit: commit, Manifest: manifest, SourceFingerprint: source,
Made: []link.MadeArtifact{{Name: "server", Kind: "image", Reference: image}},
Source: &link.SourceOnSeat{Seat: "git", Repository: "novox/mesh-catalog"}}
}
// shelfNames is whether the module the mesh holds names this digest.
func shelfNames(t *testing.T, inv *inventory.Inventory, module, digest string) bool {
t.Helper()
shelf, err := inv.Catalogue(t.Context())
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(shelf[module])
return strings.Contains(string(raw), digest)
}
// A rebuild of an unchanged source is registered with the artifacts of the build it was first made
// as: no machine is sent a new digest, a module standing on it is handed the same base, and the two
// builds are one to every rule that asks whether a send moves something. A real source change moves.
func TestARebuildOfAnUnchangedSourceKeepsItsArtifacts(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
start := time.Now().Add(-time.Hour)
for i, b := range []link.BuildResult{
anImageBuild(t, "app", "", "c1aaaaaa", strings.Repeat("a", 64), "src1:same", start),
// Another module's merge rebuilt it: a new commit, a new image digest, the same source.
anImageBuild(t, "app", "", "c2bbbbbb", strings.Repeat("b", 64), "src1:same", start.Add(time.Minute)),
} {
if _, _, err := takeIn(ctx, inv, b); err != nil {
t.Fatalf("build %d: %v", i, err)
}
}
if !shelfNames(t, inv, "app", strings.Repeat("a", 64)) || shelfNames(t, inv, "app", strings.Repeat("b", 64)) {
t.Fatal("a rebuild of an unchanged source registered the digest it made, not the one the mesh holds")
}
if src, err := inv.SourceOf(ctx, "app"); err != nil || src.BuiltFrom != "c2bbbbbb" {
t.Fatalf("the module is not at the commit it was rebuilt from: %+v %v", src, err)
}
held, err := inv.Held(ctx)
if err != nil || !strings.HasSuffix(held["app/server"], strings.Repeat("a", 64)) {
t.Fatalf("a module standing on it is handed %q, not the build the mesh holds (%v)", held["app/server"], err)
}
f, err := readMoveFacts(ctx, inv)
if err != nil {
t.Fatal(err)
}
if !f.identical("app", "c1aaaaaa", "c2bbbbbb") {
t.Fatal("two builds of one source are not one build")
}
// A real change to the source moves: its own digest registered, and not identical.
if _, _, err := takeIn(ctx, inv, anImageBuild(t, "app", "", "c3cccccc", strings.Repeat("c", 64), "src1:changed",
start.Add(2*time.Minute))); err != nil {
t.Fatal(err)
}
if !shelfNames(t, inv, "app", strings.Repeat("c", 64)) {
t.Fatal("a changed source did not register what it made")
}
if f, _ = readMoveFacts(ctx, inv); f.identical("app", "c2bbbbbb", "c3cccccc") {
t.Fatal("a changed source is read as the same build")
}
// And a builder that says no fingerprint is told apart by its artifacts alone, as before.
if _, _, err := takeIn(ctx, inv, anImageBuild(t, "app", "", "c4dddddd", strings.Repeat("d", 64), "",
start.Add(3*time.Minute))); err != nil {
t.Fatal(err)
}
if !shelfNames(t, inv, "app", strings.Repeat("d", 64)) {
t.Fatal("a build with no fingerprint did not register what it made")
}
}
// The bus rebuilt for another module's merge, its source untouched and its image digest new: no move —
// no push to its machine is held, no bus step is demanded, and `bus upgrade` has nothing to do. A real
// change to the bus's source is the planned step again.
func TestABusRebuiltFromAnUnchangedSourceDemandsNoBusStep(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
start := time.Now().Add(-time.Hour)
bus := func(id, commit, digest, source string, at time.Time) link.BuildResult {
b := anImageBuild(t, "nats", id, commit, digest, source, at)
manifest, _ := json.Marshal(map[string]any{"module": "nats", "version": "2",
"provides": []any{map[string]any{"name": "mesh-bus"}},
"resources": []any{map[string]any{"id": "svc", "type": "container", "name": "nats",
"image": b.Made[0].Reference}}})
b.Manifest = manifest
return b
}
if _, _, err := takeIn(ctx, inv, bus("", "n1111111", strings.Repeat("a", 64), "src1:bus", start)); err != nil {
t.Fatal(err)
}
if _, err := inv.Assign(ctx, "anchor", "nats"); err != nil {
t.Fatal(err)
}
if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"nats": "n1111111"}); err != nil {
t.Fatal(err)
}
// The wide rebuild: a new commit, a new image digest, the same source.
if _, _, err := takeIn(ctx, inv, bus("", "n2222222", strings.Repeat("b", 64), "src1:bus", start.Add(time.Minute))); err != nil {
t.Fatal(err)
}
if held, err := busHeld(ctx, inv, []string{"anchor"}); err != nil || len(held) != 0 {
t.Fatalf("a bus rebuilt from an unchanged source held its machine: %v %v", held, err)
}
wasSnapshot := takeBusSnapshot
t.Cleanup(func() { takeBusSnapshot = wasSnapshot })
takeBusSnapshot = func(context.Context, string, string) (string, error) {
return "", errors.New("no snapshot is taken for a bus step nobody needs")
}
var sent [][]string
wasSend := sendRollout
sendRollout = func(_ context.Context, _ *stores, names []string) ([]string, error) {
sent = append(sent, names)
return names, nil
}
t.Cleanup(func() { sendRollout = wasSend })
if err := busCommand(ctx, []string{"upgrade", "--why", "nothing changed", "--reversible"}); err != nil || len(sent) != 0 {
t.Fatalf("a bus step ran for a bus whose source is unchanged: %v, sent %v", err, sent)
}
if !shelfNames(t, inv, "nats", strings.Repeat("a", 64)) {
t.Fatal("the bus the mesh holds is not the image its machine runs")
}
// A real change to the bus's source: the planned step.
if _, _, err := takeIn(ctx, inv, bus("", "n3333333", strings.Repeat("c", 64), "src1:bus-2.12", start.Add(2*time.Minute))); err != nil {
t.Fatal(err)
}
held, err := busHeld(ctx, inv, []string{"anchor"})
if err != nil || !strings.Contains(held["anchor"], "planned step") {
t.Fatalf("a changed bus did not demand its planned step: %v %v", held, err)
}
}
// A plan's build made from the source every machine running the module runs is not sent and not
// judged: nothing moves. A build of a changed source is sent to its first machine, gated, as before.
func TestAPlanSendsNothingForAnUnchangedSource(t *testing.T) {
for _, tc := range []struct {
name string
source string
sent [][]string
}{
{"unchanged", "src1:app", nil},
{"changed", "src1:app-changed", [][]string{{"anchor"}}},
} {
t.Run(tc.name, func(t *testing.T) {
g := aGateMesh(t)
ctx := t.Context()
inv := g.open.inventory
// What anchor and laptop run (c1) was made from src1:app; the plan's build of c2 from the source
// the case says.
for _, b := range []inventory.Build{
{ID: "build-1s", Module: "app", Commit: "c1", SourceFingerprint: "src1:app",
Asked: time.Now().Add(-30 * time.Second), At: time.Now().Add(-30 * time.Second)},
{ID: "build-2s", Module: "app", Commit: "c2", SourceFingerprint: tc.source,
Asked: time.Now(), At: time.Now()},
} {
b.Manifest, _ = json.Marshal(catalogue.Manifest{Module: "app", Version: "1"})
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
p := g.plan(t)
p.Modules["app"].Build = "build-2s"
if err := inv.SavePlan(ctx, &p); err != nil {
t.Fatal(err)
}
advancePlans(ctx, g.open)
if !reflect.DeepEqual(g.sent, tc.sent) {
t.Fatalf("sent %v, want %v", g.sent, tc.sent)
}
p = g.plan(t)
s := p.Modules["app"]
if tc.sent == nil && (s.SentAt == nil || s.Gate != nil || !strings.Contains(s.Why, "no move")) {
t.Fatalf("an unchanged source was not read as no move: %+v", s)
}
if tc.sent != nil && (s.Gate == nil || len(s.First) == 0) {
t.Fatalf("a changed source was not sent to its first machine, gated: %+v", s)
}
})
}
}