Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions .claude/skills/ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,35 @@ partitions on the compilation-error overlay in production; a separate `AddTracki
variables) and **fails RED naming exactly what to provision**. Adding an input = one line in its
`missing` array.
- **Gates depend on it (`needs: [preflight, …]`) and run unconditionally** — no input-shaped `if:`.
- **The ONE legitimate exemption is a FORK PR** (GitHub withholds org secrets by design). Express it
**once**, as a check on the *event* (`github.event.pull_request.head.repo.fork != true`) — never
as a "the secret is empty" check. At the job level those two look identical and only one is safe.
- **The ONE unconditionally legitimate exemption is a FORK PR** (GitHub withholds org secrets by
design, and no maintainer action changes that). Express it **once**, as a check on the *event*
(`github.event.pull_request.head.repo.fork != true`) — never as a "the secret is empty" check. At
the job level those two look identical and only one is safe.
- **Propagate into the required check.** `collect-results` runs with `always()` and is the ONLY
required status check, so it needs `preflight` in `needs` **plus an explicit fail step** — a
skipped dependency does not fail an `always()` job, which would re-open the trapdoor one level up.

### 🚨 The SECOND secret store — and the one precondition for a `dependabot[bot]` exemption

**A run opened by Dependabot resolves `secrets.X` against a SEPARATE store** (Settings → Secrets and
variables → **Dependabot**), so a secret that is provisioned on the Actions tab and used by every
other run resolves EMPTY. The tell is `Secret source: Dependabot` in the run log, and the preflight
does exactly what it should: it fails RED naming a secret the maintainer can see in the UI. **The
remediation line must therefore name the STORE** — `Settings → Secrets → Actions` is misleading
here — and the fix is to provision the same names into the Dependabot store, never an `if:`.

Core's `dotnet-test.yml` *does* carry `github.actor != 'dependabot[bot]'` on `shared-rules` and
`cross-repo-pair`. That is legitimate for exactly one reason, and it is a **precondition, not a
precedent**: those gates also trigger on `merge_group`, where the Actions store IS available, and
the merge queue is the only path to `main` — *the exemption moves WHERE the gate runs, never
WHETHER it runs*. **Measured 2026-09-06: only `Systemorph/MeshWeaver` has a `merge_queue` rule.**
Plugins, Reinsurance, SocialMedia, Crm, Manufacturing, Education and Memex have none — several
carry a `merge_group:` trigger whose event never fires, which is the trap. Copying that `if:` into
a satellite is a skip-trapdoor, and a silent one: the satellites' required contexts are the gate
jobs themselves, and an absent required context counts as SATISFIED.

Full reference: [DependabotSecretStore.md](../../../src/MeshWeaver.Documentation/Data/Architecture/DependabotSecretStore.md).

**Legitimate `continue-on-error` (do not "fix" these):** the `Publish Test Results` reporter (the
TRX summarize step is the real gate; a GitHub-API 429 must not fail the run) and the green-marker
push/prune (losing a marker costs a redundant run, never correctness). The test is *what does a
Expand Down Expand Up @@ -230,6 +252,9 @@ Full reference:
- [ ] No `continue-on-error` on a gate's input step; no `if:` asking whether a secret/variable is
set. Fork-PR exemption expressed once, on the event.
- [ ] Missing external inputs fail a `preflight` job RED, naming what to provision.
- [ ] Adding a required secret? It is provisioned in **both** stores (Actions *and* Dependabot), and
the preflight's remediation line names the store. A `dependabot[bot]` exemption only where the
same gate runs on `merge_group` — core only.
- [ ] The required check has `preflight` in `needs` **and** an explicit fail step.
- [ ] Deleting a public framework symbol? The node JSON and the live mesh were searched too.
- [ ] Not adding `cancel-in-progress` on `main`.
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ Full reference: [/gui](.claude/skills/gui/SKILL.md) · [GUI/DataBinding.md](src/

**A CI gate must never carry `continue-on-error:` on the step that fetches its input, nor an `if:` that asks whether a secret/variable is set.** GitHub paints a skipped job the same colour as a passed one, so "the gate never ran" and "the gate passed" become indistinguishable. Instead: one `preflight` job asserts every external input and fails RED naming what to provision; gates `needs:` it and run unconditionally; the fork-PR exemption is expressed once, on the *event*, never as "the secret is empty"; the required check carries `preflight` in `needs` plus an explicit fail step. The same applies to guard TESTS — a guard whose subject moved and whose roots did not passes having checked nothing.

🚨 **A Dependabot run reads a SECOND secret store, and a preflight red there is a PROVISIONING task, never a workflow bug.** `Secret source: Dependabot` in the log means `secrets.X` resolved against Settings → Secrets → **Dependabot**, so a secret sitting on the Actions tab reads EMPTY — and the preflight's own "set it under → Actions" line sends the reader to the wrong store. **A secret any `pull_request` lane requires is provisioned in BOTH stores** (`gh secret set <NAME> --app dependabot --repo …`, maintainer-only: values are unreadable through the API); adding a `missing+=` line is a two-store act, and nothing in CI can see the second store. 🚨 **Do NOT reach for `github.actor != 'dependabot[bot]'`.** Core carries it on `shared-rules`/`cross-repo-pair` for ONE reason — those gates also run on `merge_group`, where the Actions store IS available, so *the exemption moves WHERE the gate runs, never WHETHER it runs*. Measured 2026-09-06: **only core has a `merge_queue` rule**; every satellite (and Memex) has none, and several carry a `merge_group:` trigger whose event never fires. Copying that `if:` into a satellite skips the gate outright — invisibly, because its required contexts are the gate jobs themselves and an absent required context counts as SATISFIED. Full reference: [DependabotSecretStore.md](src/MeshWeaver.Documentation/Data/Architecture/DependabotSecretStore.md).

**And never hand-roll (or copy-paste) a node repo's CI.** The shared jobs live here as `workflow_call` workflows (`.github/workflows/node-repo-{validate,compile-check,gate,tag-modules,publish-bake}.yml`); MeshWeaver.Plugins / .Education / .Reinsurance / .SocialMedia call them and keep only repo-specific policy. Adopting one renames that repo's required-status-check contexts to `<caller job> / <name>` — do it in the same change. Full reference: [/ci](.claude/skills/ci/SKILL.md).

🚨 **A change that spans this repo and a plugin repo lands DELETING-HALF-LAST, and CI now holds you to it.** A pull request whose diff removes a `public` top-level type from `src/` — a departure, a move, or a whole assembly leaving — **or a public MEMBER of a type that stays** (#3137 deleted two fields and reddened every Plugins PR for three hours) must declare its counterpart in the PR BODY: `Pairs-with: Systemorph/MeshWeaver.Plugins#904`, or `Pairs-with: none — <reason>` when nothing outside core referenced it (a reason citing a live-mesh sweep must quote `searched: true`; `searched: false` is refused). The `Cross-repo pair (public surface)` gate resolves it and stays red while the counterpart is open, draft, or merged into anything but its repo's default branch. Ordinary work never meets it (measured: `main~25 → main` removes ZERO public types and two members, both #3137). 🚨 **The gate sees exactly TWO of the seven break shapes** — a public type leaving `src/`, and a public member leaving a type that stays. It does NOT see: an added overload making a dependent's `<see cref>` ambiguous (`CS0419` under `-warnaserror` — this bit MeshWeaver.SocialMedia on 2026-09-04 when it moved its core pin), a JSON envelope's field names changing, a comment another repo parses as data, an i18n VALUE change, or — #3276, shape 7 — a method's BEHAVIOUR changing behind an **unchanged signature**, which no surface detector can see by construction. **There is no `Dependent suites (MeshWeaver.Plugins)` check — do not wait for one.** Compile-and-running the dependent's suite against the candidate core commit is #2689's stated acceptance criterion and is NOT built; the partial cover that does exist is the dependent's own CI reacting to core's release event, and [CrossRepoPairGate.md](src/MeshWeaver.Documentation/Data/Architecture/CrossRepoPairGate.md) says per shape whether that actually runs. So a green core PR does not mean the dependents still build — #3276 is a live instance. **And for a dependent that PINS core** (MeshWeaver.Plugins, `MW_PLATFORM_REF`) **even that partial cover does not fire**: its `platform-ref` job resolves the same pin on the release event as on a PR, so the event moves what is BAKED, not what `src/` compiles against — the pin bump IS its integration test, and #3276 sat 48 commits that way. Full reference: [CrossRepoPairGate.md](src/MeshWeaver.Documentation/Data/Architecture/CrossRepoPairGate.md) · [/ci](.claude/skills/ci/SKILL.md).
Expand Down
Loading
Loading