Skip to content

fix(#3461): the upstream availability gate reads the publication, not the prefix - #4345

Merged
meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/3461-release-availability-resolves-the-pointer
Sep 14, 2026
Merged

meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/3461-release-availability-resolves-the-pointer

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

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.sh is 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?". It probed prebuilt-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, at generation state of the prefix
seals the generation the publication is complete
moves _current the publication is LIVE to every pointer-following reader
then refreshes the flat copy — and publish_one_target begins that by deleting its _complete (the unseal that stops a reader seeding a mid-replace mix) the prefix has no sentinel for the whole upload-and-verify interval

So for ~90 s per target per publish the gate answered false and reported no 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), for crm, education, reinsurance, socialmedia and manufacturing as well.

It is a THIRD Azure-direct reader, and the docs said there were two

Doc/Architecture/SealedPublicationGenerations and SealedPublicationReads both enumerated two Azure-direct readers of this prefix — compose-sealed-modules.sh and node-repo-gate.yml's seed, routed in phase 3 — and test-publication-pointer-readers.py said "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.sh and ShippedPrebuiltBundles.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.py now executes three readers (17 cases). The new case stages the generation sealed, _current naming it, and the prefix's own sentinel removed:

against origin/main's script against this branch
the availability gate during the unsealed window FAILS — exit=1, and the literal text no sealed publication under prebuilt-bundles/s0123…/plugins passes
…with nothing sealed anywhere passes (still refuses) passes
…on a flat prefix with no pointer passes (still sealed) passes
total 1 failed 17 / 17

The 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 warning clean, MeshWeaver.Documentation.Test link integrity + topic map + What's New + NodeRepoLaneHostGuard 15 / 15.

Docs

  • SealedPublicationGenerations gains "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.
  • A What's New entry, 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:

the message where it comes from what it means does re-running help?
no sealed publication **under** <path on the share> check-release-availability.sh — this PR may be the ~90 s window in which the flat copy's _complete is deleted during its refresh while the generation is sealed, live and pointed at yes — and after this PR the window is gone
no SEALED publication **at** <registry URL> … (404) node-repo-gate.yml's upstream_not_ready, reached from the registry probe at :1396 — a definite 404, refused immediately on purpose the upstream genuinely has not published for that framework identity no. Look at whether the upstream's seal is blocked
no SEALED publication **under** <account>/<share>/<dir> compose-sealed-modules.sh:267 the resolved publication carries no sentinel — pointer-resolved since phase 3 depends; not this window

A live instance of the second, today: MeshWeaver.Education main asking for identity s3b3fb865f… (platform set 8596) while the last Plugins publication is s6b704cd02… (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/SealedPublicationGenerations in the follow-up, because two conditions wearing one sentence is exactly the class this fleet keeps paying for.

… 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>
Copilot AI lite review requested due to automatic review settings September 14, 2026 18:56
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

262 tests   262 ✅  2m 9s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit eb68928.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_availability fallback 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 both are 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 0 with sealed: and the is not on the share warning), 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.

Comment on lines +622 to +626
- 🚨 **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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 473 tests   1 473 ✅  2m 58s ⏱️
    2 suites      0 💤
    2 files        0 ❌

Results for commit eb68928.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

915 tests   915 ✅  3m 29s ⏱️
  2 suites    0 💤
  2 files      0 ❌

Results for commit eb68928.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 863 tests   1 671 ✅  4m 42s ⏱️
    4 suites    192 💤
    4 files        0 ❌

Results for commit eb68928.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files      6 suites   5m 24s ⏱️
2 183 tests 2 183 ✅ 0 💤 0 ❌
2 187 runs  2 187 ✅ 0 💤 0 ❌

Results for commit eb68928.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 103 tests   1 101 ✅  6m 24s ⏱️
    3 suites      2 💤
    3 files        0 ❌

Results for commit eb68928.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   18 files     18 suites   25m 8s ⏱️
7 799 tests 7 605 ✅ 194 💤 0 ❌
7 803 runs  7 609 ✅ 194 💤 0 ❌

Results for commit eb68928.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 912596e Sep 14, 2026
37 checks passed
rbuergi added a commit that referenced this pull request Sep 14, 2026
…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>
@rbuergi
rbuergi deleted the fix/3461-release-availability-resolves-the-pointer branch October 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants