Skip to content

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 into
mainfrom
fix/3732-downstream-seals-only-own-modules
Sep 13, 2026
Merged

meshweaver-cloud[bot] merged 3 commits into
mainfrom
fix/3732-downstream-seals-only-own-modules

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

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, step ext-modules: own bundles (this run's module-artifacts) stay in ext-bundles; compose-sealed-modules.sh / the registry fetch now write to ext-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 under seal-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 exports SEAL_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 says SEAL_MODULES_OWN=0 — and seals an empty modules/_index (which a reader already tells apart from "predates module sealing"). A workflow that exports nothing is still refused.
  • Harnesses: test-module-set-consistency.py +1 case (both directions + falsification arm: with the own-only branch replaced by if 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.py run_step learns an upstream list.
  • Docs: What's New 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), both composed AI / Essentials / Maps / Stripe from the sealed publication of 'plugins', no own module; and core's plugins-bake shape (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 scratchpad p3732/replay/replay.py.

== crm-before      sealing MeshWeaver.AI as module package 'AI' … ×4
                   module set: 4 MeshWeaver.* assembly file(s) across 4 bundle(s), 4 distinct name(s), 0 …, 0 carried at more than one BUILD
                   seal-modules/ = ['ai.module.nupkg', 'essentials.module.nupkg', 'maps.module.nupkg', 'stripe.module.nupkg']
== crm-after       composed only: MeshWeaver.AI (package 'AI') is an UPSTREAM's module — in this bake's compile surface, NOT in this publication's seal … ×4
                   module set: 4 MeshWeaver.* assembly file(s) across 4 bundle(s), 4 distinct name(s), 0 …, 0 carried at more than one BUILD
                   sealed set: 0 own module bundle(s) go into this publication's modules/_index; 4 upstream cop(y/ies) composed for the compile surface only and NOT sealed here (MeshWeaver#3732)
                   seal-modules/ = (empty — no directory)
== education-before / education-after   identical to Crm (same four packages)
== core-plugins-bake-before   sealing … ×4 · seal-modules/ = [ai, essentials, maps, stripe]
== core-plugins-bake-after    sealing … ×4 · sealed set: 4 own …; 0 upstream … · seal-modules/ = [ai, essentials, maps, stripe]   ← unchanged

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, 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 — "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 checkout compose-sealed-modules.sh comes from — and a publisher without SEAL_MODULES_OWN keeps the OLD behaviour, loudly:

::warning::the publisher at this lane's scripts ref predates MeshWeaver#3732 (no SEAL_MODULES_OWN):
it would refuse a publication that stages no module of its own. Sealing upstream copies as before —
this publication will carry its upstream's modules, which is the defect #3732 removes. It stops as
soon as the resolved platform set carries the newer publisher; nothing to do here.

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

Verification

python3 .github/scripts/test-module-set-consistency.py    → PASS — 2 lane(s), 8 case(s) each (incl. both publisher vintages)
python3 .github/scripts/test-module-asset-landing.py      → PASS — 2 lane(s), 4 case(s) each
python3 .github/scripts/test-publish-bake-overlap.py      → 84 passed, 0 failed (with azure-storage-file-share 12.26.0 / azure-identity 1.25.3)
check-pr-secret-preflight / check-workflow-timeouts / check-workflow-yaml-keys / check-workflow-shell → clean
dotnet build test/MeshWeaver.Documentation.Test -c Release -warnaserror → 0 warnings; dotnet test --no-build → 447/447

Live proof after merge: the first satellite publish-bake that gets past #4142's preflight prints sealed set: 0 own …; 4 upstream … and its modules/_index on the share is empty; core's plugins-bake keeps printing sealed set: 4 own ….

🤖 Generated with Claude Code

https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E

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

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

262 tests  ±0   262 ✅ ±0   2m 20s ⏱️ +13s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 85ad180. ± Comparison against base commit dbdaaac.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

508 tests  ±0   508 ✅ ±0   38s ⏱️ +3s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 85ad180. ± Comparison against base commit dbdaaac.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

390 tests  ±0   390 ✅ ±0   56s ⏱️ +16s
  2 suites ±0     0 💤 ±0 
  2 files   ±0     0 ❌ ±0 

Results for commit 85ad180. ± Comparison against base commit dbdaaac.

♻️ This comment has been updated with latest results.

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

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-empty modules/_index satisfies module_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_step always sets REGISTRY_MODULES empty and injects upstream fixtures directly. Add a no-upstream case with REGISTRY_MODULES and a stubbed registry response, then assert that the downloaded .bundle.zip reaches the compile surface but is absent from seal-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

  • $UPSTREAM also contains registry-fetched bundles when SEAL_UPSTREAMS is 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 the UPSTREAM_COMPOSED summary 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"

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.

Fixed in f736830.

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).

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.

Fixed in f736830.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 338 tests  +2   1 336 ✅ +2   3m 51s ⏱️ -1s
    3 suites ±0       2 💤 ±0 
    3 files   ±0       0 ❌ ±0 

Results for commit 85ad180. ± Comparison against base commit dbdaaac.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 036 tests  ±0   2 036 ✅ ±0   5m 4s ⏱️ -7s
    4 suites ±0       0 💤 ±0 
    4 files   ±0       0 ❌ ±0 

Results for commit 85ad180. ± Comparison against base commit dbdaaac.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files  ± 0      6 suites  ±0   7m 54s ⏱️ -42s
2 912 tests +12  2 720 ✅ +12  192 💤 ±0  0 ❌ ±0 
2 916 runs  +12  2 724 ✅ +12  192 💤 ±0  0 ❌ ±0 

Results for commit 85ad180. ± Comparison against base commit dbdaaac.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   17 files  ± 0     17 suites  ±0   20m 44s ⏱️ -19s
7 446 tests +14  7 252 ✅ +14  194 💤 ±0  0 ❌ ±0 
7 450 runs  +14  7 256 ✅ +14  194 💤 ±0  0 ❌ ±0 

Results for commit 85ad180. ± Comparison against base commit dbdaaac.

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits September 13, 2026 11:55
…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>
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