Repository navigation
cd: satellite-compat — every core build compile-checks every satellite's NodeTypes against the new set (phase 2a) - #4088
Conversation
…e's NodeTypes against the new set (phase 2a) The fleet rebuilds its satellites once a day (maintainer, 2026-09-12), which removed the only per-build evidence that a core merge did not break them. This is the counterweight: after `promote`, one reusable compile-check leg per satellite (SocialMedia, Crm, Reinsurance, Education, Manufacturing), in parallel, against the tester THIS run promoted and the module bundles THIS run packed. RED but not blocking: nothing waits for it, a red leg fails the job, `alert-on-failure` names the satellite/set/types on the ci-failure issue, `delivery-verdict` renders a per-satellite table and goes red only if the gate was SKIPPED on a publishing run. * main-cd.yml: `preflight` asserts FLEET_READER_APP_ID/_PRIVATE_KEY red; `satellite-compat-image` resolves the tester's GHCR mirror and asserts its digest equals ACR's (core holds no ACR password; OIDC inside the lane would be the #3933 startup-failure shape); `satellite-compat` matrix `uses:` node-repo-compile-check.yml; verdict + alert extended. * node-repo-compile-check.yml: optional `content-repository`/`content-ref` + `content-app-*` (publish-bake's shape), `verdict-artifact`, and `packages: read` (inside GitHub's restricted default, so the four `inherit` callers stay valid — pairing --fleet green). * compile-check.py: `--report PATH` writes the verdict as JSON; self-tested. * Guards: PlatformBakeLaneGuard pins the register (exactly five, only in that job); CdImageVersionsShareOneSourceGuard exempts the register POSITIVELY (ref is `main`, never Plugins); PlatformNeverDependsOnPlugins now sees MeshWeaver.Crm and its main-cd ledger names the gate. * ContinuousDeliveryContract.md: one dated paragraph. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arry their original rationale; Education measured, not predicted Review feedback on #4088: * The three guards now QUOTE the hazard their 2026-08-26 / 2026-09-01 originals closed (4105384: the #1814 bake-identity class — bytes from a tree the identity does not name; ce16aa6: a mid-run merge falsifying the pair tag's provenance and sealing bundles from a third tree; c88cd5d: a core PR verdict behind a sibling checkout) and state per hazard why satellite-compat is outside it: read-only content at `content-ref: main`, resolved to a sha the leg records in its verdict, compiled into a throwaway check.csproj against the set already promoted, never built into an image or seal, nothing waits on it, nothing consumes the verdict. What it CAN do — red a CD run on a satellite's state — is the decision. * node-repo-compile-check.yml resolves the content commit once and the report carries {repository, sha}; the verdict table and the ci-failure line show `<satellite> (main @ <sha>)`. Self-tested. * Education measured on the lane's exact script path (main 7c106210b, tester sha256:1ad82ff0…, run 34684158910's four bundles): 13 NodeTypes, 13 clean, 0 debt, 7 s; the harness reds by name on a planted break. It stays in the matrix, with the reading in the comment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the RUN while delivery completed
A red satellite leg fails `satellite-compat` (no continue-on-error, by rule),
so the run's conclusion is `failure` while promote, notify, bake and seal all
completed. The run's colour then says COMPATIBILITY, not shipping — stated
now rather than left implicit:
* delivery-verdict: the Compatibility step (id: compat) opens its summary
with `🚨 DELIVERY COMPLETED — this run is red because N satellite(s)
failed compatibility: <list>. Nothing failed to ship.` and exports
compat_red / compat_red_list; the run's final headline ("Say plainly")
repeats it above the PUBLISHED line when any leg is red.
* alert-on-failure: the same line opens the Compatibility section of the
ci-failure body.
* ContinuousDeliveryContract.md: the same sentence in the dated paragraph.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved workflow reporting, artifact handling, and architecture-documentation issues remain.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds non-blocking per-build compatibility checks for five satellite repositories against the promoted platform image and modules.
Changes:
- Adds satellite matrix checks, digest verification, reporting, and alerts.
- Extends reusable compile-check workflows and JSON verdict generation.
- Updates architecture documentation and validation guards.
File summaries
| File | Summary |
|---|---|
test/MeshWeaver.Documentation.Test/PlatformNeverDependsOnPluginsGuard.cs |
Validates the updated dependency direction. |
test/MeshWeaver.Documentation.Test/PlatformBakeLaneGuard.cs |
Validates the satellite register. |
test/MeshWeaver.Documentation.Test/CdImageVersionsShareOneSourceGuard.cs |
Updates image/reference guard behavior. |
src/MeshWeaver.Documentation/Data/Architecture/ContinuousDeliveryContract.md |
Documents the compatibility policy. |
.github/workflows/node-repo-compile-check.yml |
Adds cross-repository inputs and verdict artifacts. |
.github/workflows/main-cd.yml |
Adds compatibility jobs, reporting, and alerts. |
.github/scripts/compile-check.py |
Generates machine-readable compatibility reports. |
Review details
Suppressed comments (10)
.github/workflows/main-cd.yml:3160
- When
satellite-compatfails beforecompile-check.py --report(for example during checkout, image setup, or artifact download),unmeasuredis nonzero butCOMPAT_RESULTisfailure, so the success-only guard is skipped andredremains zero; thiselsethen printsevery satellite compileseven though the table saysNOT MEASURED. That is a false-green summary for the exact fail-closed case this relay is meant to expose. Only emit the green message when the job succeeded and every satellite has a verdict; otherwise report the measurement as incomplete.
echo "::error::the gate decided to PUBLISH, but satellite-compat=${COMPAT_RESULT} (satellite-compat-image=${COMPAT_IMAGE_RESULT}): the compatibility of this set with the satellites was NOT measured. A skipped gate is painted like a passed one; it is not one. Read satellite-compat-image / plugins-modules for why the legs never ran."
exit 1 ;;
esac
if [ "$COMPAT_RESULT" = "success" ] && [ "$unmeasured" -gt 0 ]; then
echo "::error::satellite-compat reports success but $unmeasured satellite(s) above have no verdict artifact — a leg that measured nothing must not read as a pass. Either the matrix and the SATELLITES register in this step diverged, or a leg uploaded no report."
.github/workflows/main-cd.yml:2577
- The budget counts only the five matrix legs, but this change also adds the separate
satellite-compat-imagerunner job, which runs on every publishing build and overlaps with the legs/modules. Thus the stated “five concurrent jobs per build” and 215-leg daily estimate undercount the new concurrency and runner load; please account for five satellite legs plus one digest-verification job.
# Budget: ubuntu-latest in a public repository — unbilled. Five parallel legs of ~2 min add
# ≤ ~3 min of wall time beside `plugins-bake` (which they do not wait for), and five concurrent
# jobs per build against the org's job ceiling. Narrowing the matrix to the satellites whose
.github/workflows/main-cd.yml:3213
- By adding
satellite-compatto this alert path, a compatibility-only failure now creates/comments theci-failureissue, but the label is created below with the description “no deployable image was published.” The new failure mode explicitly publishes the image set, so the durable issue classification is false and can mislead triage/automation; use a separate compatibility label or update the label contract to distinguish measurement failures.
echo "### 🚨 DELIVERY COMPLETED — this run is red because ${COMPAT_RED} satellite(s) failed compatibility: ${COMPAT_RED_LIST}. Nothing failed to ship." >> "$GITHUB_STEP_SUMMARY"
echo "✅ PUBLISHED an image set for \`${SHORT}\` (reason: ${REASON}) — the run's colour says COMPATIBILITY, not shipping; see the Compatibility table above and the \`ci-failure\` issue." >> "$GITHUB_STEP_SUMMARY"
elif [ "$PUBLISH" = "true" ] && [ "$PROMOTE_RESULT" = "success" ]; then
echo "### ✅ PUBLISHED an image set for \`${SHORT}\` (reason: ${REASON})" >> "$GITHUB_STEP_SUMMARY"
.github/workflows/main-cd.yml:3103
actions/download-artifactfails when apatternmatches no artifacts unless the no-match case is handled. That is exactly what happens whensatellite-compatis skipped before any leg can upload (for example, if its image bridge orplugins-modulesdependency fails), so this step aborts before the explicitNOT MEASUREDcheck below and no compatibility summary is rendered. Make collection of an absent artifact set non-fatal while preserving the following script's explicit skipped/missing-verdict failure path; the existing shard collector uses this pattern for the same action behavior.
# run, and `alert-on-failure` files it.
# `always()`: the table renders even when a verdict step above went red — a run that is
# already red for delivery still owes the reader the compatibility reading it took.
- name: Collect the satellite verdicts
.github/workflows/main-cd.yml:3230
- The alert path has the same no-match failure: when no compatibility leg produced a report,
download-artifactfails here and the job exits beforeCreate or comment on ci-failure issue. That can leave the CD run red for an unmeasured compatibility gate with no failure issue at all. Make this optional evidence collection tolerate an empty artifact set so the generic alert is still filed, while retaining the per-satellite enrichment when reports exist.
# and a failure nobody is paged for is still invisible.
# `preflight` and `notify-platform-update` are in this list ON PURPOSE (#2235): the release
# event that starts the cross-repo rebake wave used to be unfailable, and a failure nobody is
# paged for is still invisible. A red notify job does NOT mean the images are missing —
# `verify-images` answers that — it means no satellite learned about them.
.github/workflows/main-cd.yml:3143
- The report already carries
.unverifiable, andcompile-check.pydoes not setgate_failfor those NodeTypes. With any such entry, thiselsebranch still emits✅ compilesand the final summary says every satellite compiles even though part of its tree was never verified. Surface the unverifiable count/status here and ensure the run cannot present an incomplete measurement as a clean compatibility result.
debt=$(jq -r '.known_debt | length' "$f")
breaks=$(jq -r --arg bt '`' '([.new_breaks[].type] + [.fp_drift[].type] + [.stale_allow[] | . + " (compiles now — remove from allow)"]) | map($bt + . + $bt) | join(", ")' "$f")
if [ "$fail" = "true" ]; then
red=$((red + 1)); redlist="${redlist:+$redlist, }$sat"
.github/workflows/main-cd.yml:3254
- This message says the satellite "does not compile" for every reported red item, but
stale_allowis deliberately a red ratchet result for a type that now compiles cleanly. The generated issue would therefore tell triage the opposite of what happened (the detail appended on the same line contradicts the headline). Use wording such as "has compatibility failures" or classify stale-allow entries separately.
env:
GH_TOKEN: ${{ github.token }}
src/MeshWeaver.Documentation/Data/Architecture/ContinuousDeliveryContract.md:686
- Education is explicitly documented in this change as having no
compile-checkcaller on itsmain, so this sentence is false for one of the five legs. Clarify that the lane uses the reusable gate adopted by the other satellites and applies it directly to Education as an adoption gap; otherwise the contract claims a PR gate that does not exist.
in parallel runs the same reusable compile gate the satellites run on their own pull requests
(`node-repo-compile-check.yml`, pointed at the satellite's `main` through the read-only fleet-reader
src/MeshWeaver.Documentation/Data/Architecture/ContinuousDeliveryContract.md:700
- A red compatibility report is not necessarily evidence that this set breaks a NodeType: the checker also fails on fingerprint drift and stale allow entries (and Education has no allow file, so pre-existing debt can be newly exposed). This statement makes the triage contract inaccurate. Describe the result as a new break or ratchet/measurement failure, then keep the existing guidance about where the fix belongs.
under **Compatibility** — and goes red itself if the gate was *skipped* on a publishing run, because
a skipped measurement painted grey is the one outcome worse than a red one. `fail-fast: false`, no
test/MeshWeaver.Documentation.Test/PlatformNeverDependsOnPluginsGuard.cs:109
- Adding Crm changes the current fleet denominator to five satellites, but the repository-wide architecture inventory still says
MeshWeaver.Pluginsplus four satellites (RepositoryDependencyDirection.md:11) and the current bake contract still describes four satellite publishers (CiContentBake.md:695). The new contract paragraph lists five, so the shipped documentation now disagrees about which repositories the fleet contains. Update the affected current-count statements in the same change.
[
"MeshWeaver.Plugins",
"MeshWeaver.Education",
"MeshWeaver.Reinsurance",
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| + "core/plugins PAIR (#2622), packs+bakes the Plugins module bundles for that identity, " | ||
| + "and — since 2026-09-12, the counterweight to the satellites' once-a-day rebuild — " | ||
| + "points the reusable compile gate at each satellite's main in `satellite-compat`: a " | ||
| + "read-and-compile after `promote` that blocks nothing and builds nothing into an " | ||
| + "image, RED so a break is known within the hour rather than at 03:00 (its register " | ||
| + "is pinned by PlatformBakeLaneGuard)"), |
…ld broke it, never "the satellite was already broken" Maintainer pushback (via the reviewing session): "why fail compatibility?" — a single red compile cannot say which side moved: core changed a surface (red is right) or the satellite merged broken content (core reddened by a repo it should not know about — the c88cd5d hazard, exactly). The control: every leg compiles the same content a second time against the set the fleet is ON, and compat-verdict.py (new, self-tested on every row) classifies per NodeType — compiled at the baseline and not now = BROKE, the only red; failed there too or absent there = already broken, satellite side, green + advisory; no baseline = refusal to judge, green + advisory, new-image failures listed unjudged. The baseline is the one derivation the run already has: `gate` records mw-plugin-test:main's digest + 3.0.0-ci.<run> tag pre-promote (same ACR read as its completeness probe); satellite-compat-image asserts GHCR holds it, locates the building run by run_number and re-uploads that run's four module bundles (the ones that SHIPPED with the baseline) for the legs. Any gap = "no baseline: <reason>", never fatal, never silent. Both digests are in every verdict. Banner: "🚨 DELIVERY COMPLETED — this run is red because this core build BROKE N satellite(s): <list> (previous image compiled, new image does not). Nothing failed to ship." — verdict table, headline, alert (BROKE lines only), contract paragraph, guard comments. PlatformBakeLaneGuard.PlatformBake_NeverAdoptsAnotherBuildsBundles (#1725, cross-run ADOPTION) now exempts exactly the satellite-compat-image block, and a new fact asserts the block still contains the download and that neither it nor the consuming lane carries any publishing form. Verdict step, headline and alert composition executed locally against fixtures for all rows. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ver BROKE — and why the baseline uses the baseline run's bundles * compat-verdict.py: a type that fails on the new image and is absent from the previous report gets its own row, `unjudged` (reason: no per-type baseline — newer than the set the fleet is on), green, advisory; never folded into already-broken. Only previous-compiled-ok → new-fails licenses "broke". Explicit self-test row 5, alone / beside already-broken / beside broke, plus a per-type-granularity case (all-red baseline still yields BROKE for the one type that compiled there). * delivery-verdict table: 🟡 unjudged row; absent types listed distinctly on broke and already-broken rows. * Workflow comment + contract paragraph: WHY the baseline compiles against the BASELINE run's module bundles (located by run number) and not this run's — a Plugins-side move between the two sets would otherwise be filed as "already broken" (or fail the baseline spuriously), destroying the attribution the control exists for. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Phase 2(a) of the fleet CI refactor — the per-build counterweight to the daily rebuild
Decision (maintainer, 2026-09-12): the platform is assumed backwards compatible within a MAJOR; the satellites (MeshWeaver.SocialMedia, .Crm, .Reinsurance, .Education, .Manufacturing — Plugins is built inside CD already) rebuild once a day, not per platform build. That removed the only per-build evidence that a core merge did not break them. This PR is the counterweight: every core build still MEASURES compatibility, cheaply, so a break is known within the hour — not at 03:00 by the daily run, not on a portal at self-update.
Phase 2(b), the instance link probe (#4083), is another session's;
ModulePlatformLink.cs/Mesh.Contractare untouched.Design
A new matrix job in
main-cd.yml,satellite-compat, runs afterpromote: one leg per satellite, in parallel (fail-fast: false), each auses:of the existing reusablenode-repo-compile-check.yml— the very gate the satellites run on their own PRs — pointed at the satellite'smain, at this run's set, and — the control — at the set the fleet is currently on (maintainer pushback via the reviewing session: "why fail compatibility?" — a single red compile cannot tell which side moved).The baseline control. Every leg compiles the same content twice and
compat-verdict.py(new, self-tested on every row) classifies per NodeType:Where the baseline comes from — one derivation, the one the run already has.
gate(pre-promote, already Azure-logged-in for its completeness probe) reads themw-plugin-testmanifest taggedmainfrom ACR (az acr manifest list-metadata) and records its digest and its3.0.0-ci.<run>tag (baseline_tester_digest/_version/_run_number) — the set the fleet is on, read before this run'spromotemoves the pointer.satellite-compat-imagethen (1) asserts GHCR holds that digest, (2) locates the run that built it byrun_number(= the version suffix; verified live:3.0.0-ci.8414↔ run 34688449431run_number=8414), (3) downloads that run's fourmodule-bundle-*artifacts — the modules that shipped with the baseline — and re-uploads them assatellite-compat-baseline-bundlesfor the legs. Any step of that failing → no baseline, with the reason (::warning::, never fatal, never a skip painted grey: the leg still runs and reports the refusal). Both digests are recorded in every verdict JSON (images.new/images.baseline/images.baseline_reason).Why the baseline compiles against the baseline run's bundles and not this run's (stated in the workflow comment and the contract paragraph so a later "we already have this run's bundles, why download the old ones" optimisation is told what it would break): the four modules are Plugins content packed per build. Compiling the previous image with this run's bundles would make a Plugins-side move between the two sets (a type renamed in MeshWeaver.AI, say) fail the baseline reading too and be filed as "already broken" — attributing a Plugins move to the satellite — or fail it spuriously where the new bundle needs new core surface, hiding a core break. The whole control is attribution; that optimisation would remove it.
Every classification is per NodeType (the type's own name in the baseline report's
oklist), never inferred from the satellite's overall colour —compat-verdict.py --self-testholds that with an all-red-baseline case that still yields BROKE for the one type that compiled there.Inputs per leg (all five identical except
content-repository):content-repositorySystemorph/<satellite>(matrix, literal per entry)content-refmaincontent.sha), shown as<satellite> (main @ <sha>)content-app-id/-private-keysecrets.FLEET_READER_APP_ID/_PRIVATE_KEYpermission-contents: read,repositories:= that one satellite (publish-bake's exact shape). Measured 2026-09-12:meshweaver-reader(installation 161095771) is installed org-wide,repository_selection: all— Crm includedtest-image/image-digestghcr.io/systemorph/mw-plugin-test:<version>@ this run's tester digestsatellite-compat-imageresolves the GHCR mirrorpromotephase B wrote and asserts its digest equalsplugins-bake-image.outputs.digest(ACR)acr-username/acr-passwordgithub.actor/GITHUB_TOKENid-token: writeof every satellite caller — the #3933 startup-failure shapemodule-artifactsmodule-bundle-MeshWeaver.{AI,Markdown.Collaboration,Maps,Payments.Stripe}plugins-modulesartifacts — the same pattern and bytesplugins-bakeseals; every satellite declares exactly these four (registry-modules: AI Essentials Stripe Maps,upstream-seed: plugins)platform-refneeds.gate.outputs.shacompile-check.pyandcompat-verdict.pyat this run's core shaverdict-artifactsatellite-compat-<satellite>judge-against-baselinetruefalse: plain red stays red)baseline-image-digest/baseline-unavailable-reasonsatellite-compat-image.outputs.baseline_digest/.baseline_reasonbaseline-module-artifactssatellite-compat-baseline-bundlesEducation — measured, green
Education has no
compile-checkjob and noscripts/compile-check.allowon itsmain, so its leg judges the whole tree with nothing grandfathered — therefore it was measured before being listed, on the lane's exact script path (the lane's steps replayed:docker cp /app+ shared frameworks from the promoted testermw-plugin-test@sha256:1ad82ff0cbac…, the fourmodule-bundle-*artifacts of publishing run 34684158910 unpacked into the reference set,compile-check.py --refs … --reportwithMW_REPO_ROOT/MW_ALLOW_FILEas the lane sets them) against Educationmain@7c106210ba6f0b8118dbe70b3cd101ce19d12020:Positive control on the same harness — a
ThisSymbolDoesNotExistreference appended toAgenticBusiness/Menu/Source/MenuAreas.csin the scratch clone:✗ 1 NON-allowlisted NodeType(s) FAIL to compile (regression): - AgenticBusiness/Menu,gate_fail: true, rc=1; clone restored. Education stays in the matrix, the reading in the workflow comment beside its entry. No Education issue was needed.Reusable lane changes (
node-repo-compile-check.yml, extended, not forked): optionalcontent-repository,content-ref,verdict-artifact,judge-against-baseline,baseline-image-digest,baseline-module-artifacts,baseline-unavailable-reasoninputs; optionalcontent-app-id/content-app-private-keysecrets (asserted RED whencontent-repositoryis set — never a fallback togithub.token); job permissions gainpackages: read— inside GitHub's RESTRICTED default, so the four satellites calling withinheritstay valid (check-workflow-permission-pairing.py --fleet: 44 pairs, 0 violations). Withjudge-against-baselinethe new-image compile's exit code becomes a datum, a second compile runs againstrefs-baseline/(previous image + its bundles), and the judge (compat-verdict.py, fetched atplatform-reflikecompile-check.py, self-tested in-run) sets the leg's colour. A node repo calling with nocontent-repositorybehaves byte-for-byte as before.Scripts.
compile-check.py:--report PATHwrites the verdict as JSON (gate_fail,new_breaks[{type,error}],fp_drift,stale_allow,known_debt, the clean types by name (ok), counts,content.{repository,sha}); self-tested.compat-verdict.py(new): the classifier above;--self-testcovers all rows in both directions — including the explicit fifth row "type absent from baseline, fails on new ⇒ unjudged, not BROKE", alone, beside an already-broken type, and beside a BROKE type — plus per-type granularity, fingerprint-drift, stale-allow, known-debt-on-new and the CLI exit codes (1 = broke, 0 otherwise, 2 = no new report).Semantics — RED, but NOT blocking, and both halves are deliberate
Stated plainly: a red satellite leg ANNOTATES delivery — the Compatibility table on the run summary and a line on the
ci-failureissue — and never stopspromote,notify-platform-update,plugins-bakeorpublish-bake. Nothing downstream consumes the verdict.And the contradiction that follows, stated: a red satellite leg turns the run RED while delivery has completed; the run's colour then says compatibility, not shipping. (No
continue-on-error, by rule, sosatellite-compat=failuremakes the run's conclusionfailureeven though promote, notify, bake and seal all completed.) And since the baseline control, red means exactly one thing. So the verdict's summary opens — and its final headline repeats —🚨 DELIVERY COMPLETED — this run is red because this core build BROKE <n> satellite(s): <list> (previous image compiled, new image does not). Nothing failed to ship., and theci-failurealert body carries the same line above its Compatibility section (which lists only BROKE lines — an already-broken satellite or an unestablished baseline is an advisory on the run page, never an alert about this build). The same sentence is inContinuousDeliveryContract.md.promoteis already done,notify-platform-updateandplugins-bakedo notneedit. The set is published; each instance gates itself. Blocking the publish on a satellite's compile would hand every satellite a veto over the platform's delivery.alert-on-failure(satellite-compat+satellite-compat-imagein itsneeds) downloads the verdict artifacts and appends the banner plus one line per BROKE satellite — satellite, content sha, both digests, the set, the first three failing types with their first error.delivery-verdict(both jobs inneeds) renders a per-satellite table (content sha, verdict 🔴/🟡/✅/⚪, NodeType count, types,baseline → newdigests) under Compatibility, runs underalways()on a publishing run, and goes red itself only if the gate was skipped/cancelled on a publishing run or reports success with a satellite lacking a verdict. Executed locally against fixture verdicts for all rows (broke, already-broken, unjudged, already-broken with absent types, compatible, no-baseline) plus a not-measured leg: table, advisory line, banner and outputs as designed;skipped→ rc 1;successwith a missing verdict → rc 1.preflightassertsFLEET_READER_APP_ID/FLEET_READER_APP_PRIVATE_KEYRED naming both secret stores. Nocontinue-on-erroranywhere; noif:on a secret; a leg that cannot mint, pull or compile is a red leg. Timeouts: lane job 20 (existing),satellite-compat-image10, all literal.What a red means and who acts: "this core build broke that satellite's NodeTypes — they compiled against the set the fleet is on and do not against this one." The
ci-failureissue names satellite, both digests, set and types → the control instance's triage. The fix is on core's side by measurement: restore the surface, or — if the move was intended — land the satellite's adaptation. A satellite that was already broken is not a red here (advisory; its own daily run / ci-main-red issue own it), and a stale allow entry is an advisory. The daily rebuild will not repair a core break on its own.Budget
ubuntu-latestin a public repo — zero billed minutes.compile-check / Compile every NodeType (vs core), run 34686027657, 2026-09-12 09:31:58Z → 09:33:48Z). Not a trial of this branch (below). Two compiles per leg now (new + baseline): expect ~4–5 min a leg; the extra compile is the control the red rests on.plugins-modulesand run besideplugins-bake(which needs the same thing and takes longer): expected addition to CD's critical path ~0; worst case ≈ +5 min.satellite-compat-image(~1 min) runs besideplugins-modules.The guards — original rationale, and why this job is outside each hazard
Each guard's comment quotes the commit that wrote the rule it extends (found by
git log -S); each was proven able to fail by mutating the workflow (then restored, md5-verified).satellite-compatis outside itPlatformBakeLaneGuard.PlatformBake_CompilesOnlyWhatTheImageEmbedscontent-ref: main, resolved once to a sha the leg records; compiled into a throwawaycheck.csprojagainst the set ALREADY promoted; nothing enters an image, bundle or seal; no version derived; afterpromote, nothing waits, nothing consumes the verdict.CdImageVersionsShareOneSourceGuard.EveryPluginCheckoutUsesTheCommitTheGateResolvedTheSatelliteCompatRegister_IsAMeasurementNotAnImageInputexcuses it only withcontent-ref: mainand never Plugins.content-ref→ sha: red.PlatformNeverDependsOnPluginsGuard.OnlyTheImageLanes_ReachIntoAPluginRepositorypromote; reads content only. And the red is classified: a satellite's own broken commit fails at the baseline too → advisory, never red.MeshWeaver.CrmjoinsSatelliteRepos. Narrower shape (API-read-only) rejected: it cannot compile NodeTypes.PlatformBakeLaneGuard.PlatformBake_NeverAdoptsAnotherBuildsBundlesgh run downloadin a publishing workflow is the defect.satellite-compat-imagedownloads the previous run's bundles as a compile reference for the baseline reading; re-uploaded under a name only the compat legs read, unpacked intorefs-baseline/, never into a bake directory, seal or publication.SatelliteCompatBaselineDownload_IsAReferenceNeverAPublicationasserts the block still contains the download + re-upload and that neither it nor the consuming lane carries any publishing form (publish-bake-bundles.sh,push-bundle-publication.sh,--bake-output,--seed,az storage,BAKE_PUBLISH_TARGETS). Plantingpublish-bake-bundles.shin the block → red; agh run downloadoutside it → the original fact red.Guards (all with
--root .and--self-test; verbatim summary lines, after the last push)Docs
One dated paragraph in
Data/Architecture/ContinuousDeliveryContract.md("After the promote" section), now covering the baseline control and the banner. No new page. No What's New entry: CI-only, no user-visible effect.First-run plan
Continuous Delivery (main)run after merge whosegatedecidespublish=true. Reconcile ticks that publish nothing skip the compat lane by design, anddelivery-verdictdoes not judge it on those.gate's linebaseline (the set the fleet is on): mw-plugin-test:main = sha256:… = 3.0.0-ci.<n>, built by run #<n>andsatellite-compat-imagegreen withsatellite-compat tester: … (equal to ACR's)andbaseline set: ghcr.io/…@sha256:… (<version>, run <id> #<n>) + 4 module bundles— abaseline NOT established: <reason>warning instead means every leg refuses to judge (⚪) and the reason is the thing to fix; (2) fiveCompatibility: <satellite>'s NodeTypes compile against this setlegs, none skipped or cancelled (the verdict's Compatibility step goes RED if the job did not run); (3) fivesatellite-compat-*artifacts, each acompile-check-verdict.jsonwithverdict,content.sha,images.newandimages.baseline; (4) the Compatibility table listing all five as ✅ (or 🟡 with a named satellite-side cause; expected: Education 13 clean); (5)delivery-verdictgreen; noci-failureissue opened by this run.alert-on-failurefiles/comments theci-failureissue with the banner and* **<satellite>** (main @ <sha>): compiled against the previous image (<digest>) and does not against <version>: <type> — <error>; …→ control-instance triage: read the leg's log (both compiles are there); the fix is core's — restore the surface, or land the satellite's adaptation if the move was intended. The set is already published and every instance gates itself — nothing to roll back in delivery.git revertof the merge commit. Nothing else needs undoing — no instance, publication, tag, registry entry or downstream job consumes the verdict artifacts or the new jobs' outputs; the lane's new inputs are optional and the satellites' callers do not pass them.Not done / to watch
main-cd.yml'sworkflow_dispatchresolves main's HEAD server-side and, withrebuild=true, publishes a real image set, seals the Plugins publication and POSTs the release event — not a branch trial. The reusable lane has no dispatch trigger. The compile duration is the same lane on Crm's CI; the verdict/alert shell was executed locally from the workflow file against fixtures; the first real execution is the first main build after merge.promotephase B copies the index to GHCR by digest;satellite-compat-imageasserts equality and fails RED naming both digests if that ever stops holding.main-cd.yml(~1.5 days at current cadence); a baseline older than that yields a refusal-to-judge with that reason, not a red.maincarries a3.0.0-ci.<n>tag today (measured:3.0.0-ci.8414).content.shaand the baseline column tell that apart from a core break.plugins-modulescomment says "this repo holds no registry credential" whileMW_REGISTRY_KEYexists in core's secrets (noMW_REGISTRY_URLvar). Not touched; noting it.🤖 Generated with Claude Code