From 897bcce50252202e468e82f932beede4ad88932d Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 6 Oct 2026 22:11:27 +0200 Subject: [PATCH] Issue 280: a rebuild of an unchanged source was read as a new bus An image is not byte-reproducible, so ADR 0236's no-move rule never held for one; the rule now also reads a build's source. Progressive insight on ADR 0236 says what the rule assumed and what stands. --- ...n-itself-and-so-it-rolls-out-unattended.md | 14 ++++ .../00-report.md | 83 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md diff --git a/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md b/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md index 464e28d..2573fee 100644 --- a/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md +++ b/02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md @@ -143,6 +143,20 @@ machine. So: waits and the remedy; - **a rebuild that made the same artifacts from the same manifest is no move.** + > **Progressive insight — 2026-10-06.** This rule assumed that a rebuild changing nothing makes the same + > artifacts. That holds for an archive or a bundle, which the builder packs deterministically. It does + > not hold for an image: every build of an unchanged source makes a new image digest. So a catalogue + > merge that never touched the bus rebuilt it, the new digest read as a new bus build, and every send + > to the control node was refused until a planned bus upgrade + > ([issue 280](../04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md)). + > The rule said "a rebuild that made the same artifacts from the same manifest". It now also covers + > **a rebuild made from the same source**: the module's tree at the commit, the trees of the contexts + > it read, and its bases and toolchains by digest, hashed by the builder as the build's source + > fingerprint. Such a rebuild is registered with the artifacts of the build it repeats, so no machine + > is sent a new digest. Identical artifacts remain the second way to be no move, and the only way for + > a build the registry decides. The decision stands: a rebuild that changes nothing is no move. Only + > the test for "changes nothing" was wrong. + **The release plan walks what waits.** Whenever builds no gate has seen wait on machines and no plan that has started walks them, the mesh opens a release plan: every such machine heard from, **one at a time, the control node last**, each sent everything waiting there and judged by the gate before the next is diff --git a/04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md b/04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md new file mode 100644 index 0000000..a13c933 --- /dev/null +++ b/04-ISSUES/280-a-rebuild-of-an-unchanged-source-was-read-as-a-new-bus/00-report.md @@ -0,0 +1,83 @@ +--- +status: located +opened: 2026-10-06 +located-in: [mesh-controller cmd/mesh-controller, mesh-controller internal/builder, mesh-controller internal/inventory] +fixed-by: mesh-controller PR #99 +amended-design: +--- + +# 280. A rebuild of an unchanged source was read as a new bus + +## Symptom + +On 2026-10-06 a catalogue merge added the merge check (a script at the repository's root) and changed +the forge module. Nothing under the bus module's directory changed. The merge rebuilt the bus all the +same, and the controller read the rebuild as a new bus build. From then on every send to the control +node was refused until someone ran a planned `bus upgrade`. That was a deadlock. The fix for issue 278, +which narrows what a merge rebuilds, was itself waiting to be sent to the control node. A person took +one bus step by hand to break it. + +## Diagnosis + +**Two builds of one source counted as two builds.** [ADR 0236](../../02-DECISIONS/0236-a-build-is-judged-on-its-first-machine-and-put-back-by-something-other-than-itself-and-so-it-rolls-out-unattended.md) +treats a rebuild as no move when it made the same artifacts from the same manifest. That works for an +archive or a bundle: the builder packs those deterministically, so one source gives one digest. **An +image is not byte-reproducible.** Every `docker build` of an unchanged source makes a new image +digest. So for every module shipped as an image, the bus among them, a rebuild was always a move. The +bus's guard did the rest: no send reaches the bus's machine while a new bus build waits there. + +The rule asked the wrong question. What the mesh needs to know is whether the build was made from +something different. The builder can answer that, because it reads only what it was given. The recipe +and the compiler see the module's own directory and nothing else. Any other repository an artifact +reads is cloned at a ref the manifest names. Every base is handed over by digest. A TypeScript or Go +bundle is compiled in a toolchain image the mesh holds by digest. + +**Why the merge rebuilt the bus.** The first guess was the shared-code rule from before issue 278, +still live because the new announcer had not been deployed. That guess is wrong. The merge changed a +file at the repository's root, the merge check script. **A file at the root is shared code under both +rules**: issue 278 lets a directory holding a module claim its own files, and the root is never such a +directory. The controller's `plans` what-if, which saves nothing, shows this. It was given the merge's +files with the announcer's answer (the forge module's directory holds a module). It planned 103 +modules. The same files without the root script plan the forge module alone. **The new announcer would +not have narrowed this merge.** + +## Fix + +- **The builder says what each build was made from**: its source fingerprint, a hash of + - the git tree of the module's directory at the commit built, which covers its manifest, its recipes + and its code; + - the git tree of each other repository an artifact's context is cloned from; + - every base it was handed, by digest only, so the registry address it was copied to does not count; + - for a bundle, the compiler image's digest and the builder's own recipe for that language. + + A build that pulls packages from the mesh's package registry at build time gets no fingerprint. That + covers a package artifact, a TypeScript bundle with packages of its own, and an image whose recipe + reads the registry credential. The same source can give a different program there, so such a build + is told apart by its artifacts alone, as before. The controller records the fingerprint with each + build. +- **A build whose source is unchanged is no move.** The controller treats two builds of a module with + one fingerprint as one build. It registers the rebuild at its new commit but with the artifacts of + the build it repeats, which is the oldest build in the unbroken run with that fingerprint. A build + that failed its gate breaks the run. As a result, no machine is ever sent a new digest for a source + nobody changed. A module that stands on it is handed the same base, so it does not move either. The + bus's planned step asks for nothing. A plan whose build matches what every machine running the + module was sent sends nothing and judges nothing. Identical artifacts remain a second way to be no + move. + +## Left open + +The wide rebuild itself. The builder reads only a module's own directory, so a file at a repository's +root, outside every module's directory, is read by no module built from a subdirectory. The controller +still treats such a file as shared code and rebuilds everything built from the repository. With this +fix that costs build time and no longer moves anything. Narrowing it belongs to the controller's +planning rule (issue 278's area) and is not done here. + +## How it is checked + +| Rule | Checked by | +|---|---| +| a fingerprint names what a build was made from | the builder's test: one tree and one base give one fingerprint, even with the base copied to another registry address; a changed tree or a moved base gives another; a build the registry decides, or whose tree was not named, has none | +| an unchanged source keeps its artifacts | the controller's test: an image module rebuilt from an unchanged source with a new digest is registered with the digest the mesh held, at the new commit; modules standing on it are handed that digest; the two builds are one; a changed source registers and moves; a build with no fingerprint is told apart by its artifacts | +| no bus step for an unchanged bus | the controller's test: the bus rebuilt from an unchanged source with a new digest holds no push to its machine, `bus upgrade` has nothing to do and takes no snapshot, and a real change to its source demands the planned step again | +| a plan sends nothing for an unchanged source | the controller's test: a plan's build made from the source every machine runs is marked sent with "no move", with no send and no gate; a changed source is sent to its first machine and gated | +| live | the next catalogue merge that rebuilds the bus without touching its directory demands no bus step; `bus` says every machine runs the build the mesh holds |