What main requires here, and what each context proves
MeshWeaver.Plugins is protected by CLASSIC branch protection, not a ruleset. Measured
2026-09-11: repos/Systemorph/MeshWeaver.Plugins/branches/main/protection answers 200 with a
required_status_checks block, and the repository's one ruleset (19008635, Copilot review for
default branch) carries only deletion, non_fast_forward and copilot_code_review — no
required_status_checks rule. So the classic endpoint is the whole answer here, which is the
opposite of the platform repo, where it answers 404 and the rulesets hold everything
(Repositories.md).
strict: false — a branch merely behind main still merges. enforce_admins: true — there is
no override, not even for an owner.
The seven
| Context (verbatim) | Published by | What it proves that nothing else does |
|---|---|---|
Validate node repos |
ci.yml job validate-name-shim, name: Validate node repos |
The legacy name, kept alive by a shim so the rename of 2026-09-08 left no unguarded window. Same verdict as the row below. |
validate / Validate node repos |
ci.yml job id validate → node-repo-validate.yml job Validate node repos |
Repo-shape policy: workflow timeouts ≤ 45 min, the PR-secret preflight, allow-file ratchets, manifest/lock freshness. |
Compile every NodeType (vs core) |
ci.yml job compile-check, name: Compile every NodeType (vs core) |
Every Source/*.cs compiles against the resolved platform. It never runs a test. |
Build + test the portal hosts |
ci.yml job portal-hosts-gate, name: Build + test the portal hosts |
The ~3,000 .NET tests of the 48 portal-host projects. Required since 2026-09-01, after a PR merged with it red and broke main for five hours. |
Repo policy gates |
ci.yml job repo-gates, name: Repo policy gates |
This repo's own policy scripts. |
Module bundles / All selected bundles built |
ci.yml job id modules-floor, name: Module bundles → node-repo-module-pack.yml job All selected bundles built |
Every selected module bundle was actually built. Like the row below it is an AGGREGATE by design — the per-module legs come and go with the selection. |
test-repos / Compile + render node repos (MeshWeaver from ACR) |
ci.yml job id test-repos (it carries no name:) → node-repo-gate.yml job verify, name: Compile + render node repos (MeshWeaver from ACR) |
The only lane that EXECUTES a NodeType's Tests layout area. Imports every node repo into a real mesh, asserts each NodeType reaches CompilationStatus.Ok, renders it, and runs its Tests area green. Required 2026-09-11. |
🚨 Read that middle column carefully — it is the point of this page. A reusable-workflow call
publishes <caller job> / <inner job name>, and the caller contributes its name: if it has
one and its job id otherwise. test-repos has no name:, which is why the context begins with
a lowercase hyphenated id; modules-floor does have one, which is why its context begins
Module bundles and not modules-floor. Two jobs in the same file, two different rules, and
neither string appears anywhere in the workflow as a literal.
Note what is not in the list and why it does not have to be: tests-ratchet
(tests=skipped is debt) runs if: always() off test-repos and reads the gate log; it is a
consumer of the same evidence. Gate shard i/n is deliberately never required — the shard
count is an input, and a repo that required Gate shard 1/1 would wait forever the day the fan-out
widened. The aggregate verify job folds the shards and refuses a run whose slices are not a
disjoint cover of the discovered total, which is what makes a fan-out safe to require at all.
Why the seventh was missing, and what it cost
test-repos existed and ran on every PR — it simply could not block a merge. On 2026-09-10
#1624 auto-merged with test-repos / Compile + render node repos (MeshWeaver from ACR) reporting
completed / failure, and every other required context green. That is the same shape as the
2026-09-01 incident that made Build + test the portal hosts required: a lane that measures
something real, reports honestly, and decides nothing.
The property at stake is specific. compile-check compiles Source/; it never runs a test.
validate's two check-test-suites steps prove the ratchet can fail and that the allow file only
shrinks; neither executes a Tests area. Between #1040 retiring the old job and #1075, Store/Installer
was red on main for five days (213/214, deterministic) behind an entirely green required-check
wall, found only because somebody ran the gate by hand.
The measurement that justified requiring it
🚨 Under classic protection, adding a required context is either free or repo-wide fatal, and the
only thing that separates the two cases is whether that exact name is ALREADY being published on
every PR. A required context with no check-run at all blocks forever — "Expected — Waiting for
status to be reported" — with a full green wall and no red check to point at, and with
enforce_admins: true nobody can merge past it, including the change that would fix it. A
skipped context, by contrast, counts as satisfied. See
Renaming a Required Status Check
(get Doc/Architecture/RenamingARequiredCheck).
So the name was measured before the protection was touched, over the last 45 merged pull requests (#1574 … #1622, 2026-09-09T19:27Z → 2026-09-11T05:51Z), reading check-runs on each PR's full head sha (an abbreviated sha fails the API and the empty body reads as absence):
gh api --paginate "repos/Systemorph/MeshWeaver.Plugins/commits/<full sha>/check-runs?per_page=100" \
--jq '.check_runs[]|"\(.name) | \(.status) | \(.conclusion)"'
| outcome | count (of 45) |
|---|---|
completed / success |
44 |
completed / failure |
1 — #1624, the PR that merged red |
skipped |
0 |
| never reported | 0 |
Extended to the 17 open PRs at the time of the edit: published on every one whose ci.yml run had
started (14 of 17 — the other three were mid-flight or had a workflow-level startup_failure).
Denominator stated on purpose: 62 head shas examined, 59 with a started ci.yml run, 59 publications,
zero skips and zero silent absences.
Three properties were confirmed structurally, not just statistically, because a rate is not a guarantee:
- It runs on
pull_requestunconditionally. ci.yml'son:carries a barepull_request:— nopaths, nobranchesfilter. The caller job's condition isif: ${{ !cancelled() }}— not a secret test, notgithub.actor != 'dependabot[bot]'. It runs even when aneeds:job failed. - Its verdict is never a grey tick. Inside
node-repo-gate.ymltheverifyjob isif: always(), and its first step turns every "cannot measure" case into a RED that names the cause: a failedplan, agateresult that disagrees with the plan, a shard that never ran. Observed live on #1623 —planfailed,gateskipped (publishing the unresolved matrix expression as its name), andverifyreported failure, notskipped. - A legitimately empty run still reports success, and says why. On the Dependabot PR #1544 the
plan set
nothing-to-gate=true, the gate skipped, andverifyconcluded success having asserted that the plan said so — rather than inferring it from produced-no-evidence.
The only way the context fails to publish is a workflow-level startup_failure (16 on
pull_request in the last 1,000 ci.yml runs, 14 of them clustered on four branches on the evening of
2026-09-10). That case publishes zero check-runs, so all six pre-existing required contexts are
absent too and the PR is already blocked. Requiring a seventh therefore adds no new deadlock
class — which is the argument that made this a safe single additive edit rather than a shim-job
migration.
The edit
Additive, through the sub-resource that cannot drop the others:
gh api -X POST \
"repos/Systemorph/MeshWeaver.Plugins/branches/main/protection/required_status_checks/contexts" \
-f 'contexts[]=test-repos / Compile + render node repos (MeshWeaver from ACR)'
Read back afterwards: seven contexts, all app_id: 15368 (GitHub Actions), strict: false,
enforce_admins: true, and every other protection setting byte-identical to the pre-edit capture.
Blast radius, measured before the edit: of 17 open PRs, exactly one moved from mergeable to
blocked — #1635 (draft; test-repos red, all six older contexts green). Every other PR that the
new context blocks was already blocked by an older one, or had a run still in flight.
The standing rule
Never add a required context whose exact name you have not just seen published on real PR head
shas. The workflow name is not the context name, the job id is not the context name, and a
reusable-workflow call publishes <caller job> / <inner job name> — three different strings, one of
which is right. Confirm the literal, then edit protection; never the other way round.
The expectation is also encoded, so this decision is audited rather than merely written down:
Hosting/RepoHealth → RepoHealthPolicy.Fleet lists these context names for this repository, and
its Fleet area reports a Critical if main stops requiring one. See
Repositories.md.