Repository navigation
fix(#3461): phase 4 — core publishes the plugins prefix as generations - #4269
Conversation
`main-cd.yml`'s `plugins-bake` passes `publication-layout: generation`, so core's publications of `prebuilt-bundles/<identity>/plugins` are written into their own run-named directory, verified there, sealed, and only then pointed at by `_current`. The prefix is no longer mutated in place for any reader that follows the pointer — which is every portal, both Azure-direct readers, and the satellites' gate seed. The precondition that makes a one-producer-at-a-time flip safe is that every producer runs a publisher carrying #4249 (the layout belongs to the prefix, so a run resolving a live `_current` publishes a generation whatever its caller passed). Measured 2026-09-14 across all six node repos — Plugins, Crm, Education, Reinsurance, SocialMedia, Manufacturing — every `node-repo-*.yml` `uses:` is `@main` and every `scripts-ref:` is `main`. No pinned publisher and no pinned reader of this prefix exists in the fleet. Refs #3461 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The unresolved pointer-ordering race can serve older plugin bytes, and the related guard and documentation corrections remain outstanding.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR enables generation-based publishing for core Plugins and documents the phase 4 rollout.
Changes:
- Enables
publication-layout: generationin core CD. - Updates architecture rollout documentation.
- Adds a user-facing What's New entry.
File summaries
| File | Review summary |
|---|---|
.github/workflows/main-cd.yml |
Critical (1 vote): Pointer updates can regress to an older generation; add monotonic/CAS protection and a regression test. Moderate (2 votes): Add a persistent guard for the generation-layout wiring. |
src/MeshWeaver.Documentation/Data/Architecture/SealedPublicationGenerations.md |
Nit (1 vote): Resolve contradictory phase-status text. Nit (1 vote): Align related architecture pages or mark them historical. |
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-14-an-update-is-never-read-while-it-is-being-written.md |
Nit (3 votes): Qualify the guarantee for installations still using the flat compatibility copy. |
Review details
Suppressed comments (4)
src/MeshWeaver.Documentation/Data/Architecture/SealedPublicationGenerations.md:559
- This new status bullet conflicts with the Phase 3 status above, which still says its pin half and phases 4–5 are open. After adding the landed Phase 4 entry, update that earlier status block (or scope it explicitly to the satellite) so the architecture page does not report Phase 4 as both landed and open.
- **Phase 4's core half is landed** (2026-09-14) — `main-cd.yml`'s `plugins-bake` passes
`publication-layout: generation`, so `prebuilt-bundles/<identity>/plugins` is on the generation
layout and every reader that follows `_current` stops reading a directory mutated in place. The
satellite's own flip is explicitness, not correctness (phase 4 above). **Phase 5 is open.**
src/MeshWeaver.Documentation/Data/Architecture/SealedPublicationGenerations.md:559
- This status change leaves other shipped architecture pages contradictory:
SealedPublicationReads.md:576-588still sayspluginsmust flip in ONE change set, andBundleDeliveryStages.md:213says phase 4 is unreachable and unlanded. Update those status passages in the same change, or explicitly mark them as historical snapshots, so readers do not get two incompatible migration states.
- **Phase 4's core half is landed** (2026-09-14) — `main-cd.yml`'s `plugins-bake` passes
`publication-layout: generation`, so `prebuilt-bundles/<identity>/plugins` is on the generation
layout and every reader that follows `_current` stops reading a directory mutated in place. The
satellite's own flip is explicitness, not correctness (phase 4 above). **Phase 5 is open.**
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-14-an-update-is-never-read-while-it-is-being-written.md:33
- The pointer is moved after the generation is sealed but before the flat compatibility copy is refreshed (
publish_publicationcallsmove_pointerbeforepublish_one_targetfor the flat directory), so it is not the publication's “very last step.” This ordering is load-bearing; please describe it accurately.
**Each publication is now written into its own folder**, checked there, and only then pointed at.
A one-line pointer says which folder currently applies, and it is moved as the very last step —
after everything in the new folder is verified complete.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-14-an-update-is-never-read-while-it-is-being-written.md:43
- This says every reader falls back to the flat copy, but
node-repo-gate.yml's recursiveseedreader explicitly refuses when it cannot resolve a usable pointer because falling back would flatten multiple generations into one seed. Scope this claim to pointer-aware portal readers or mention that gate exception.
**Nothing has to be upgraded to benefit.** A copy in the old flat layout is still published beside
the new one, so an installation that predates this change reads exactly what it read before. If the
pointer is ever missing or unreadable, every reader falls back to that copy — the worst case is the
behaviour of the day before yesterday, not a failure.
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # publishes; on a reconcile run that can be older, and a publisher predating #4249 would then | ||
| # refresh the flat copy without moving the pointer — which leaves pointer-following readers on | ||
| # the NEWER generation, the direction that is safe. Doc/Architecture/SealedPublicationGenerations. | ||
| publication-layout: generation |
| # publishes; on a reconcile run that can be older, and a publisher predating #4249 would then | ||
| # refresh the flat copy without moving the pointer — which leaves pointer-following readers on | ||
| # the NEWER generation, the direction that is safe. Doc/Architecture/SealedPublicationGenerations. | ||
| publication-layout: generation |
| --- | ||
| Name: An update is never read while it is being written | ||
| Category: Fix | ||
| Description: Installations read the platform's prepared plugin content out of a folder that was being overwritten in place while they read it. Each update is now written into its own folder and only pointed at once it is complete, so a read can no longer catch an update half-finished. |
Two process facts from this PR, measured — one of them is a correction of my own first readingRecorded here because both are about the lane rather than the change, and the first one is the sort of thing the next person reaches for under time pressure. 1. 🚨 Drafting a PR does NOT hold it once the merge queue has admitted itCopilot's review landed a real finding (the pointer-ordering race — fixed in #4273) after this PR's required checks were green. I converted it to draft to hold it, and it merged three minutes later anyway. My first reading was "draft conversion did not stop the bot". That is wrong, and the timeline says so: The conversion worked as far as it goes — this PR is still
This had already cost core #4147 the same way earlier the same night. It is going onto the fleet CI page. 2. "Green + armed auto-merge" raced the review, on a live delivery pathThe finding was correct and material: under the So a change to the fleet's most-read publication prefix was merged with a valid, unaddressed review finding against it, purely because the required checks finished before the reviewer did. Nothing currently expresses "a delivery-path change does not arm until its review has landed" — Not proposing the policy here; recording that the gap is real and that it was exercised, not hypothesised. #4273 carries the fix, its harness case, the guard that holds the flip in place, and the doc section. 🤖 Generated with Claude Code |
Refs #3461— phase 4's core half. #4249 removed the reason the flip had to be atomic across two repositories; this takes it.What changes
main-cd.yml'splugins-bakepassespublication-layout: generation. From this merge, core's publications ofprebuilt-bundles/<identity>/pluginsare written into their own run-named directory, verified there, sealed there, and only then pointed at by_current— written last. The prefix is never mutated in place again for a reader that follows the pointer.The precondition, measured rather than assumed
🚨 The prefix-ownership mechanism (#4249) lives in the PUBLISHER, so "either order" is a claim about which publisher each producer is RUNNING. A producer resolving a
publish-bake-bundles.shthat predates #4249 would refresh the flat copy in place and never move the pointer — the stale serve the mechanism claims to have made unreachable. That is the one thing that had to be true before flipping, and it is checkable.Measured 2026-09-14, all six node repos (
ci.ymlon eachmain, read over REST):uses:scripts-ref:…/node-repo-{validate,gate,publish-bake}.yml@mainmain(×5)…@main(validate, compile-check, gate, publish-bake)main(×5)…@mainmain(×4)…@mainmain(×5)…@mainmain(×5)…@mainmain(×5)No pinned publisher and no pinned reader of this prefix exists in the fleet. MeshWeaver.Plugins'
publish-bakein particular passesscripts-ref: main, so the other producer ofpluginsruns coremain's publisher on every publish — it starts writing generations on its very next run, with no change in its repository, announced by the::warning::#4249 added. Its own flip is then explicitness, not correctness.The one exception is this lane itself:
plugins-bakepasses noscripts-ref, so it resolves its scripts atplatform-ref=gate.outputs.sha, the commit the run publishes. On a reconcile run that can be older than #4249; such a run refreshes the flat copy without moving the pointer, which leaves pointer-following readers on the newer generation — the safe direction, and the same flat-copy residual phase 5 removes.Why the blast radius is bounded
ResolvePublicationPointerdegrades an absent, blank, unreadable, escaping or dangling pointer to the source directory, where the flat compatibility copy still sits, still sealed. Worst case is the pre-flip serve.node-repo-gate.yml'sseed—download-batchrecurses and flattens, so a union is the one thing it must not build. It refuses instead, transiently; a re-run reads the publication that now applies. That workflow is@mainfor all six repos (table above).PrebuiltBundleRetention.MinimumAgeis 30 days and the config key refuses a shorter value, so the first generations written by this change are not collectable until 2026-10-14 at the earliest, under rules whose protected set is {what_currentnames} ∪ {everything young}.BAKE_PUBLICATION_DIR(the CD publishes every image and every sealed bundle publication to cr.meshweaver.cloud (#3353) #3760 fleet-registry push) is a local runner directory materialised from the run's own verified upload plan — layout-independent, unaffected.Negative control — the harness fails when the SDK it pins is absent
Both runs are against a
.github/scripts/tree byte-identical toorigin/main(git diff --stat origin/main -- .github/scripts/is empty) — this PR changes no script. So the 90 green assertions are a measurement of the generation path this flip enters, on the exact publisher that will run it, and the first run shows the harness is not structurally unable to fail.Verification (each step able to fail)
test-publish-bake-overlap.py(pinned SDK)test-publication-pointer-readers.pypyyaml; it refuses rather than skips without it)dotnet build test/Memex.Portal.Shared.Test -c Release -warnaserrordotnet test --filter PublicationGeneration --no-buildyaml.safe_load(main-cd.yml)→jobs['plugins-bake'].with['publication-layout']'generation'check-workflow-timeouts.pycheck-workflow-yaml-keys.pycheck-workflow-shell.pyWhat this does NOT do
Docs:
Doc/Architecture/SealedPublicationGenerations→ phase 4 records what landed, the measured fleet-wide precondition, the reconcile-run residual and the fail-safe reader argument; "Where this stands" is corrected (it still said no caller anywhere passespublication-layout). What's New:2026-09-14-an-update-is-never-read-while-it-is-being-written(Category: Fix).Pairs-with: none— CI lane input + docs; no public type or member leavessrc/. No interface gains a member. No i18n catalog key changes.🤖 Generated with Claude Code