A rebuild of an unchanged source is no move, whatever image digest it made (hq issue 280) #99

Merged
mesh-admin merged 1 commits from fix/an-unchanged-source-is-no-move into main 2026-10-06 20:12:06 +00:00
Contributor

novox/hq issue 280. An image is not byte-reproducible, so ADR 0236's rule that a rebuild with identical artifacts is no move never held for one. On 2026-10-06 a catalogue merge (mesh-catalog b792bf4b) rebuilt the bus without touching it. The new image digest was read as a new bus build, and every send to the control node was refused until a planned bus upgrade.

What changes

  • Builder (internal/builder/source.go): every build gets a source fingerprint, src1: followed by a sha256 over:

    • the module name and the git tree of its directory at the commit (git rev-parse HEAD:<path>). This covers the manifest, the recipes and the code: the recipe and the compiler see only that directory.
    • the git tree of each artifact context's clone;
    • every base it was handed, by digest only, so the registry address it was copied to does not count;
    • for a bundle, the toolchain image's digest and a hash of the builder's toolchain recipe.

    A build that resolves packages from the mesh registry at build time gets no fingerprint. That covers a package artifact, a TS bundle with packages of its own, and an image that reads .npmrc. Such builds fall back to artifact identity, as before. The fingerprint is carried as source-fingerprint on the build outcome.

  • Inventory: migration 0074 adds build.source_fingerprint. The new internal/inventory/source.go adds:

    • a module's builds fall into runs with one fingerprint, and the oldest build of the run stands for it. A gate-failed build breaks a run.
    • Held answers the standing build's artifacts, so modules standing on a same-source rebuild get the same base.
    • SourceFingerprints, StandingBuild and SameSourceCommits are new.
  • Controller:

    • takeIn registers a same-source rebuild at its new commit with the standing build's manifest (the old artifacts). No machine is ever sent a new digest for an unchanged source, and a later push cannot restart the bus behind the planned step's back.
    • moveFacts.identical (holds, cascades, release plans) and busPending.same (busHeld, bus upgrade) treat equal fingerprints as one build.
    • A plan's build whose run includes the commit every running machine was sent is marked sent with "no move": no send, no gate.
    • Identical artifacts stay a second way to be no move.

Tests

  • internal/builder/source_test.go: one tree and one base give one fingerprint, even with the base mirrored to another registry address. A changed tree or a moved base gives another fingerprint. A registry-decided build has none.
  • cmd/mesh-controller/same_source_test.go:
    • An image module rebuilt from an unchanged source with a new digest keeps its old digest registered at the new commit; Held hands out the old digest, and the two builds are identical. A changed source moves. A build without a fingerprint is judged by its artifacts alone.
    • The bus rebuilt from an unchanged source with a new digest: busHeld is empty, bus upgrade has nothing to do and takes no snapshot, and the shelf keeps the image the machine runs. A real change to its source demands the planned step.
    • A plan sends and gates nothing for an unchanged source; for a changed source it does a gated send to the first machine.
  • Full suite: go test -race ./... with a throwaway postgres and per-test buses, plus gofmt and vet, all green (GOFLAGS=-mod=vendor GOPROXY=off).

Note on deployment

Fingerprints exist only for builds made by a builder carrying this change. Builds recorded earlier, including the two bus builds of 2026-10-06, have none and are judged by their artifacts, exactly as now.

Why b792bf4b rebuilt the bus

It was not the pre-278 shared-path rule. The merge added merge-check.sh at the catalogue repository's root, and a root file is shared code under issue 278's rule as well. plans what-if with the merge's files and module-dirs=modules/gitea still plans 103 modules; without the root file it plans gitea alone. Narrowing that rule is left open in issue 280, because the builder reads only a module's own directory. With this PR the wide rebuild costs build time and moves nothing.

