Packport v0.26.0 #317

Closed
PerishFire wants to merge 0 commits from release/v0.26.0 into main
Owner
Topology-preserving settlement of https://releases.plumb.perish.uk/v1/channels/stable.json.
Record the datum v0.26.0 judges against
Some checks failed
guard / guard (push) Failing after 1m49s
eeb48a6157
Read the release line from the ref, not from the job
Some checks failed
guard / guard (push) Failing after 2m35s
36e5a7b77d
Read the release line from the ref, not from the job

The guard lane declared `PLUMB_RELEASE_VERSION` at job level so `doctor` would
know which line it stood on. On the v0.26.0 release line that declaration
reached everything else the job ran, including `cargo test`, and every fixture
that invokes `doctor` was suddenly judged as standing on a release line with no
datum. Twelve tests failed on a branch where nothing was wrong.

The signal was never needed. `actions/checkout` leaves the pushed branch checked
out, so the line is already legible from the ref; a run that declares a version
still wins, which is what a release run does. The lane loses the env block and
the leak with it.

The doctor suite now clears `PLUMB_RELEASE_VERSION` before every invocation. A
fixture's verdict must not depend on the release the surrounding job happens to
be cutting, and one test says so directly by setting the variable and watching it
outrank the branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/read-the-line-from-the-ref@4341cf0c50
(cherry picked from commit 75fc604bdb)
Write the datum in TOML
Some checks failed
guard / guard (push) Has been cancelled
1196efbfd8
Write the datum in TOML

Biome formats a repository's JSON, and on the first release line to carry one it
reported the datum as badly formatted — which it was, by Biome's defaults, and
would be again under any other defaults. Machine-owned state is not a
repository's to format, but Plumb governs no formatter's configuration and
cannot ask fifteen repositories to make an exception for its seat.

So the seat stops holding a file any formatter claims. The datum is TOML now,
which is the language this domain already declares in, and which Biome does not
read at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/datum-in-toml@410e17f839
(cherry picked from commit bf67875783)
Let prepare record a datum onto a line that already stands
Some checks failed
guard / guard (push) Has been cancelled
837c3a2ceb
Let prepare record a datum onto a line that already stands

The migration note promises that an open release line can take `plumb stable
prepare` again for the same version, which records the datum and moves nothing
else. It could not. `prepare` always asked to cut, and cutting refuses once the
line has moved past the branch it was cut from — correctly, because a release
line is frozen and prepare does not move it, but that refusal also blocked the
one thing a standing line might legitimately need.

Prepare now looks before it cuts. A line that does not exist is cut and
protected as before; a line that stands is left exactly where it is and only its
datum is recorded, which is idempotent on its own. The refusal that keeps a line
from being moved is untouched, because nothing here moves one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/record-onto-a-standing-line@0871e7c617
(cherry picked from commit 8b3e9e821f)
Record the datum v0.26.0 judges against
Some checks failed
guard / guard (push) Failing after 2m24s
0841159de0
Let the recording commit own the datum seat
Some checks failed
guard / guard (push) Failing after 2m26s
8defcf3631
Let the recording commit own the datum seat

A line prepared by an older Plumb carries whatever that Plumb wrote — the very
case the migration note describes — and re-running prepare added the current
datum beside the old one instead of replacing it. Two datums in one seat is not
a state anything reads correctly, and on a protected line there is no ordinary
commit that could clean it: the release line admits cherry-picks and the datum
commit, and neither of those is a tidying commit.

So the recording commit owns the seat. It sweeps every leaf the seat holds
before writing the one this Plumb records, which makes migrating a line forward
a single re-run of prepare. `freeze` accepts the commit on the same terms, now
stated as the seat rather than the leaf: it touches nothing outside the seat,
and what it leaves there decodes for the line it names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/the-datum-owns-its-seat@9be9fca10e
(cherry picked from commit fdce3b680e)
Read a seat holding a stray leaf as unrecorded
Some checks failed
guard / guard (push) Has been cancelled
c827599b86
Read a seat holding a stray leaf as unrecorded

The sweep that gives the recording commit its seat only ran when a datum was
written, and the check that decides whether to write one looked at a single
leaf. A line carrying both an older Plumb's datum and this one's therefore
reported itself already recorded and kept both, which is the exact state the
sweep exists to prevent.

Already recorded now means what the seat says, not what one leaf says: the
current datum decodes and nothing else sits beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/a-stray-leaf-is-not-recorded@e268412db4
(cherry picked from commit 2369c7a8ae)
Record the datum v0.26.0 judges against
All checks were successful
guard / guard (push) Successful in 2m48s
3704bc5187
Export the release version before deriving from it
All checks were successful
guard / guard (push) Successful in 2m44s
3d8d677a72
Export the release version before deriving from it

