Repository navigation
fix(#3732): a downstream publication seals only its own modules — upstream copies are composed, never re-sealed - #4172
Merged
meshweaver-cloud[bot] merged 3 commits intoSep 13, 2026
Conversation
…tream copies are composed, never re-sealed The `ext-modules` step of node-repo-publish-bake.yml kept this repository's own module bundles and the ones composed from an upstream's sealed publication in ONE directory and sealed all of them into this publication's modules/_index. A satellite therefore re-published its upstream's modules (Crm and Education each sealed AI, Essentials, Maps, Stripe — plugins' four — as their own module set), and one identity held five copies of each; when the upstream resealed with a newer build the copies diverged and every reader of the identity saw two builds of one assembly (#3732, defect 1). Now: own bundles in `ext-bundles`, upstream-composed ones in `ext-bundles-upstream`; both are composed into the compile surface and both take part in the one-build-per-name verdict (its denominator is unchanged: the set the bake loads); ONLY own bundles are staged for the seal, and the step prints the sealed-set denominator beside the composed one. The gate lane keeps the same directory split so the shared harness covers both. publish-bake-bundles.sh accepts an empty staged set only when the workflow exports SEAL_MODULES_OWN=0 (every composed module was an upstream's); a workflow that exports nothing is still the pre-#2707 skew and is still refused. Harness: test-module-set-consistency.py gains the both-direction case (1 own sealed, 4 upstream composed+guarded, not sealed; with the own-only branch removed all 5 are sealed) — 8 cases × 2 lanes pass; test-publish-bake-overlap.py gains the own-empty seal and the skew refusal — 84/84 with the pinned SDK; test-module-asset-landing.py 4 × 2 pass. Replayed on Crm's / Education's 2026-09-12 composition and core's plugins-bake shape: before = 4 upstream modules sealed, after = 0 sealed / 4 composed only; core unchanged at 4 own sealed. Not addressed: pending_module_activation / content-types on a portal replica (portal-side). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E
Contributor
Contributor
Contributor
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical compatibility and moderate correctness findings remain before approval.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR ensures downstream publications seal only repository-owned modules while still composing upstream or registry modules for compilation and validation.
Changes:
- Separates owned and upstream bundles in publish-bake and gate workflows.
- Adds explicit empty-owned-set handling and expanded test coverage.
- Updates architecture documentation and release notes.
File summaries
| File | Change | Final review notes |
|---|---|---|
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-13-a-downstream-publication-seals-only-its-own-modules.md |
Adds release documentation. | No final comments. |
src/MeshWeaver.Documentation/Data/Architecture/BundleDeliveryStages.md |
Documents revised sealing behavior. | Nit (2 votes): update contradictory canonical text and stale status entry. |
.github/workflows/node-repo-publish-bake.yml |
Composes both origins and seals only owned modules. | Critical (1 vote), moderate (1 vote), and two nits (1 vote each) concerning publisher compatibility, registry-path coverage, and wording/provenance. |
.github/workflows/node-repo-gate.yml |
Mirrors the bundle split for gate validation. | No final comments. |
.github/scripts/test-publish-bake-overlap.py |
Tests empty-index and compatibility behavior. | No final comments. |
.github/scripts/test-module-set-consistency.py |
Tests composition and sealing boundaries. | No final comments. |
.github/scripts/test-module-asset-landing.py |
Supports upstream bundle fixtures. | No final comments. |
.github/scripts/publish-bake-bundles.sh |
Handles explicit empty owned-module sets. | Moderate (1 vote): previously sealed downstream publications may skip republishing and retain old bundles. |
Review details
Suppressed comments (4)
.github/scripts/publish-bake-bundles.sh:245
- This new empty-own branch does not repair an already-sealed downstream publication: the normal sealed-skip later returns when
published_sha == SOURCE_SHA, and an old publication with the pre-fix non-emptymodules/_indexsatisfiesmodule_set=true. Thus the first post-merge bake can skip without replacing the upstream copies, leaving the defect in place until the content SHA changes. Compare the existing module set with the current own-only set (or otherwise version this contract) and force a republish when they differ.
if [ "${SEAL_MODULES_OWN:-}" = "0" ]; then
echo "module set: composed ${EXT_MODULES_UPSTREAM:-?} upstream module cop(y/ies) for the compile surface ($(ls "$EXT_MODULES_DIR" 2>/dev/null | tr '\n' ' ')) and owns none — sealing an EMPTY modules/_index by design (MeshWeaver#3732); the upstream's own publication carries those modules."
.github/workflows/node-repo-publish-bake.yml:1407
- This new upstream-only branch makes the comment immediately above at lines 1387-1389 false: not every composed byte is sealed by this publication anymore. Please update that comment to distinguish own bundles from compile-surface-only upstream bundles, so the workflow does not contradict its new behavior.
else
UPSTREAM_COMPOSED=$((UPSTREAM_COMPOSED + 1))
echo "composed only: $name (package '$pkg') is an UPSTREAM's module — in this bake's compile surface, NOT in this publication's seal; its own source's publication carries it (MeshWeaver#3732)"
.github/workflows/node-repo-publish-bake.yml:1285
- The new registry fallback writes into
$UPSTREAM, but the workflow harnesses never execute this branch:run_stepalways setsREGISTRY_MODULESempty and injects upstream fixtures directly. Add a no-upstream case withREGISTRY_MODULESand a stubbed registry response, then assert that the downloaded.bundle.zipreaches the compile surface but is absent fromseal-modules; otherwise the explicitly supported registry-origin half of this change can regress independently.
curl -fsS -H "Authorization: Bearer $REGISTRY_KEY" -o "$UPSTREAM/$pkg.bundle.zip" "$REGISTRY_URL/api/plugins/bundles/$pkg/$ver" \
.github/workflows/node-repo-publish-bake.yml:1407
$UPSTREAMalso contains registry-fetched bundles whenSEAL_UPSTREAMSis empty, but this new message labels every non-own bundle as an "UPSTREAM's module" and says its source publication carries it. That is false for the no-upstream registry path, and theUPSTREAM_COMPOSEDsummary has the same ambiguity. Preserve the registry-vs-sealed-upstream provenance (or use an external-origin label) so operators are not sent to a nonexistent upstream publication.
echo "composed only: $name (package '$pkg') is an UPSTREAM's module — in this bake's compile surface, NOT in this publication's seal; its own source's publication carries it (MeshWeaver#3732)"
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| # Read by publish-bake-bundles.sh: a bake that composed modules but STAGED none is a | ||
| # version-skew refusal there (workflow predates module sealing) — unless this step says | ||
| # the staged set is empty BECAUSE every composed module was an upstream's (own = 0). | ||
| echo "SEAL_MODULES_OWN=$SEALED_OWN" >> "$GITHUB_ENV" |
Comment on lines
+145
to
+150
| > **2026-09-13 — defect 1 fixed at the lane.** The `ext-modules` step of `node-repo-publish-bake.yml` | ||
| > keeps this repository's own module bundles and the ones composed from an upstream's seal in two | ||
| > directories; both are composed into the compile surface and both take part in the one-build | ||
| > verdict, but only the own ones are staged for `modules/_index`. A satellite that owns no module | ||
| > seals an EMPTY index, which `publish-bake-bundles.sh` accepts only when the workflow exports | ||
| > `SEAL_MODULES_OWN=0` (a workflow that exports nothing is still the pre-#2707 skew, refused). |
Contributor
Contributor
Contributor
Contributor
…ling text follows the contract Copilot review: - the workflow moves at `uses: …@main` while `publish-bake-bundles.sh` is checked out at `scripts-ref || platform-ref` — which a satellite resolves to the newest SEALED set and therefore lags core main by up to an hour. A new workflow staging own-only against an OLD publisher would hit its "composed modules but staged none" refusal, whose message names the wrong cause, and every satellite owns no module at all — so that window would red the whole fleet. The step now PROBES the publisher it will run (the same sparse checkout the compose script comes from): no `SEAL_MODULES_OWN` in it ⇒ keep the old behaviour, seal everything, and say why, as a warning. The harness executes both vintages: with a pre-#3732 publisher on disk all five bundles are sealed and the log explains it; with the current one exactly the own one is. - `CiContentBake` said the publisher seals "the composed bundles" and `BundleDeliveryStages`' status table still called defect 1 structural/open. Both now state the contract: a publication seals only what its repository owns, one copy of each module per identity, both denominators printed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g the skew, REFUSE past it The publisher-vintage fallback tolerates #3732's defect 1 (a downstream sealing its upstream's modules) for the window between this workflow landing at `@main` and the first SEALED set whose core commit carries it — normally one core CD cut, ~20 minutes. An undated tolerance is how a known defect rides for a month behind a warning nobody re-reads (the `"signature":"not-required"` inbox), so the branch now carries LEGACY_PUBLISHER_RED_FROM = 2026-09-15T00:00:00Z: - before it: a warning that says what it is tolerating, that THIS publication therefore carries its upstream's modules, and what ends it (the next sealed set); - after it: a REFUSAL naming the real defect — a publisher that old means the set this run resolved is not advancing (#4063 / #4142 are the two shapes), and re-sealing upstream modules must never be the way past it. The harness executes that direction too, by moving the constant into the past rather than waiting for the clock: the same pre-#3732 publisher that is tolerated today is refused past the end, and the case fails loudly if the constant is ever removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rbuergi
marked this pull request as ready for review
September 13, 2026 10:14
This was referenced Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes defect 1 of #3732 at the lane that produces it. A downstream publication now seals only the modules its repository owns; modules composed from an upstream's sealed publication (or from the registry, for a repository that declares no upstream) are still composed into the bake's compile surface and still checked for one-build-per-name — they are never re-sealed into this publication's
modules/_index.Pairs-with: none — CI lane + scripts + docs; no public type or member leaves
src/.🚨 Live on merge for all six callers (every satellite calls
node-repo-publish-bake.yml@main), which is why the negative control below is run on the real repositories' composed sets rather than on a synthetic.What changes
node-repo-publish-bake.yml, stepext-modules: own bundles (this run'smodule-artifacts) stay inext-bundles;compose-sealed-modules.sh/ the registry fetch now write toext-bundles-upstream. The loop composes BOTH into/ext/<module>and digests BOTH for the verdict (module set: … carried at more than one BUILD, denominator unchanged = what the bake loads). Only own bundles are staged underseal-modules/. A new line prints the other denominator:sealed set: N own module bundle(s) …; M upstream cop(y/ies) composed for the compile surface only and NOT sealed here. The step exportsSEAL_MODULES_OWN/EXT_MODULES_UPSTREAM.node-repo-gate.yml, same step: the same directory split (it seals nothing), so the shared harness executes one shape in both lanes.publish-bake-bundles.sh: the "composed modules but staged none" refusal (pre-ci(gates): a gate composes the modules its upstream was sealed against — the seal carries them, the registry serves them, no fallback (#2698) #2707 workflow skew) now has ONE legitimate exception — the workflow saysSEAL_MODULES_OWN=0— and seals an emptymodules/_index(which a reader already tells apart from "predates module sealing"). A workflow that exports nothing is still refused.test-module-set-consistency.py+1 case (both directions + falsification arm: with the own-only branch replaced byif true, all five bundles land in the seal);test-publish-bake-overlap.py+2 (own-empty seals; the same bake without the export is refused);test-module-asset-landing.pyrun_steplearns anupstreamlist.2026-09-13-a-downstream-publication-seals-only-its-own-modules;BundleDeliveryStages→ stage ② dated note and its 2026-09-13 status row, which called defect 1 structural/open;CiContentBake→ The publication is the unit of consistency, whose canonical sentence said the publisher seals "the composed bundles".Negative control — the real repositories' composed sets, before and after
The step body was extracted by id from
origin/main(before) and from this branch (after) with the harness's own extractor and executed on the module sets the last successful publish-bake logs record — Crm job 103598896751 and Education job 103588285511 (2026-09-12), bothcomposed AI / Essentials / Maps / Stripe from the sealed publication of 'plugins', no own module; and core'splugins-bakeshape (four own artifacts, no upstream). Package ids, module names and file names as logged; bytes synthetic (the fetch needs the registry key). Replay script: session scratchpadp3732/replay/replay.py.The guard's denominator is the composed set — 4 on Crm/Education before and after — because that is the population the bake loads and stamps records against; the seal is the population that changed (4 → 0 on the satellites, 4 → 4 on core). Pointing the guard at a publication's ~40 module bundles is a change to
node-repo-module-pack.yml, which is owned by another session this hour and is not in this PR.Landing order — the publisher moves at its own ref (Copilot review)
This workflow is resolved at
uses: …@main, whilepublish-bake-bundles.shis checked out atscripts-ref || platform-ref— which a satellite resolves to the newest sealed set and therefore lags coremainby up to an hour. A new workflow staging own-only against an OLD publisher would hit its "composed modules but staged none" refusal (whose message names the wrong cause — "bump the caller's pin"), and since the satellites own no module at all, that window would red every satellite publish-bake.So the step probes the publisher it will run —
mw-platform-gate/.github/scripts/publish-bake-bundles.sh, the same sparse checkoutcompose-sealed-modules.shcomes from — and a publisher withoutSEAL_MODULES_OWNkeeps the OLD behaviour, loudly:Both vintages are executed by the harness: with a pre-#3732 publisher on disk all five bundles are sealed and the log says why; with the current one exactly the own one is.
What this does NOT address
pending_module_activation(absent on 5/5 replica payloads on 2026-09-13) andcontent-typesnaming module-declared types on some replicas — is a portal-side reading (a replica that has not loaded what it holds) and is unchanged by this PR.Verification
Live proof after merge: the first satellite publish-bake that gets past #4142's preflight prints
sealed set: 0 own …; 4 upstream …and itsmodules/_indexon the share is empty; core'splugins-bakekeeps printingsealed set: 4 own ….🤖 Generated with Claude Code
https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E