novox/hq issue 280. An image is not byte-reproducible, so ADR 0236's rule that a rebuild with identical artifacts is no move never held for one. On 2026-10-06 a catalogue merge (mesh-catalog b792bf4b) rebuilt the bus without touching it. The new image digest was read as a new bus build, and every send to the control node was refused until a planned `bus upgrade`. ## What changes - **Builder** (`internal/builder/source.go`): every build gets a **source fingerprint**, `src1:` followed by a sha256 over: - the module name and the git tree of its directory at the commit (`git rev-parse HEAD:<path>`). This covers the manifest, the recipes and the code: the recipe and the compiler see only that directory. - the git tree of each artifact context's clone; - every base it was handed, by digest only, so the registry address it was copied to does not count; - for a bundle, the toolchain image's digest and a hash of the builder's toolchain recipe. A build that resolves packages from the mesh registry at build time gets **no** fingerprint. That covers a package artifact, a TS bundle with packages of its own, and an image that reads `.npmrc`. Such builds fall back to artifact identity, as before. The fingerprint is carried as `source-fingerprint` on the build outcome. - **Inventory**: migration 0074 adds `build.source_fingerprint`. The new `internal/inventory/source.go` adds: - a module's builds fall into runs with one fingerprint, and the oldest build of the run stands for it. A gate-failed build breaks a run. - `Held` answers the standing build's artifacts, so modules standing on a same-source rebuild get the same base. - `SourceFingerprints`, `StandingBuild` and `SameSourceCommits` are new. - **Controller**: - `takeIn` registers a same-source rebuild at its new commit with the **standing build's manifest** (the old artifacts). No machine is ever sent a new digest for an unchanged source, and a later push cannot restart the bus behind the planned step's back. - `moveFacts.identical` (holds, cascades, release plans) and `busPending.same` (`busHeld`, `bus upgrade`) treat equal fingerprints as one build. - A plan's build whose run includes the commit every running machine was sent is marked sent with "no move": no send, no gate. - Identical artifacts stay a second way to be no move. ## Tests - `internal/builder/source_test.go`: one tree and one base give one fingerprint, even with the base mirrored to another registry address. A changed tree or a moved base gives another fingerprint. A registry-decided build has none. - `cmd/mesh-controller/same_source_test.go`: - An image module rebuilt from an unchanged source with a new digest keeps its old digest registered at the new commit; `Held` hands out the old digest, and the two builds are identical. A changed source moves. A build without a fingerprint is judged by its artifacts alone. - **The bus** rebuilt from an unchanged source with a new digest: `busHeld` is empty, `bus upgrade` has nothing to do and takes no snapshot, and the shelf keeps the image the machine runs. A real change to its source demands the planned step. - A plan sends and gates nothing for an unchanged source; for a changed source it does a gated send to the first machine. - Full suite: `go test -race ./...` with a throwaway postgres and per-test buses, plus gofmt and vet, all green (GOFLAGS=-mod=vendor GOPROXY=off). ## Note on deployment Fingerprints exist only for builds made by a builder carrying this change. Builds recorded earlier, including the two bus builds of 2026-10-06, have none and are judged by their artifacts, exactly as now. ## Why b792bf4b rebuilt the bus It was **not** the pre-278 shared-path rule. The merge added `merge-check.sh` at the catalogue repository's root, and a root file is shared code under issue 278's rule as well. `plans` what-if with the merge's files and `module-dirs=modules/gitea` still plans 103 modules; without the root file it plans gitea alone. Narrowing that rule is left open in issue 280, because the builder reads only a module's own directory. With this PR the wide rebuild costs build time and moves nothing.
mesh-admin added 1 commit 2026-10-06 20:11:19 +00:00
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
792352dfad
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.
mesh-admin merged commit 9b6b0c5686 into main 2026-10-06 20:12:06 +00:00
mesh-admin deleted branch fix/an-unchanged-source-is-no-move 2026-10-06 20:12:06 +00:00
Author
Contributor

Merge check: ERROR at 792352df — the check could not run: a throwaway postgres:17-alpine could not be raised: docker run --label mesh.build=build-1791317509716888018 -d --rm --name build-1791317509716888018-store -p 127.0.0.1::5432 -e POSTGRES_PASSWORD=check postgres:17-alpine: exit status 125

Run by the build seat on shanks as build-1791317509716888018 (builds --log build-1791317509716888018).

**Merge check: ERROR** at `792352df` — the check could not run: a throwaway postgres:17-alpine could not be raised: docker run --label mesh.build=build-1791317509716888018 -d --rm --name build-1791317509716888018-store -p 127.0.0.1::5432 -e POSTGRES_PASSWORD=check postgres:17-alpine: exit status 125 Run by the build seat on shanks as `build-1791317509716888018` (`builds --log build-1791317509716888018`).
Author
Contributor

Delivery novox/mesh-controller@792352dfad8c — delivered since 2026-10-06T23:05:28Z

Machines

  • build-agent on ace: passed — healthy 3 times over 2m43s
  • build-agent on the rest running it: sent
  • mesh-controller on novox: passed — healthy 3 times over 3m47s
  • mesh-controller on the rest running it: sent
  • route-proxy on ace: passed — healthy 3 times over 2m30s
  • route-proxy on the rest running it: sent

Transitions

  • 2026-10-06 23:05 (new) → proposed (announced): novox/mesh-controller#99's head announced
  • 2026-10-06 23:05 proposed → held (merged-unchecked): merged without a passing check — a verdict known with it is taken first: only a person decides that it goes on
  • 2026-10-06 23:05 held → delivering (go): its walk started on a word that was not this owner's: the controller's own path, which waits for nobody, or a person's plans go
  • 2026-10-06 23:05 delivering → delivered (done): its walk is done: every machine of its deploy plan passed, or was left as its policy says

The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.

<!-- mesh-delivery:view --> **Delivery** `novox/mesh-controller@792352dfad8c` — **delivered** since 2026-10-06T23:05:28Z **Machines** - build-agent on ace: passed — healthy 3 times over 2m43s - build-agent on the rest running it: sent - mesh-controller on novox: passed — healthy 3 times over 3m47s - mesh-controller on the rest running it: sent - route-proxy on ace: passed — healthy 3 times over 2m30s - route-proxy on the rest running it: sent **Transitions** - 2026-10-06 23:05 (new) → proposed (announced): novox/mesh-controller#99's head announced - 2026-10-06 23:05 proposed → held (merged-unchecked): merged without a passing check — a verdict known with it is taken first: only a person decides that it goes on - 2026-10-06 23:05 held → delivering (go): its walk started on a word that was not this owner's: the controller's own path, which waits for nobody, or a person's `plans go` - 2026-10-06 23:05 delivering → delivered (done): its walk is done: every machine of its deploy plan passed, or was left as its policy says The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
This repo is archived. You cannot comment on pull requests.
No Reviewers
1 Participants
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-controller#99