You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Dependabot PRs are red fleet-wide: the Dependabot secret store is a partial mirror of Actions secrets #3399
Every Dependabot PR in the fleet is red at preflight, and the gate is right to be
Dependabot workflow runs read secrets from a separate store (Secret source: Dependabot in the
run log) — the repo's Actions secrets are simply not visible to them. Each satellite's Dependabot
store is a partial mirror of its Actions store, so preflight finds an empty required secret and
fails red:
##[error]Required CI inputs are not provisioned, so the heavy gate cannot run.
A gate that cannot run fails RED — it must never go grey and read as a pass.
##[error]missing: secrets.REGISTRY_PUBLISH_TOKEN — …
The preflight is behaving exactly as designed (AGENTS.md: "a gate NEVER tests its own inputs —
no skip-trapdoors"). This is a provisioning gap, not a workflow bug, and the fix must not be an if: that asks whether a secret is set — that is the precise anti-pattern the rule forbids.
All three are dev-dependency bumps that have been red since 2026-08-31 for this reason alone.
Explicitly not proposed
No continue-on-error, no if: secrets.X != '', no Dependabot-actor exemption on the heavy gate.
Any of those turns "the gate never ran" into a green tick, which is how the AddTracking outage
shipped — the preflight's own error message says so.
Measured 2026-09-06 while draining the satellite PR queues.
Measured the whole fleet — and the answer is (a) provision, decisively, for a reason I did not expect
Both stores read for every repo carrying a .github/dependabot.yml, 2026-09-06. actions/secrets
and dependabot/secrets return names only; no value was read or printed. total_count read
before the list in every case, no head.
Org level: zero Actions secrets and zero Dependabot secrets, so the per-repo tables are the
whole story. And no repo holds a Dependabot secret that Actions lacks — the mirror drifts one
way only, which is why nobody notices it drifting.
2 · What actually reds a PR — the raw diff ∩ each preflight's missing+= list
Most of the raw diff is push-only (publish-bake, tag-modules) or gated on if: inputs.publish,
so it is inert on a pull request. The failing set is the intersection with what each preflight
asserts on pull_request — and these preflights deliberately assert what the later lanes consume
too, which is correct and is why a publish-only token like REGISTRY_PUBLISH_TOKEN reds a PR.
repo
asserted on a PR, absent from the Dependabot store
derived (main's preflight list × the store) — see the correction below
Crm
none
control — preflight asserts exactly the 4 secrets its Dependabot store holds
Education
none
control — PR-time preflight wants MW_REGISTRY_USERNAME + MW_REGISTRY_PASSWORD, both mirrored; its AZURE_* assertion lives in the push-only publish-bake (line 1373, job at 1346)
Plugins
none — latent
preflight wants MW_TEST_IMAGE + ACR_*, all mirrored; REGISTRY_PUBLISH_TOKEN reaches modules-floor/modules-rest whose publish step is if: inputs.publish (false on a PR)
MeshWeaver (core)
none today — latent, store empty
dotnet-test.ymlshared-rules + cross-repo-pair both assert MESHWEAVER_APP_ID/_PRIVATE_KEY; both are actor-exempted instead
Memex
not measured
store empty, no Dependabot PR open to measure against
Open Dependabot PRs fleet-wide: 3, exactly the three you list (Manufacturing#40,
SocialMedia#117, Reinsurance#128). Core, Plugins, Crm, Education and Memex have none open.
Correction to the Reinsurance row
Its Required CI inputs job passed on #128 (run 33354473532) and the PR is red one job later:
— the same absent MW_REGISTRY_KEY, surfacing at compile-check with a message that names a script
argument rather than a secret. That run is from 2026-09-01 and predates the fix already merged into
Reinsurance's ci.yml, which moved those four assertions into preflight with exactly this
rationale recorded in the file:
Measured on Dependabot PR #128: preflight GREEN, then compile-check RED one job later […] because
the Dependabot secret store lacked MW_REGISTRY_KEY and nothing here had asked for it.
So the store gap is real and the four names are right; what is not yet measured is a preflight red
naming them. It would be, on a re-run.
3 · Recommendation: (a) provision. (b) is not merely undesirable here — it is unavailable.
I expected to argue this on taste. It turned out to be structural, and core already contains both
halves of the argument.
Core does carry the actor exemption.dotnet-test.yml has if: ${{ … && github.actor != 'dependabot[bot]' }} on shared-rules (line 1997) and cross-repo-pair (line 2128), and collect-results mirrors the identical predicate on its fail
steps. So the fleet already has a sanctioned answer to this exact problem — and the file states the
one reason it is not a trapdoor:
🚨 And this is NOT a skip-trapdoor, for a reason specific to this workflow: it also triggers on merge_group, where the credential IS available. […] The exemption moves WHERE the gate runs,
never WHETHER it runs.
That is the precondition, and it is testable. Measured today:
repo
merge_queue rule?
Systemorph/MeshWeaver
yes — ruleset 2128472, merge_queue with check_response_timeout_minutes: 45
no — ruleset is deletion + non_fast_forward + copilot_code_review only; classic protection has no merge-queue block
Education
no — ruleset 19153714: deletion, non_fast_forward, pull_request, copilot_code_review, required_status_checks
Memex
no — ruleset 21038115, same five
Only core has a merge queue. Several satellites do carry a merge_group: trigger in ci.yml
— which is the trap: the trigger exists and the event never fires. Copying core's if: into a
satellite would move the gate to an event with no path to main, i.e. it would not move it at all.
Three further reasons, in descending order of how much they would have cost:
The fork exemption states an impossibility; a Dependabot exemption would state a preference.
GitHub withholds secrets from a fork and no maintainer action changes that — skipping is the
honest signal and "ask a maintainer" is the only actionable message. A Dependabot run can be
given them: one gh secret set --app dependabot per name. Exempting it is a decision not to
gate, wearing the costume of a structural limit. That is the same category error the rule
forbids, one level up.
In a satellite the skip would be invisible. Their required contexts are the gate jobs
themselves (compile-check / …, test-repos / …, Validate node repos), not a single collect-results; a required context that never appears counts as satisfied. The PR would go
from red, honestly to green, having proven nothing — the AddTracking shape.
A Dependabot PR is the case where the gate matters most. Manufacturing and Crm watch exactly
one ecosystem, github-actions. The diff is the CI's own plumbing — actions/download-artifact 7 → 8 is Manufacturing#40. Exempting the gate from the PR that
changes the gate's own machinery is the worst place to spend an exemption.
On "how would a reader tell exempted from passed" if (b) were ever right: the only honest answer
is core's, and it is not a label — it is that the gate genuinely runs, on the queue's temporary ref,
with the Actions store, before the merge lands. A badge saying "exempted" would not be an answer;
the distinction has to be that the check happened somewhere, or it is a skip.
4 · What the maintainer needs to run — I did not, and will not, touch any secret store
Values are unreadable through the API from any credential, so mirroring needs whoever holds them. Eight commands, three repos, each taking the same value as the identically-named Actions secret:
# MeshWeaver.Manufacturing — unblocks #40
gh secret set ACR_USERNAME --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
gh secret set ACR_PASSWORD --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
gh secret set MW_REGISTRY_INSTANCE_KEY --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
# MeshWeaver.SocialMedia — unblocks #117
gh secret set REGISTRY_PUBLISH_TOKEN --app dependabot --repo Systemorph/MeshWeaver.SocialMedia
# MeshWeaver.Reinsurance — unblocks #128
gh secret set MW_REGISTRY_KEY --app dependabot --repo Systemorph/MeshWeaver.Reinsurance
gh secret set AZURE_CLIENT_ID --app dependabot --repo Systemorph/MeshWeaver.Reinsurance
gh secret set AZURE_TENANT_ID --app dependabot --repo Systemorph/MeshWeaver.Reinsurance
gh secret set AZURE_SUBSCRIPTION_ID --app dependabot --repo Systemorph/MeshWeaver.Reinsurance
Then re-run each PR's workflow; the positive signal is All required CI inputs are provisioned. in
the preflight log, not a green wall.
Not proposed for provisioning (deliberate): PLATFORM_WEBHOOK_SECRET, MESHWEAVER_REPO_TOKEN, ACR_PUSH_* and the AZURE_* triple in Plugins/Crm/Education — no PR-time lane asserts them, so
mirroring them would widen the blast radius of a Dependabot run for no gate coverage. Mirror on
demand, when a preflight starts asserting one.
5 · Why this recurred, and the durable half
#3399 is the second occurrence of #2249, which was closed on 2026-08 after provisioning four
names into two satellites. Since then Reinsurance added MW_REGISTRY_KEY + the AZURE_* triple to
its preflight, SocialMedia added REGISTRY_PUBLISH_TOKEN, and Manufacturing was created — each into
Actions only, each silently re-opening the hole. Nothing in CI can see the second store, so no
gate, test or review could have caught any of them; only the next weekly Dependabot PR could, and it
did, six days late and reading as a workflow bug.
So the rule has to live where someone adding a missing+= line will read it:
A secret that any pull_request-triggered lane requires is provisioned in both stores.
Adding a line to a preflight's missing array is a two-store act.
Also worth a separate, small fix — the preflight's own remediation text. Manufacturing's says:
Set them under **Settings → Secrets and variables → Actions**, then re-run.
which is precisely wrong on a Dependabot run, and sends the reader to a tab where the secret is
already sitting. SocialMedia, Crm and Reinsurance carry a Dependabot note on some of their lines
(Reinsurance on 5 of 11); Manufacturing has none. Worth folding into whatever next PR touches those
files rather than spending four heavy satellite CI runs on message text.
Two smaller findings: the same credential is MW_REGISTRY_INSTANCE_KEY in Manufacturing and MW_REGISTRY_KEY everywhere else (you noted this); and core's and Memex's Dependabot stores are
empty, not partial — core survives only because both of its credentialed gates are actor-exempted
under the merge-queue precondition, which is exactly the arrangement that is not portable to the
satellites.
It changes no gate's behaviour: a doc page carrying the measurement and the precondition, plus the
rule in AGENTS.md and /ci so the next session does not "fix" #3399 by copying core's if: into
a satellite — which is the failure mode this issue is one keystroke away from.
Closing nothing. The three PRs stay red until the eight commands above are run; that is
provisioning, and it is correctly not something a PR can do.
Re-measured against the right denominator — there are no controls, and the list is 11 commands
The earlier narrowing scored each repo as raw store diff ∩ what its preflight asserts. That denominator is the defect this issue is about, one level up: a secret can be CONSUMED by a pull-request job that no preflight ever asked about, and the empty value then surfaces deep in a later lane naming no secret at all. That is precisely Reinsurance#128 (run 33354473532) — Required CI inputsPASSED, compile-check died one job later on compose-sealed-modules.sh: --registry-url needs --registry-key. Scored by the preflight list, that run reads as no gap.
Re-measured against what a PR-reachable job actually references (every secrets.NAME in a job whose if: does not exclude a Dependabot PR, including names handed to a shared workflow_call lane):
repo
consumed on a Dependabot PR, missing from the Dependabot store
Manufacturing
ACR_USERNAME, ACR_PASSWORD, MW_REGISTRY_INSTANCE_KEY — measured red
MW_REGISTRY_KEY — latent, not a control: same compile-check/test-repos path, same absent key
Education
MW_REGISTRY_KEY — latent: consumed by the e2e-* jobs
Plugins
REGISTRY_PUBLISH_TOKEN — latent (publish step is if: inputs.publish, false on a PR)
MeshWeaver
MESHWEAVER_APP_PRIVATE_KEY — latent: auto-armandmerge-queue-steward run on a Dependabot PR and both assert the App credential
Memex
MESHWEAVER_APP_PRIVATE_KEY — latent, same shared lane
Crm and Education looked clean only because their preflights were the least complete — the signal was inverted. Core's exposure is not just dotnet-test.yml (which is actor-exempted); it is auto-arm and merge-queue-steward, neither of which is.
Two structural facts
Only secrets. is doubled.GET /repos/{o}/{r}/dependabot/variables → 404. A Dependabot run reads ordinary repository variables, which is why vars.MW_TEST_IMAGE resolves perfectly in the very run whose secrets.ACR_USERNAME is empty — and why this reads as a workflow bug.
No CI credential can list the Dependabot store. The permissions: block has no secrets/dependabot-secrets key, and the org's only App (meshweaver-cloud, 4220566) holds contents/metadata/pull_requests. And a name diff is weaker anyway: no API returns a value, so a name present with an empty value is invisible to it.
Mirrored already — 14 identifiers, no human needed
Each established from a source an operator is entitled to read; no secret value was read, printed or stored.
AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_SUBSCRIPTION_ID → Reinsurance, SocialMedia, Crm, Education. github-actions-bake is the only principal in the tenant carrying a federated credential for these repos (both the classic and the immutable subject format) and holds exactly Storage File Data Privileged Contributor on the storage account each repo's own vars.BAKE_PUBLISH_TARGETS names.
MESHWEAVER_APP_ID → MeshWeaver, Memex (gh api /apps/meshweaver-cloud → 4220566; the App auto-arm.yml names by slug).
Giving a Dependabot run the OIDC client id grants it nothing — federation is keyed on the token subject, and a PR run's subject matches no federated credential, so azure/login still refuses.
What genuinely needs you — 11 commands
gh secret set reads the value from stdin when --body is omitted (there is no --body-file flag). Same value as the identically-named Actions secret:
gh secret set MESHWEAVER_APP_PRIVATE_KEY --app dependabot --repo Systemorph/MeshWeaver
gh secret set MESHWEAVER_APP_PRIVATE_KEY --app dependabot --repo Systemorph/Memex
gh secret set REGISTRY_PUBLISH_TOKEN --app dependabot --repo Systemorph/MeshWeaver.Plugins
gh secret set MW_REGISTRY_KEY --app dependabot --repo Systemorph/MeshWeaver.Education
gh secret set MW_REGISTRY_KEY --app dependabot --repo Systemorph/MeshWeaver.Reinsurance
gh secret set MW_REGISTRY_KEY --app dependabot --repo Systemorph/MeshWeaver.SocialMedia
gh secret set REGISTRY_PUBLISH_TOKEN --app dependabot --repo Systemorph/MeshWeaver.SocialMedia
gh secret set MW_REGISTRY_KEY --app dependabot --repo Systemorph/MeshWeaver.Crm
gh secret set ACR_USERNAME --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
gh secret set ACR_PASSWORD --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
gh secret set MW_REGISTRY_INSTANCE_KEY --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
The three blocked PRs need only the last five lines (Manufacturing#40 → the three Manufacturing lines; SocialMedia#117 → REGISTRY_PUBLISH_TOKEN; Reinsurance#128 → MW_REGISTRY_KEY). The other six close latent gaps before they surface.
.github/scripts/check-pr-secret-preflight.py: every secrets.NAME a PR-reachable job consumes must be asserted by a preflight, so a store gap is named in the first cheap job instead of eight minutes into a lane. Static, no credential — because none exists. Runs in core's dotnet-test.yml and reaches satellites through node-repo-validate.yml at the caller's platform-ref, so nothing is reddened by merging it; a repo adopts the gate on its next pin bump. Exemptions are one reasoned line in .github/pr-secret-preflight-allow.txt, and a stale entry fails.
Falsified both ways on real trees: core with both MESHWEAVER_APP_ID assertions broken → 1 violation, exit 1; restored → 0, exit 0. SocialMedia's real tree → 4 violations, exit 1 (5 of 9 asserted); its real preflight completion → 0, exit 0 (9 of 9). Self-test is 19 cases and runs first.
Worth recording: the first core falsification came back green, because merge-queue-steward.yml asserts the same name and assertion is a union across jobs — breaking one job was not a falsification. Writing it also caught a genuine false positive: auto-arm.yml asserts with [ -n "$VAR" ] and the first regex required ${VAR}.
Doc: Doc/Architecture/DependabotSecretStore (corrected denominator, the absent variables store, the credential constraint, the gate, and the identifier-vs-credential split).
Leaving this open — the 11 above are provisioning, which no PR can do.
Re-measured 2026-09-07 ~17:00Z from the Dependabot runs themselves — the table above is wrong in two rows, and one of them is the row it calls its control
All 22 open Dependabot PRs across the four satellites are still red. I re-derived the gap per repo
from the run log rather than from a store diff, because the store diff cannot see an empty
value — a secret that exists with an empty string is invisible to keys[] and reads as provisioned.
Instrument. For each repo: newest event=pull_request, actor=dependabot[bot], conclusion=failure run → its Required CI inputs job → the log. Three independent readings per
repo, so they can disagree and expose a bad parse:
Secret source: Dependabot present (confirms which store answered),
the step's env echo — NAME: with nothing after it is empty in that store, NAME: *** is present,
Crm is NOT clean. The table says none, and cites Crm draining to zero as "the control that
makes the whole table credible" — so the credibility argument rests on the one entry that is wrong.
Crm's five Dependabot PRs die with:
which AGENTS.md already names as the fingerprint of an absent MW_REGISTRY_KEY (Reinsurance#128).
And Crm's preflight goes GREEN while that happens — for a reason worth recording, because it is
not "the preflight is broken". Crm added the MW_REGISTRY_KEY assertion to its preflight on main
at 2026-09-07T12:45Z. Its five Dependabot branches predate that: grep -c 'MW_REGISTRY_KEY:-'
on PR #59's ownci.yml = 0. A gate defined in a workflow file evaluates the version in the
PR's own tree, so fixing the gate on main unblocks nothing already open — the same structural fact
that made the platform-pin move unblock zero of 19 PRs today. Rebasing those branches would turn a
late unnamed failure into a clear red; it would not make them pass.
Reinsurance needs one secret, not four.AZURE_CLIENT_ID, AZURE_SUBSCRIPTION_ID and AZURE_TENANT_ID all echo as *** in the Dependabot store — they are present. Only MW_REGISTRY_KEY is empty, and it is the only one the preflight names.
Remediation — six commands, not nine
gh secret set MW_REGISTRY_KEY --app dependabot --repo Systemorph/MeshWeaver.Reinsurance
gh secret set MW_REGISTRY_KEY --app dependabot --repo Systemorph/MeshWeaver.Crm
gh secret set REGISTRY_PUBLISH_TOKEN --app dependabot --repo Systemorph/MeshWeaver.SocialMedia
gh secret set ACR_USERNAME --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
gh secret set ACR_PASSWORD --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
gh secret set MW_REGISTRY_INSTANCE_KEY --app dependabot --repo Systemorph/MeshWeaver.Manufacturing
Values must equal the Actions secret of the same name; they are unreadable through the API, so this
stays maintainer-only.
One caveat on my own instrument, since it bit me twice here
My first parse matched ::error::missing: — but a downloaded log renders it ##[error]missing:, so
it reported MISSING=[] for every repo. My second matched the env echo with \s{2} where the log
has three spaces, and reported both empty and present as []. Each time the failure mode was a parse returning nothing, presenting as "nothing is missing" — the same shape as the skipped gate
this issue is about. That is why the table above prints the present column: a row with an empty
gap and an empty denominator is a failed read, not a clean repo.
Activity
rbuergi commentedon Sep 6, 2026
Measured the whole fleet — and the answer is (a) provision, decisively, for a reason I did not expect
Both stores read for every repo carrying a
.github/dependabot.yml, 2026-09-06.actions/secretsand
dependabot/secretsreturn names only; no value was read or printed.total_countreadbefore the list in every case, no
head.1 · Raw store diff (Actions ∖ Dependabot)
ACR_PUSH_PASSWORD,ACR_PUSH_USERNAME,AZURE_CLIENT_ID,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,MESHWEAVER_REPO_TOKEN,PLATFORM_WEBHOOK_SECRET,REGISTRY_PUBLISH_TOKENAZURE_CLIENT_ID,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,MESHWEAVER_REPO_TOKEN,MW_REGISTRY_KEY,PLATFORM_WEBHOOK_SECRETREGISTRY_PUBLISH_TOKENAZURE_CLIENT_ID,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,MW_REGISTRY_KEY,PLATFORM_WEBHOOK_SECRETACR_PASSWORD,ACR_USERNAME,MW_REGISTRY_INSTANCE_KEY,PLATFORM_WEBHOOK_SECRETAZURE_CLIENT_ID,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,MESHWEAVER_APP_ID,MESHWEAVER_APP_PRIVATE_KEY,MW_REGISTRY_KEY,PLATFORM_WEBHOOK_SECRETOrg level: zero Actions secrets and zero Dependabot secrets, so the per-repo tables are the
whole story. And no repo holds a Dependabot secret that Actions lacks — the mirror drifts one
way only, which is why nobody notices it drifting.
2 · What actually reds a PR — the raw diff ∩ each preflight's
missing+=listMost of the raw diff is push-only (
publish-bake,tag-modules) or gated onif: inputs.publish,so it is inert on a pull request. The failing set is the intersection with what each
preflightasserts on
pull_request— and these preflights deliberately assert what the later lanes consumetoo, which is correct and is why a publish-only token like
REGISTRY_PUBLISH_TOKENreds a PR.ACR_USERNAME,ACR_PASSWORD,MW_REGISTRY_INSTANCE_KEYSecret source: Dependabot, all three empty in the step env, three##[error]linesREGISTRY_PUBLISH_TOKENSecret source: DependabotlineMW_REGISTRY_KEY,AZURE_CLIENT_ID,AZURE_TENANT_ID,AZURE_SUBSCRIPTION_IDMW_REGISTRY_USERNAME+MW_REGISTRY_PASSWORD, both mirrored; itsAZURE_*assertion lives in the push-onlypublish-bake(line 1373, job at 1346)MW_TEST_IMAGE+ACR_*, all mirrored;REGISTRY_PUBLISH_TOKENreachesmodules-floor/modules-restwhose publish step isif: inputs.publish(false on a PR)dotnet-test.ymlshared-rules+cross-repo-pairboth assertMESHWEAVER_APP_ID/_PRIVATE_KEY; both are actor-exempted insteadOpen Dependabot PRs fleet-wide: 3, exactly the three you list (Manufacturing#40,
SocialMedia#117, Reinsurance#128). Core, Plugins, Crm, Education and Memex have none open.
Correction to the Reinsurance row
Its
Required CI inputsjob passed on #128 (run 33354473532) and the PR is red one job later:— the same absent
MW_REGISTRY_KEY, surfacing atcompile-checkwith a message that names a scriptargument rather than a secret. That run is from 2026-09-01 and predates the fix already merged into
Reinsurance's
ci.yml, which moved those four assertions intopreflightwith exactly thisrationale recorded in the file:
So the store gap is real and the four names are right; what is not yet measured is a preflight red
naming them. It would be, on a re-run.
3 · Recommendation: (a) provision. (b) is not merely undesirable here — it is unavailable.
I expected to argue this on taste. It turned out to be structural, and core already contains both
halves of the argument.
Core does carry the actor exemption.
dotnet-test.ymlhasif: ${{ … && github.actor != 'dependabot[bot]' }}onshared-rules(line 1997) andcross-repo-pair(line 2128), andcollect-resultsmirrors the identical predicate on its failsteps. So the fleet already has a sanctioned answer to this exact problem — and the file states the
one reason it is not a trapdoor:
That is the precondition, and it is testable. Measured today:
merge_queuerule?merge_queuewithcheck_response_timeout_minutes: 45deletion+non_fast_forward+copilot_code_reviewonly; classic protection has no merge-queue blockdeletion,non_fast_forward,pull_request,copilot_code_review,required_status_checksOnly core has a merge queue. Several satellites do carry a
merge_group:trigger inci.yml— which is the trap: the trigger exists and the event never fires. Copying core's
if:into asatellite would move the gate to an event with no path to
main, i.e. it would not move it at all.Three further reasons, in descending order of how much they would have cost:
GitHub withholds secrets from a fork and no maintainer action changes that — skipping is the
honest signal and "ask a maintainer" is the only actionable message. A Dependabot run can be
given them: one
gh secret set --app dependabotper name. Exempting it is a decision not togate, wearing the costume of a structural limit. That is the same category error the rule
forbids, one level up.
themselves (
compile-check / …,test-repos / …,Validate node repos), not a singlecollect-results; a required context that never appears counts as satisfied. The PR would gofrom red, honestly to green, having proven nothing — the
AddTrackingshape.one ecosystem,
github-actions. The diff is the CI's own plumbing —actions/download-artifact7 → 8 is Manufacturing#40. Exempting the gate from the PR thatchanges the gate's own machinery is the worst place to spend an exemption.
On "how would a reader tell exempted from passed" if (b) were ever right: the only honest answer
is core's, and it is not a label — it is that the gate genuinely runs, on the queue's temporary ref,
with the Actions store, before the merge lands. A badge saying "exempted" would not be an answer;
the distinction has to be that the check happened somewhere, or it is a skip.
4 · What the maintainer needs to run — I did not, and will not, touch any secret store
Values are unreadable through the API from any credential, so mirroring needs whoever holds them.
Eight commands, three repos, each taking the same value as the identically-named Actions secret:
Then re-run each PR's workflow; the positive signal is
All required CI inputs are provisioned.inthe preflight log, not a green wall.
Not proposed for provisioning (deliberate):
PLATFORM_WEBHOOK_SECRET,MESHWEAVER_REPO_TOKEN,ACR_PUSH_*and theAZURE_*triple in Plugins/Crm/Education — no PR-time lane asserts them, somirroring them would widen the blast radius of a Dependabot run for no gate coverage. Mirror on
demand, when a preflight starts asserting one.
5 · Why this recurred, and the durable half
#3399 is the second occurrence of #2249, which was closed on 2026-08 after provisioning four
names into two satellites. Since then Reinsurance added
MW_REGISTRY_KEY+ theAZURE_*triple toits preflight, SocialMedia added
REGISTRY_PUBLISH_TOKEN, and Manufacturing was created — each intoActions only, each silently re-opening the hole. Nothing in CI can see the second store, so no
gate, test or review could have caught any of them; only the next weekly Dependabot PR could, and it
did, six days late and reading as a workflow bug.
So the rule has to live where someone adding a
missing+=line will read it:Also worth a separate, small fix — the preflight's own remediation text. Manufacturing's says:
which is precisely wrong on a Dependabot run, and sends the reader to a tab where the secret is
already sitting. SocialMedia, Crm and Reinsurance carry a Dependabot note on some of their lines
(Reinsurance on 5 of 11); Manufacturing has none. Worth folding into whatever next PR touches those
files rather than spending four heavy satellite CI runs on message text.
Two smaller findings: the same credential is
MW_REGISTRY_INSTANCE_KEYin Manufacturing andMW_REGISTRY_KEYeverywhere else (you noted this); and core's and Memex's Dependabot stores areempty, not partial — core survives only because both of its credentialed gates are actor-exempted
under the merge-queue precondition, which is exactly the arrangement that is not portable to the
satellites.
6 · PR
#3412 — #3412
It changes no gate's behaviour: a doc page carrying the measurement and the precondition, plus the
rule in
AGENTS.mdand/ciso the next session does not "fix" #3399 by copying core'sif:intoa satellite — which is the failure mode this issue is one keystroke away from.
Closing nothing. The three PRs stay red until the eight commands above are run; that is
provisioning, and it is correctly not something a PR can do.
rbuergi commentedon Sep 6, 2026
Re-measured against the right denominator — there are no controls, and the list is 11 commands
The earlier narrowing scored each repo as raw store diff ∩ what its
preflightasserts. That denominator is the defect this issue is about, one level up: a secret can be CONSUMED by a pull-request job that no preflight ever asked about, and the empty value then surfaces deep in a later lane naming no secret at all. That is precisely Reinsurance#128 (run 33354473532) —Required CI inputsPASSED,compile-checkdied one job later oncompose-sealed-modules.sh: --registry-url needs --registry-key. Scored by the preflight list, that run reads as no gap.Re-measured against what a PR-reachable job actually references (every
secrets.NAMEin a job whoseif:does not exclude a Dependabot PR, including names handed to a sharedworkflow_calllane):ACR_USERNAME,ACR_PASSWORD,MW_REGISTRY_INSTANCE_KEY— measured redREGISTRY_PUBLISH_TOKEN(measured red),MW_REGISTRY_KEY(latent)MW_REGISTRY_KEY— measured red atcompile-checkMW_REGISTRY_KEY— latent, not a control: samecompile-check/test-repospath, same absent keyMW_REGISTRY_KEY— latent: consumed by thee2e-*jobsREGISTRY_PUBLISH_TOKEN— latent (publish step isif: inputs.publish, false on a PR)MESHWEAVER_APP_PRIVATE_KEY— latent:auto-armandmerge-queue-stewardrun on a Dependabot PR and both assert the App credentialMESHWEAVER_APP_PRIVATE_KEY— latent, same shared laneCrm and Education looked clean only because their preflights were the least complete — the signal was inverted. Core's exposure is not just
dotnet-test.yml(which is actor-exempted); it isauto-armandmerge-queue-steward, neither of which is.Two structural facts
secrets.is doubled.GET /repos/{o}/{r}/dependabot/variables→ 404. A Dependabot run reads ordinary repository variables, which is whyvars.MW_TEST_IMAGEresolves perfectly in the very run whosesecrets.ACR_USERNAMEis empty — and why this reads as a workflow bug.permissions:block has nosecrets/dependabot-secretskey, and the org's only App (meshweaver-cloud, 4220566) holds contents/metadata/pull_requests. And a name diff is weaker anyway: no API returns a value, so a name present with an empty value is invisible to it.Mirrored already — 14 identifiers, no human needed
Each established from a source an operator is entitled to read; no secret value was read, printed or stored.
AZURE_CLIENT_ID/AZURE_TENANT_ID/AZURE_SUBSCRIPTION_ID→ Reinsurance, SocialMedia, Crm, Education.github-actions-bakeis the only principal in the tenant carrying a federated credential for these repos (both the classic and the immutable subject format) and holds exactly Storage File Data Privileged Contributor on the storage account each repo's ownvars.BAKE_PUBLISH_TARGETSnames.MESHWEAVER_APP_ID→ MeshWeaver, Memex (gh api /apps/meshweaver-cloud→ 4220566; the Appauto-arm.ymlnames by slug).Giving a Dependabot run the OIDC client id grants it nothing — federation is keyed on the token subject, and a PR run's subject matches no federated credential, so
azure/loginstill refuses.What genuinely needs you — 11 commands
gh secret setreads the value from stdin when--bodyis omitted (there is no--body-fileflag). Same value as the identically-named Actions secret:The three blocked PRs need only the last five lines (Manufacturing#40 → the three
Manufacturinglines; SocialMedia#117 →REGISTRY_PUBLISH_TOKEN; Reinsurance#128 →MW_REGISTRY_KEY). The other six close latent gaps before they surface.The durable half — #3437
.github/scripts/check-pr-secret-preflight.py: everysecrets.NAMEa PR-reachable job consumes must be asserted by a preflight, so a store gap is named in the first cheap job instead of eight minutes into a lane. Static, no credential — because none exists. Runs in core'sdotnet-test.ymland reaches satellites throughnode-repo-validate.ymlat the caller'splatform-ref, so nothing is reddened by merging it; a repo adopts the gate on its next pin bump. Exemptions are one reasoned line in.github/pr-secret-preflight-allow.txt, and a stale entry fails.Falsified both ways on real trees: core with both
MESHWEAVER_APP_IDassertions broken → 1 violation, exit 1; restored → 0, exit 0. SocialMedia's real tree → 4 violations, exit 1 (5 of 9 asserted); its real preflight completion → 0, exit 0 (9 of 9). Self-test is 19 cases and runs first.Worth recording: the first core falsification came back green, because
merge-queue-steward.ymlasserts the same name and assertion is a union across jobs — breaking one job was not a falsification. Writing it also caught a genuine false positive:auto-arm.ymlasserts with[ -n "$VAR" ]and the first regex required${VAR}.Doc:
Doc/Architecture/DependabotSecretStore(corrected denominator, the absent variables store, the credential constraint, the gate, and the identifier-vs-credential split).Leaving this open — the 11 above are provisioning, which no PR can do.
ci: a commented-out assertion must not count as one (Copilot review, #…
rbuergi commentedon Sep 7, 2026
Re-measured 2026-09-07 ~17:00Z from the Dependabot runs themselves — the table above is wrong in two rows, and one of them is the row it calls its control
All 22 open Dependabot PRs across the four satellites are still red. I re-derived the gap per repo
from the run log rather than from a store diff, because the store diff cannot see an empty
value — a secret that exists with an empty string is invisible to
keys[]and reads as provisioned.Instrument. For each repo: newest
event=pull_request,actor=dependabot[bot],conclusion=failurerun → itsRequired CI inputsjob → the log. Three independent readings perrepo, so they can disagree and expose a bad parse:
Secret source: Dependabotpresent (confirms which store answered),NAME:with nothing after it is empty in that store,NAME: ***is present,##[error]missing:lines.REGISTRY_PUBLISH_TOKENACR_USERNAME,ACR_PASSWORD,MESHWEAVER_APP_ID,MESHWEAVER_APP_PRIVATE_KEYsecrets.REGISTRY_PUBLISH_TOKENMW_REGISTRY_KEYACR_*,AZURE_CLIENT_ID,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,MESHWEAVER_APP_*secrets.MW_REGISTRY_KEYACR_USERNAME,ACR_PASSWORD,MW_REGISTRY_INSTANCE_KEYMW_REGISTRY_KEYonlyACR_*,AZURE_CLIENT_ID,AZURE_SUBSCRIPTION_ID,AZURE_TENANT_ID,MESHWEAVER_APP_*secrets.MW_REGISTRY_KEYThe two corrections
Crm is NOT clean. The table says none, and cites Crm draining to zero as "the control that
makes the whole table credible" — so the credibility argument rests on the one entry that is wrong.
Crm's five Dependabot PRs die with:
which AGENTS.md already names as the fingerprint of an absent
MW_REGISTRY_KEY(Reinsurance#128).And Crm's preflight goes GREEN while that happens — for a reason worth recording, because it is
not "the preflight is broken". Crm added the
MW_REGISTRY_KEYassertion to its preflight onmainat 2026-09-07T12:45Z. Its five Dependabot branches predate that:
grep -c 'MW_REGISTRY_KEY:-'on PR #59's own
ci.yml= 0. A gate defined in a workflow file evaluates the version in thePR's own tree, so fixing the gate on
mainunblocks nothing already open — the same structural factthat made the platform-pin move unblock zero of 19 PRs today. Rebasing those branches would turn a
late unnamed failure into a clear red; it would not make them pass.
Reinsurance needs one secret, not four.
AZURE_CLIENT_ID,AZURE_SUBSCRIPTION_IDandAZURE_TENANT_IDall echo as***in the Dependabot store — they are present. OnlyMW_REGISTRY_KEYis empty, and it is the only one the preflight names.Remediation — six commands, not nine
Values must equal the Actions secret of the same name; they are unreadable through the API, so this
stays maintainer-only.
One caveat on my own instrument, since it bit me twice here
My first parse matched
::error::missing:— but a downloaded log renders it##[error]missing:, soit reported
MISSING=[]for every repo. My second matched the env echo with\s{2}where the loghas three spaces, and reported both empty and present as
[]. Each time the failure mode was aparse returning nothing, presenting as "nothing is missing" — the same shape as the skipped gate
this issue is about. That is why the table above prints the present column: a row with an empty
gap and an empty denominator is a failed read, not a clean repo.
Blocked on the six commands: 22 PRs — SocialMedia #146 #145 #144 #143 #142 #117, Crm #63 #62 #61
#60 #59, Manufacturing #62 #61 #60 #59 #40, Reinsurance #176 #175 #174 #173 #172 #128.