The rendered ship lane planned a release by assigning the version, assigning the
channel from it, and exporting both afterwards. A shell assignment is not an
environment entry, so `plumb release channel` ran without the version it needs
and the first release to reach this lane stopped at its first step with
`PLUMB_RELEASE_VERSION is required`.

The lane exports each value as it is derived. A test reads the rendered step and
refuses an order where a derivation runs before the export it depends on, which
is the only way this class stays caught: the text is generated, and generated
text is exactly where nobody looks twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/export-before-deriving@c5570e761e
(cherry picked from commit 42ad0ce184)
Render what the runners actually need
All checks were successful
guard / guard (push) Successful in 2m44s
833a3c7f76
Render what the runners actually need

The rendered ship lane had never carried a release, and the first one that
reached it lost half its build matrix to two things the shared workflow it
replaced had always done and the render had not.

A matrix runner holds only its own target. macOS built `aarch64-apple-darwin`
and could not build `x86_64-apple-darwin`, because nothing added it. The lane now
adds the declared target before building it, as the shared workflow does.

A step whose body is a shell script must name the shell it is written in. The
Windows runner reads an undeclared body as PowerShell, so the first scripted step
failed to parse before it ran a line. The shared workflow never met this because
it installed Plumb through a composite action; a rendered lane writes the script
itself and must say what it wrote. Every scripted step in the ship lane now
declares bash, and a test walks the rendered text refusing any that does not —
generated text is exactly where nobody looks twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/render-what-the-runners-need@261834cf11
(cherry picked from commit c04509e7cd)
Write the Windows install in the shell Windows has
All checks were successful
guard / guard (push) Successful in 2m43s
fdc8692463
Write the Windows install in the shell Windows has

Declaring bash on the scripted steps moved the Windows build one line further
and stopped: `Cannot find: bash in PATH`. The runner has Git for Windows but not
its shell, so a POSIX install step cannot run there at all — which is why the
shared workflow reached for a composite action, whose two halves are a bash
script and a PowerShell script.

A rendered lane writes its own steps, so it carries both halves itself. The jobs
a matrix reaches install through bash off Windows and through `manage.ps1` on it;
the jobs that only ever run in the forge container keep the one step they had.

That needs a condition, and a rendered lane was said to carry none — because
rendering decides which jobs exist rather than gating them. The one thing render
time cannot decide is which operating system a matrix job lands on, so the law
now says what it always meant: no condition render time could have decided, and
`runner.os` is the only condition a rendered step may hold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/a-windows-runner-has-no-bash@2556cea79e
(cherry picked from commit d93176539c)
Guard the steps only a stable release runs
All checks were successful
guard / guard (push) Successful in 2m41s
c0fe1a14f6
Guard the steps only a stable release runs

The rendered seal job fetched a promotion proof and activated the stable pointer
whatever channel it was running, and a beta stopped at the first of them: a
promotion is derived for a stable version, and `v0.26.0-beta.1` is not one. The
shared workflow has always guarded both on the resolved channel; the render lost
that when it rewrote the job.

Naming the manager moves out of the activation step, because the smoke jobs need
that output on every channel and the activation it sat inside runs on one.

The law about conditions widens by the same reasoning as last time, and this is
the last shape it needs: a rendered lane carries no condition render time could
have decided, and what render time cannot decide is what only the run knows —
which operating system a matrix job lands on, and which channel the dispatched
ref carries.

The seal job runs in the forge container and never on Windows, so it goes back
to the single install step; only the matrix jobs carry both halves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/guard-the-stable-only-steps@79031eb885
(cherry picked from commit 3177c9f0a5)
Let the promotion proof travel only with stable
All checks were successful
guard / guard (push) Successful in 3m1s
c63345c9cb
Let the promotion proof travel only with stable

Guarding the promotion step was not enough. The proof's path sat in the seal
job's environment, so every step read it, and `plumb release compile` refused a
beta that appeared to be carrying a proof it could not have: only stable accepts
one.

The path now travels on the two steps that may hold it, and compiling splits by
channel exactly as the shared workflow has always split it. A test reads the
rendered seal job and refuses the proof in the job environment, or on any step
the channel does not guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/scope-the-promotion-proof@7d6e8d0337
(cherry picked from commit 754363745d)
Hand each job what its own step needs
Some checks failed
guard / guard (push) Has been cancelled
27cd25e5d5
Hand each job what its own step needs

