The image lanes track core main — DECLARED, not defaulted
⚠️ Amended 2026-09-09 (MeshWeaver#3842).
ci.ymlno longer holds a frozen pin — it resolves the newest SEALED set per run (PlatformResolution) — and the bounded-gap gate (check-platform-pins.py --check-image-gap, the third guarantee below) is deleted with it: a bound that reddens when time passes is the failure that policy forbids. The declaration, its fail-closed resolver and the recorded commit stay, andcheck-image-platform-ref.pystill enforces them.
MeshWeaver#3259. This repo reaches for the platform under two identities that are chosen from different refs at different moments, and until this change nothing in the tree said so.
| lane | how it resolves the core half | value, measured 2026-09-04 |
|---|---|---|
ci.yml — every gate |
${{ vars.MW_PLATFORM_REF \|\| env.MW_PLATFORM_REF }}, the env default a literal sha |
a frozen pin, with a fail-closed resolver job (#724) |
portal-ai-image.yml |
${{ vars.MW_PLATFORM_REF \|\| 'main' }} |
core main, resolved at the instant the step ran |
portal-next-image.yml |
same | same |
log-watcher-image.yml |
same | same |
And vars.MW_PLATFORM_REF is genuinely unset — the repo carries only BAKE_PUBLISH_TARGETS,
MW_TEST_IMAGE and PLATFORM_WEBHOOK_URL. So the || 'main' half of an expression, written at the
point of use, decided what every published image carried, and was the only place that said so.
Every gate in ci.yml judged a pair that is not the pair the images ship.
What that cost
ci.7658 shipped an image pairing a core half predating MeshWeaver#3206 with a Plugins half already
carrying #1263's refusal — a pair that was never built or tested anywhere. It existed for the
first time in production. Attributing it afterwards took a cross-repo comparison, because no lane
had written down the core commit it built against. Core records the incident in
Doc/Architecture/ImagePairSkew (MeshWeaver#3247).
The divergence is INTENDED and it stays
The obvious-looking fix — point the image lanes at ci.yml's pin — is not what landed, and it
is not an oversight that it did not.
- Images track core's tip on purpose: a core fix has to be able to reach a deployable image within the hour. That property is what made MeshWeaver#3206 deployable at all.
- MeshWeaver#1067 ("track
:main") was closed deliberately on 2026-09-01. Reopening it is a maintainer-level decision about what this repo IS, not something to slide in behind a CI cleanup. So nothing here compares one ref to another, and nothing reds when they differ. Drift is the normal state. A gate that fires constantly gets bypassed, and one that must be bypassed is worse than none.
There are THREE core-ish pins, and all three may legitimately disagree
This is the part a later reader's instinct will want to "fix". Do not.
| # | pin | what it chooses |
|---|---|---|
| 1 | uses: Systemorph/MeshWeaver/.github/workflows/<lane>.yml@<sha> |
which definition of a reusable workflow runs |
| 2 | MW_PLATFORM_REF |
which core commit the run builds and tests against (Portal hosts checks core out at it) |
| 3 | MW_IMAGE_DIGEST / MW_PLATFORM_SET |
the platform image the repo compiles against |
Plus, since this change, a fourth declared fact: MW_IMAGE_PLATFORM_REF, the core the images
are built from.
Demonstrated, not theorised. #1268 moved MW_PLATFORM_REF and both uses: refs together from
b88be8a05 to c41a34fd, and it went red. core#3268 was a change to the workflow definition, so
only the uses: refs needed it; advancing MW_PLATFORM_REF dragged the core source forward with
it (b88be8a05..c41a34fd = 48 commits — name the endpoints when citing a range; b88be8a05..9acbbb39
is 2, and both are right for their own range) and broke two tests. The landed fix, 700a83ac,
deliberately splits them: uses: on c41a34fd, MW_PLATFORM_REF back on b88be8a05,
check-platform-pins.py green on the split.
Pin 3 has its own reason to move independently: a superseded-image-assemblies: declaration is only
meaningful while the pinned image still defines the moved types, and moving the image pin is
precisely what retires it.
So check-image-platform-ref.py is indifferent to every pair of them. It never reads a job-level
uses:, never reads MW_IMAGE_DIGEST/MW_PLATFORM_SET, and never compares MW_IMAGE_PLATFORM_REF
to MW_PLATFORM_REF. Its self-test carries #1268's split as a control that could have falsified
that claim: a fixture with all three pins on different values and the image lane on main, which
must pass. It also identifies the pinned lane by the fact that it declares MW_PLATFORM_REF,
not by how it wires the resolver — otherwise rewiring ci.yml would reclassify it as an image lane
and demand a declaration it should not have.
The value being added is that each fact is stated and fails closed when unset. Never that any two of them match.
What changed
|| 'main' is the silent default antipattern: a fallback that changes what ships with nobody
declaring it. Three things replace it, in each of the three image lanes.
- A declaration. A workflow-level
env: MW_IMAGE_PLATFORM_REF: main, with the reasoning beside it. A workflowenvis visible and reviewable in the repo; a repo variable is not — which is the whole reason the unsetvars.MW_PLATFORM_REFcould stand in a comment as the thing that decides the answer while deciding nothing. - A fail-closed resolver. A first step,
id: platform-ref, reading${{ vars.MW_PLATFORM_REF || env.MW_IMAGE_PLATFORM_REF }}, which prints the ref and where it came from and exits 1 when it resolves empty. The checkout then reads that step's output, so the resolver cannot be routed around. It carries noif:— a gate must never be conditional on its own input, because GitHub paints a skipped step with the same tick as a passed one. The repo VARIABLE still wins, so one variable freezes the whole repo — gates and images together — with no PR. - A record. Each lane resolves its checkout to a concrete 40-character commit, asserts the shape, and writes it into the job summary alongside the declared ref and its source. Nothing anywhere recorded this before; it is what any later comparison of the shipped pair has to read.
The enforcement
Prose that asserts an invariant nothing checks is not an invariant.
scripts/check-image-platform-ref.py runs in validate (self-test first, so a refactor cannot make
it vacuous) and fails RED on: a re-grown quoted-literal fallback on any platform checkout, a missing
or empty declaration, two lanes declaring different values, a checkout that bypasses the
resolver, a resolver that gains an if: or loses its emptiness test, a resolver where the variable
no longer overrides the declaration, a resolver or a gate that gains a continue-on-error:, a
lane that stops recording the commit it built, and — since option (c) below — a lane that stops
bounding the gap between that commit and ci.yml's pin. It also
fails when it can find no platform checkout or no image lane at all — a guard whose subject
has moved passes having checked nothing.
The gap between the two pairs is BOUNDED — MeshWeaver#3259, option (c)
Recording the commit says which core an image ships. It does not say whether anything ever
judged that core. ci.yml pins a different one and every gate in this repo judges that pair,
so the further the two drift, the more of the shipped pair has never been built or tested anywhere.
ci.7658 is what the far end of that looks like.
So each image lane now runs, before it can push anything:
- name: The core this image ships is not far from the core ci.yml tests
env:
GH_TOKEN: ${{ github.token }}
PIN_OVERRIDE: ${{ vars.MW_PLATFORM_REF }}
BUILT: ${{ steps.platform-sha.outputs.sha }}
run: |
python3 scripts/check-platform-pins.py --check-image-gap \
--built "$BUILT" --pin-override "$PIN_OVERRIDE"
It bounds the gap. It does not abolish it
This is the distinction the whole design turns on, and the one a later reader's instinct will want to erase.
- What it measures:
compare/<ci.yml's MW_PLATFORM_REF>...<the commit this image checked out>—ahead_byfor the distance, and the two commit dates for the age. - What it refuses: only a gap over 24 hours or over 120 commits. On a normal day it is
silent: measured 2026-09-04,
main's pin7d644de95(11:07:02Z) against coremain's tip42f0cbc4e(21:20:41Z) is 10.2 h / 51 commits — comfortably inside both. - What it never asks: that the two be equal. Divergence is the intended state; making the two halves one object is option (a), which reopens MeshWeaver#1067, and it is a maintainer decision. The gate's own remedy text says so, and its self-test asserts every failure carries that sentence — a gate whose remedy quietly reopens a settled decision is worse than no gate.
One bound, not a second one
The thresholds are MAX_LAG_HOURS = 24 and MAX_COMMITS_BEHIND = 120 in
scripts/check-platform-pins.py — the same two constants --check-staleness already applies to
this repo's two frozen platform inputs (#1264), reached through the same _distance_problems
judgement. The workflow passes neither, so the script is the policy. Two different bounds for one
question would be its own defect, and widening one to make a run pass is the band-aid this gate
exists to make unnecessary: the pin can always move instead.
The judgement gained one field to do this — StalenessSubject.tip, the far end. The two
staleness arms measure a frozen input against core main; this arm measures the tested pin against
a commit one image really built, which is core's tip only at the instant that lane ran. It
defaults to "core main", so the older arms' text is byte-identical.
Why --pin-override exists
vars.MW_PLATFORM_REF overrides ci.yml's pin and all three image lanes' declaration at once.
When it is set, both halves follow one ref and the gap is nil by construction — so the gate is
handed the run's own view of the variable rather than reading the file. Measuring the file's literal
while a variable drove the run would be a gate reporting on a platform nobody built, which is the
same defect it exists to catch.
The trapdoors it does not have
Its only external input is GITHUB_TOKEN reading a public repository, so there is nothing to
provision and therefore nothing to ask whether exists; its other input is produced two steps
earlier by a step that already fails RED when the platform checkout did not resolve. It carries no
if: and no continue-on-error:, and it runs before ACR login, so a refused pair costs no image.
Every path that could otherwise only be seen inside an image lane — no token, an empty --built, a
--built that is a branch name, a core that refuses, a pin core does not know, a built commit core
does not know — is RED, and each is a self-test case.
The guard on the guard
scripts/check-image-platform-ref.py gained rule (7): every image lane must carry the gate,
reading the recorded commit through steps.<recorder>.outputs.* (a literal, or another step's
output, is refused — a grep hit is not a binder), passing ${{ vars.MW_PLATFORM_REF }} to
--pin-override, carrying no if: and no continue-on-error:, and appearing strictly before
the first step that logs in to, pushes to, or publishes into a registry. A lane that no longer
publishes at all fails too: the ordering rule would otherwise bind to nothing. Rule (5) gained the
same continue-on-error: refusal on the resolver — the twin of its if: trapdoor.
🚨 A SUBSTRING IS NOT AN INVOCATION
The first draft of rule (7) matched --check-image-gap as text, and the Copilot review on #1346
found two ways through it. Both were reproduced before being closed:
--check-image-gap=false.check-platform-pins.pyparses--flag=valueintovalues, not intoswitches, so the mode is never entered:main()falls through to the DEFAULT pin-agreement check and prints "✓ every platform pin in ci.yml agrees with its own copies" — exit 0, and not one word about the shipped pair. Measured directly, not reasoned about. A lane carrying that line reads as gated to any reader and to any grep.--max-lag-hours 999999. The overrides exist so an incident can widen the bound for one manual run without a commit. In a workflow they are the band-aid this whole gate exists to make unnecessary — the pin can always move instead.- …and two more the same parse closes: another mode on the same command line (
main()dispatches self-test → check-tags → check-staleness → image-gap, first match wins, so an earlier one silently replaces this one), and the whole line commented out.
So the invocation is now parsed: the mode must appear bare, exactly once, with no = form,
no threshold override, and no second mode — over a body with whole-line # comments stripped, since
a commented-out flag neither runs nor counts. Each shape is a mutation case in the self-test.
The ordering rule had the mirror of the same defect: index > first_publishing_step passes on
equality, so a single step that pushes and then calls the checker satisfied it while the bytes
had already left. It is now strictly <, with a same-step mutation case.
It was watched failing
Live, against the real API. With
ci.yml's pin moved (in a scratch copy — no pin value was edited) to17ee6d9fa, the shaci.7574was cut from, against the commit an image would have built that evening:pair gap: 60.6 h (bound 24 h), 428 commit(s) (bound 120), exit 1, both bounds named —MW_PLATFORM_REF, the core this repo TESTS against, is 60.6 h behind the core commit this image built, over the 24 h bound…the core commit this image built is 428 commits ahead of the tested pin, over the 120 boundand the same invocation against the real, unmutated
ci.ymlexits 0 at 10.2 h / 51 commits.The guard. With the gate deleted from
log-watcher-image.yml,check-image-platform-ref.pyanswered "log-watcher-image.yml does not BOUND the gap between the core it ships and the core ci.yml tests — no step runscheck-platform-pins.py --check-image-gap" and exited 1.
What this does NOT do — stated plainly
It DETECTS a large gap; it PREVENTS nothing. The class of fault that took production down — a core/Plugins pair that was never built or tested anywhere — is bounded now, not eliminated:
- A SMALL gap is still an untested pair. 51 commits inside the bound is 51 commits of core that no gate in this repo judged before the image shipped. The bound is calibrated on the case that hurt, not on the case that cannot.
- Nothing boots the pair and nothing signs in. The
signin-smokegate (MeshWeaver#3254, option (d)) was closed unmerged by maintainer decision;main-cd.ymlis untouched. That was the only proposal that did not depend on predicting which pairings are dangerous — it booted whatever pair was actually built. With it declined, the static comparison above is the only thing that observes the shipped pair at all. - Only option (a) — building the images from the pin — would prevent the class outright. It remains an open maintainer decision, unchanged by this: it reopens MeshWeaver#1067, and its cost is that a core fix would no longer reach any image until someone moved the pin.
Operating it
- To freeze the whole repo (a bisect, an upstream outage, a deliberate hold): set the repo
variable
MW_PLATFORM_REFto a sha or a tag. It overridesci.yml's env default and all three image lanes' declaration, needs no PR, and the resolver prints which source answered. - To change what the image lanes track: edit
MW_IMAGE_PLATFORM_REFin all three workflows, in one commit. The guard refuses a half-moved set — three copies exist only because a workflowenvcannot be shared across files, and nothing in Actions relates one copy to another. - To find which core an image carries: read the run's job summary.
declared ref,ref sourceandresolved commitare the three rows. - When an image lane reds on the gap: move
MW_PLATFORM_REFinci.ymlforward — one literal, both ends checked (git grep -c <symbol> <sha> -- <path>at each end, never by date), as that pin's own header says. Moving it is also what--check-stalenesswould be asking for independently, since the gap and the pin's own staleness are nearly the same number. Never widen the bound, and never close the gap by pointing an image lane at the pin — that is option (a) and it is not a fix for a red gate. - …and when the pin is HELD ON PURPOSE and cannot move — a bisect, a core contract this repo has
not reconciled, exactly what #1268 did when advancing
MW_PLATFORM_REF48 commits reddened twoPluginCatalogtests — the answer is the repo variableMW_PLATFORM_REF, which freezes both halves together:ci.yml's pin and all three image lanes at once, no PR. That is the one scenario where this gate reds while nobody has done anything wrong, and it is not a false red: for as long as the pin is held back, an image built from core's tip carries a core half this repo's own gates cannot judge — the ci.7658 shape. The gate makes shipping that a deliberate choice instead of the default. The cost of freezing (core fixes stop reaching images until the hold ends) is option (a)'s cost, taken temporarily and explicitly rather than permanently. - To run the gate by hand (needs a token; reads only public core):
GH_TOKEN=$(gh auth token) python3 scripts/check-platform-pins.py --check-image-gap --built <40-hex>— add a workflow path as a positional argument to measure against aci.ymlother than this repo's.