Repository navigation
fix(#3461): the upstream availability gate reads the publication, not the prefix - #4345
Conversation
… the prefix
🚨 A LIVE false red, not phase-5 preparation. `check-release-availability.sh` —
the gate `main-cd`, `release.yml` and EVERY node repo's `publish-bake` run to ask
"is this upstream published for the identity I am about to build against?" —
probed `prebuilt-bundles/<identity>/<source>/_complete`. That is the FLAT
compatibility copy's sentinel, and under the generation layout the two answers
come apart on every publish:
1. publish_publication seals the generation → complete
2. … moves `_current` → LIVE to every reader
3. … then refreshes the flat copy, and publish_one_target
begins that by DELETING its `_complete` → prefix has NO sentinel
For that whole upload-and-verify interval (~90 s per target) the gate answered
`false` and reported `no sealed publication under …` for a publication that is
sealed, live and pointed at. That wording is not neutral: it is the message that
held MeshWeaver.Reinsurance 23 times in 24 hours (#3583), so a false instance
costs a reader the same investigation as a true one.
Live for `plugins` since #4269 (05:33Z today) and, since #4341 (c86aea8),
for crm / education / reinsurance / socialmedia / manufacturing as well — which
is why this lands now rather than with phase 5.
🚨 And it is a THIRD Azure-direct reader of this prefix. The design page and the
pointer-readers harness both said there were TWO (compose-sealed-modules.sh and
node-repo-gate.yml's seed, routed in phase 3). Both are corrected: a page that
enumerates readers and is wrong about the denominator is how this one survived
phase 3, phase 4 and two reviews.
The fix is the siblings' resolver, verbatim, with the siblings' fall-back: an
absent, blank, unreadable, escaping or dangling pointer means the source
directory — this gate's own previous behaviour — so the change can only ever add
an answer it used to get wrong.
WATCHED FAILING. test-publication-pointer-readers.py now executes THREE readers
(17 cases). The new fixture IS the window: generation sealed, `_current` naming
it, the prefix's own sentinel REMOVED.
* against origin/main's script: 1 case fails, with the literal false-hold text
`no sealed publication under prebuilt-bundles/<identity>/plugins`;
* against this branch: 17/17.
* Its two siblings — nothing sealed anywhere ⇒ still REFUSES; a flat prefix
with no pointer ⇒ still sealed — pass on BOTH, which is what makes them
controls rather than new behaviour, and what stops the case above passing
because the script always exits 0.
Siblings green: test-release-availability-refusals.py (the three outcomes stay
distinguishable), shellcheck -S warning clean, MeshWeaver.Documentation.Test
link integrity + topic map + What's New + NodeRepoLaneHostGuard 15/15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Test Results (shard 0)262 tests 262 ✅ 2m 9s ⏱️ Results for commit eb68928. |
There was a problem hiding this comment.
🟡 Changes recommended
Add fallback-branch coverage and correct the remaining stale reader-count documentation.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Updates the upstream availability gate to follow _current publication pointers, preventing false build holds during flat-copy refreshes.
Changes:
- Adds pointer-aware resolution with safe fallback behavior.
- Extends regression coverage for the availability gate.
- Updates architecture documentation and What's New.
File summaries
| File | Summary |
|---|---|
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-14-a-publication-is-checked-where-it-actually-is.md |
Adds the user-facing fix entry. |
src/MeshWeaver.Documentation/Data/Architecture/SealedPublicationReads.md |
Documents the third reader; coverage wording still needs updating. |
src/MeshWeaver.Documentation/Data/Architecture/SealedPublicationGenerations.md |
Documents the false-red fix; remaining two-reader references need correction. |
.github/scripts/test-publication-pointer-readers.py |
Adds availability-gate tests; fallback branches and stale reader-count wording need coverage or updates. |
.github/scripts/check-release-availability.sh |
Resolves the active publication before checking its seal. |
Review details
Suppressed comments (2)
.github/scripts/test-publication-pointer-readers.py:429
- The new availability reader is only exercised for a pointed sealed generation and the flat/no-seal controls. Its safety-critical blank, unreadable, escaping, and dangling-pointer fallbacks are not run through this reader, so a regression in those branches could still pass this guard despite the shared contract; add equivalent
run_availabilityfallback fixtures.
print("the availability gate, during the window in which the flat copy is unsealed:")
root = base / "avail-window" / "remote"
stage(root, pointer=GENERATION)
(root / ACCOUNT / SHARE / "prebuilt-bundles" / IDENTITY / SOURCE / "_complete").unlink()
proc = run_availability(base / "avail-window" / "run", root)
src/MeshWeaver.Documentation/Data/Architecture/SealedPublicationGenerations.md:328
- The page now introduces three Azure-direct readers here, but it still contains a later Phase 3 heading that says “The two Azure-direct readers” and a status bullet that says
bothare covered by the harness. Those remaining statements contradict the new reader and its 17-case test, so the architecture page still reports an incorrect migration state.
- 🚨 **THREE Azure-direct readers composed their paths under the bare PREFIX — not two, which is
what this page said for a week.** `compose-sealed-modules.sh` (the module-set index and each
module, on the OIDC fallback path) and `node-repo-gate.yml`'s inline `download-batch` were routed
in phase 3. The third, found on 2026-09-14 **after** phase 4 had landed, is
`check-release-availability.sh` — the upstream gate `main-cd`, `release.yml` and every node repo's
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @@ -2,10 +2,12 @@ | |||
| """The two Azure-direct readers of a sealed publication, against a share that HAS a generation. | |||
There was a problem hiding this comment.
Correct — and it is the same defect this PR exists to fix, one level up: a count left behind by an edit that changed everything except the sentence someone reads first.
Fixed, together with your second finding and the fall-back case your summary asked for. Landing as a follow-up rather than here, because this PR was admitted to the merge queue before the review arrived (a push to a queued branch is refused, and dequeuing to correct a docstring is not worth a steward event on a change whose shipped behaviour is already right).
The follow-up carries:
- the docstring's opening line — "The THREE Azure-direct readers…";
SealedPublicationReads's bullet rewritten whole (it still said "14 cases over the two readers", and the edit that added the third reader had left a duplicated half-sentence in it) — now one coverage statement, 18 cases over three readers;- a case for the new resolver's fall-back branch: a DANGLING pointer must degrade to the prefix — this gate's own previous behaviour — rather than reporting the upstream absent. A resolver that read "the generation named is not there" as "nothing is published" would turn an interrupted pointer move into a fleet-wide hold, strictly worse than the defect it replaces. It asserts both halves (
exit 0withsealed:and theis not on the sharewarning), so the fall-back cannot pass in silence.
Watched failing: against origin/main's script both new availability cases fail — the window case with the literal false-hold text, the dangling case because the old script resolves nothing and therefore warns nothing. Against the branch: 18/18.
| - 🚨 **THREE Azure-direct readers addressed the PREFIX rather than the publication**, and this | ||
| entry said two until 2026-09-14. The third is `check-release-availability.sh`, the upstream gate | ||
| every publishing repo runs, which probed the prefix's own sentinel and therefore answered | ||
| *"no sealed publication"* for the ~90 s of every publish in which the flat copy is unsealed — | ||
| routed through the same resolution now. |
There was a problem hiding this comment.
Correct, and worse than a stale number: the edit that added the third reader to this bullet left a duplicated half-sentence in it too ("compose their paths / used to compose their paths"), so the paragraph no longer parsed.
Rewritten whole in the follow-up (this PR is already in the merge queue, so a push here is refused): the two readers phase 3 routed, then the third and what it was answering wrong, then one coverage statement — 18 cases over the three readers — instead of a count embedded in a clause about two of them.
That single coverage sentence is the point of the change rather than a tidy-up. A page that enumerates readers and is wrong about the denominator is exactly how check-release-availability.sh survived phase 3, phase 4 and two reviews: every reading of "are they all routed?" was answered against a list of two.
Test Results (shard 1)1 473 tests 1 473 ✅ 2m 58s ⏱️ Results for commit eb68928. |
Test Results (shard 2)915 tests 915 ✅ 3m 29s ⏱️ Results for commit eb68928. |
Test Results (shard 4)1 863 tests 1 671 ✅ 4m 42s ⏱️ Results for commit eb68928. |
Test Results (shard 5) 6 files 6 suites 5m 24s ⏱️ Results for commit eb68928. |
Test Results (shard 3)1 103 tests 1 101 ✅ 6m 24s ⏱️ Results for commit eb68928. |
Test Results 18 files 18 suites 25m 8s ⏱️ Results for commit eb68928. |
…e nor deterministic, and "re-running helps" was never a property of the message Four findings from Copilot's review, all correct, all verified at file:line. 1. 🚨 `<generation>.<16 hex>` is a 64-bit truncation and cannot be called collision-free — a collision would let one replica replace the other's record and put back exactly the lost update the design removes. The FULL SHA-256 now. The generation LEAF can afford 16 hex because a collision there means two payloads sharing a directory, which the bytes' own verification catches; a record file has no second check. The shorter-name alternative is stated as what it is — exclusive-create plus a byte-equality check on collision, a rule rather than a shorter hash. 2. 🚨 THE BIGGER ONE: content-addressing the record is worth nothing if the record is not a function of the landing, and `ModuleActivationEntry` is not. The four `Previous*` fields are populated from `landedBefore` and `PreviousToKeep` (ModuleLandingService.cs:724, :872) — from whichever head THAT REPLICA observed — so two replicas landing identical bytes at one version would write different records, get different names, and accumulate files. Idempotence lost exactly where it is needed. The fix is the design pointing at itself: `Previous*` IS the fallback decision, and the point of deriving is that head and fallback are computed at read time rather than stored. A per-landing record therefore carries the landing's own facts only — Name, Source, PackagePath, Directory, Version, FrameworkMvid, MinMeshVersion, SourceCommit — every one a function of the bytes and the request. `Enabled` stays in its own marker; `Previous*` is not stored at all. 3. 🚨 "Re-running helps" was never a property of the message — it was a property of the WINDOW, and #4345 removed the window. A sentinel probe answers `false` identically for a real absence and for a refresh in flight, so the first row's `yes` would have told an operator to re-run a job that cannot pass. The table now says NO on every row and leaves the diagnosis to the identity, with the share arm marked "since the fix: a REAL absence" because the probe follows `_current` and the window can no longer produce it. 4. The registry probe retries `408` and `429` as well as any `5xx` and `000` (node-repo-gate.yml registry_get) — "only 5xx and 000" was inaccurate. Verified: the built MeshWeaver.Documentation.dll carries both new strings (a positive signal the embedded resources refreshed), and link integrity + topic map + conflict markers is 6 passed / 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A live false red on every publishing repo's upstream gate — found while checking what phase 4 (#4341) touched
🚨 This is not phase-5 preparation.
check-release-availability.shis the gatemain-cd,release.ymland every node repo'spublish-bakerun to ask "is this upstream published for the identity I am about to build against?". It probedprebuilt-bundles/<identity>/<source>/_complete— the flat compatibility copy's sentinel — and under the generation layout the two answers come apart on every publish:publish_publication, atgeneration_currentpublish_one_targetbegins that by deleting its_complete(the unseal that stops a reader seeding a mid-replace mix)So for ~90 s per target per publish the gate answered
falseand reportedno sealed publication under …. That wording is not neutral — it is the message that means an upstream has not published yet, and the one that held MeshWeaver.Reinsurance 23 times in 24 hours (#3583). A false instance costs a reader exactly the same investigation as a true one.Live since #4269 for
plugins(merged 05:33Z today) and, since #4341 (c86aea88f8, merged an hour ago), forcrm,education,reinsurance,socialmediaandmanufacturingas well.It is a THIRD Azure-direct reader, and the docs said there were two
Doc/Architecture/SealedPublicationGenerationsandSealedPublicationReadsboth enumerated two Azure-direct readers of this prefix —compose-sealed-modules.shandnode-repo-gate.yml'sseed, routed in phase 3 — andtest-publication-pointer-readers.pysaid "over 2 readers" in its own success line. That denominator is how this reader survived phase 3, phase 4 and two reviews. All three are corrected here.The fix
The siblings' resolver, verbatim, with the siblings' fall-back: an absent, blank, unreadable, escaping or dangling pointer means the source directory — this gate's own previous behaviour — so the change can only ever add an answer it used to get wrong. It agrees with
publish-bake-bundles.sh,bake-scope.sh,carry-forward-bundles.sh,compose-sealed-modules.shandShippedPrebuiltBundles.PublicationDirectoryOf, which is the point: a lane that resolves differently reads what portals do not serve.Watched failing — the fixture IS the window
test-publication-pointer-readers.pynow executes three readers (17 cases). The new case stages the generation sealed,_currentnaming it, and the prefix's own sentinel removed:origin/main's scriptexit=1, and the literal textno sealed publication under prebuilt-bundles/s0123…/pluginsThe two siblings pass on both scripts, which is what makes them regression controls rather than new behaviour — and what stops the first case passing merely because the script always exits 0.
Siblings green:
test-release-availability-refusals.py(the three outcomes stay distinguishable — the harness that already guards this script's log quality),shellcheck -S warningclean,MeshWeaver.Documentation.Testlink integrity + topic map + What's New +NodeRepoLaneHostGuard15 / 15.Docs
SealedPublicationGenerationsgains "The third reader, and the false hold it was already producing", and its "TWO Azure-direct readers" bullet is corrected to three with the finding dated.SealedPublicationReads's matching bullet likewise.Category: Fix— a build held on content that had in fact been published is a delay a user feels, and it is the kind of fix that goes missing.Refs #3461.
🤖 Generated with Claude Code
🚨 Added after review: which "no sealed publication" this fixes — and the two it does NOT
Three places print a near-identical sentence with opposite remedies, and conflating them would make a genuine outage look transient. The discriminator is the preposition and what follows it:
no sealed publication **under** <path on the share>check-release-availability.sh— this PR_completeis deleted during its refresh while the generation is sealed, live and pointed atno SEALED publication **at** <registry URL> … (404)node-repo-gate.yml'supstream_not_ready, reached from the registry probe at:1396— a definite 404, refused immediately on purposeno SEALED publication **under** <account>/<share>/<dir>compose-sealed-modules.sh:267A live instance of the second, today: MeshWeaver.Education
mainasking for identitys3b3fb865f…(platform set 8596) while the last Plugins publication iss6b704cd02…(set 8581, 12:53Z) — its seal blocked on Plugins#1872. Nothing in this PR touches that, and reading it as this window would send someone to re-run a job that cannot pass.The same table goes into
Doc/Architecture/SealedPublicationGenerationsin the follow-up, because two conditions wearing one sentence is exactly the class this fleet keeps paying for.