Three more things the shared workflow carried and the render dropped, all of the
same shape: a job that was one job when the shared lane ran it, and is several
jobs now, must be handed what the single job used to have on disk.

The projection jobs gate on the compiled capsule and never saw one, because the
capsule is written in the seal job's workspace. The seal job now hands it on as
an artifact, and a projection that follows a binary picks it up. A product with
no binary has no capsule and no seal job, so nothing is handed on and nothing
asks — which the render decides, as it decides every other shape.

The smoke jobs were pointed at a manager URL produced by `plumb ship binary
manager`, a verb that has never existed. The seal job answered with an empty
string, `set -eu` did not notice because the failure sat inside a command
substitution, and every smoke died asking for the URL it had been promised. The
managers are now read from the seal or the stable pointer exactly as the shared
workflow reads them, one per platform, and each smoke runs against its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/hand-the-jobs-what-they-need@c3c8b2b40e
(cherry picked from commit a8bf5c409c)
Bind the bootstrap to the beta that will stand at the frozen commit
All checks were successful
guard / guard (push) Successful in 2m41s
1f59b2b4ff
Bind the bootstrap to the beta that will stand at the frozen commit

`v0.26.0-beta.1` published while the release line was still absorbing fixes, so
it stands at a commit the line has moved past. The bootstrap requires the
published beta to sit at the very commit stable freezes, and freeze itself
derives its promotion from the one exact seal standing there, so a beta that
falls behind the line is spent.

The contract names `v0.26.0-beta.2`. beta.1 keeps its published identity, which
is what a published identity is for; nothing points at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/bind-to-beta-two@50dd6ebd5b
(cherry picked from commit 7087ec2f40)
Install declared packages before projecting a module
All checks were successful
guard / guard (push) Successful in 2m46s
61f5ad8f2d
Install declared packages before projecting a module

The module projection reached for pnpm and there was none: the forge image
carries corepack, and corepack hands over pnpm only when a job enables it. The
shared workflow installed declared dependencies before it projected anything;
the projection job did not, because the guard lane is where that step had been
copied to and the ship lane never ran to find out.

The projection job now installs what the repository declares, on the same terms
the shared workflow used, with the store seated outside the source — which also
folds in the seating step that was standing there alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/install-before-projecting@c6a06e90e3
(cherry picked from commit 265cab1205)
Bind the bootstrap to beta.3
All checks were successful
guard / guard (push) Successful in 2m42s
3452c731ac
Bind the bootstrap to beta.3

beta.2 published and then the line moved again, for the last gap the projection
jobs had. A published beta keeps its identity and stops being the one this
release promotes; the contract names the next.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/bind-to-beta-three@3bc41e53ad
(cherry picked from commit 2ae4b8a077)
Read a seat commit as a seat commit, whatever it wrote
All checks were successful
guard / guard (push) Successful in 2m43s
guard / guard (pull_request) Successful in 2m47s
831223ae5f
Read a seat commit as a seat commit, whatever it wrote

Freeze refused the release line: it accepts a datum commit by proving the datum
this Plumb writes decodes at that commit, and the line's first datum commit was
written by a Plumb that wrote a different leaf. Every line migrated forward
carries such a commit, so every such line would refuse to freeze.

The rule was over-specified. A commit that touches nothing outside the datum
seat is a seat commit; the seat belongs to the mechanism, the recording commit
sweeps it, and whether what stands there now decodes is doctor's question, asked
of the line rather than of one commit in its history.

The bootstrap names beta.4, because beta.3 published and the line moves again to
carry this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Land-Source: task/freeze-reads-the-seat@ddaf56a3e3
(cherry picked from commit 27a8bf6c8b)
Author
Owner

Superseded by #318. This pull could not merge: main had moved past the line while the line was still absorbing fixes, so a plain merge conflicted on the lane templates, seals and changelog. The ancestry was settled instead by a merge that keeps main's tree entire and records the line as a second parent — empty diff against main, stable commit 831223a reachable from the result.

The gap that made this necessary is #320, and the resolution is now written into the brief.

Superseded by #318. This pull could not merge: main had moved past the line while the line was still absorbing fixes, so a plain merge conflicted on the lane templates, seals and changelog. The ancestry was settled instead by a merge that keeps main's tree entire and records the line as a second parent — empty diff against main, stable commit `831223a` reachable from the result. The gap that made this necessary is #320, and the resolution is now written into the brief.
PerishFire closed this pull request 2026-08-18 11:35:08 +00:00
All checks were successful
guard / guard (push) Successful in 2m43s
guard / guard (pull_request) Successful in 2m47s

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
PerishLab/plumb!317
No description provided.