Skip to content

Arm the cross-repo plugin gate with a read-only deploy key - #1067

Merged
rbuergi merged 2 commits into
mainfrom
ci/arm-plugin-gate
Aug 10, 2026
Merged

rbuergi merged 2 commits into
mainfrom
ci/arm-plugin-gate

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Why

The gate added in 98dd7d4 was never armed — PLUGINS_REPO_TOKEN was not among the repo's
secrets, so every PR took the warn-only branch:

Plugin gate NOT armed: … the cross-repo API gate did not run and this PR is UNVERIFIED against the plugins.

Green, and testing nothing — the exact failure shape the job's own comments say it exists to end.

It has now cost a production outage. #683 deleted AiSettingsNodeType.AddSkillSource. The
plugins repo's Store/Plugin/Source/Localizer.cs still called it, and no build in this repo can
see that file
— plugin node source is Roslyn-compiled against this assembly from a different repo.
It merged unverified and surfaced only when the mesh recompiled Store/Plugin in production:
CS0117. Since every plugin declares requires: Store, that left every nodeType:Store/Plugin
partition on memex.systemorph.com — Edu, Agent, Skill, Essentials, Collaboration, DataModelling,
Feedback, Instances, Publish — on the compilation-error overlay. The plugins repo fixed its side in
b10c981, two days later.

Store is in VITAL_MODULES for exactly this reason, so an armed gate would have failed #683.

The credential

A read-only deploy key on Systemorph/MeshWeaver.Plugins (created, id 159776507), stored here
as PLUGINS_REPO_SSH_KEY (set). The gate needs exactly one capability — read that one repo — and a
deploy key grants that and nothing else, so a secret every workflow run can read cannot be widened.

Two alternatives were considered and rejected, and the reasoning is in the workflow comment so it
survives:

Option Why not
Fine-grained PAT Minimal too, but minted against a person and it expires. A gate that silently disarms when someone's token lapses is the very failure this job prevents.
Org meshweaver-cloud GitHub App Installed on all repos with write scopes (self-update, issues/PR/webhooks). Its private key here would let any workflow — including one added by a PR — mint org-wide write tokens.

Revoke by deleting the deploy key on the plugins repo and replacing the secret; nothing else depends
on it.

Behaviour unchanged otherwise

The three-way asymmetry is preserved exactly: fork PR → skip with a warning; credential absent →
warn, do not block; credential present but checkout fails → fail loudly (now worded for a
revoked/replaced deploy key rather than an expired token).

main-cd.yml needs no change — it builds the mw-plugin-test image but never checks out the
plugins repo.

Self-verifying

This PR is the first run with the secret present, so its own plugin-gate job is the test: it will
check out the plugins repo and compile + run Store and Essentials against this PR's build
output. A green gate here means it is genuinely armed; a red one is the gate doing its job.

🤖 Generated with Claude Code

The gate has existed since 98dd7d4 but PLUGINS_REPO_TOKEN was never
provisioned, so every PR took the warn-only branch: "gate NOT armed … this PR is
UNVERIFIED against the plugins". Green, and testing nothing.

That cost a production outage. #683 deleted AiSettingsNodeType.AddSkillSource;
the plugins repo's Store/Plugin/Source/Localizer.cs still called it, and no
build in this repo can see that file. It merged unverified and surfaced only
when the mesh recompiled Store/Plugin in production — CS0117, and since every
plugin declares `requires: Store`, every nodeType:Store/Plugin partition on
memex.systemorph.com (Edu, Agent, Skill, Essentials, Collaboration,
DataModelling, Feedback, Instances, Publish) was left on the compilation-error
overlay. Store is in VITAL_MODULES precisely so this job would have failed that
PR. The plugins repo fixed its side in b10c981, two days later.

The credential is a READ-ONLY DEPLOY KEY on Systemorph/MeshWeaver.Plugins,
stored here as PLUGINS_REPO_SSH_KEY. The gate needs exactly one capability —
read that repo — and a deploy key grants that and nothing else, so a secret
every workflow run can read cannot be widened into anything else. Rejected:

  - a fine-grained PAT is minimal too, but it is minted against a PERSON and
    EXPIRES; a gate that silently disarms when a token lapses is the exact
    failure shape this job exists to prevent;
  - the org's `meshweaver-cloud` App is installed on ALL repos with write
    scopes (self-update, issues/PR/webhook integrations), so its private key
    here would let any workflow — including one added by a PR — mint org-wide
    write tokens.

Revoke by deleting the deploy key on the plugins repo and replacing the secret;
nothing else depends on it. The warn/fail branches keep their asymmetry: absent
credential warns, present-but-unusable fails loudly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 10, 2026 05:35

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.

Pull request overview

Arms the existing cross-repo plugin compatibility gate by switching from a non-existent/unused token secret to a read-only SSH deploy key, so PRs are actually verified against Systemorph/MeshWeaver.Plugins when credentials are available.

Changes:

  • Replace PLUGINS_REPO_TOKEN with PLUGINS_REPO_SSH_KEY and use actions/checkout via ssh-key to read the plugins repo.
  • Update gate messaging/comments to reflect deploy-key lifecycle (missing vs revoked/replaced) and the intended security rationale.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +739 to 740
ssh-key: ${{ secrets.PLUGINS_REPO_SSH_KEY }}
fetch-depth: 1
@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 284 tests  ±0   1 279 ✅ ±0   7m 29s ⏱️ + 1m 35s
   11 suites ±0       5 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 6d153d6. ± Comparison against base commit b1aa58d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

690 tests  ±0   690 ✅ +1   12m 57s ⏱️ -27s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌  - 1 

Results for commit 6d153d6. ± Comparison against base commit b1aa58d.

♻️ This comment has been updated with latest results.

Arming the gate exposed a second defect in it: the job locates the tester with

  find . -path '*/MeshWeaver.PluginTester/bin/*/mw-plugin-test.dll'

inside shard 0's build artifact, but that artifact packs "only the TEST
projects' bin trees" (its own comment). tools/MeshWeaver.PluginTester is a TOOL,
so the sweep never picked it up and the dll was never in the tarball. The very
first armed run therefore died on

  ::error::mw-plugin-test.dll not in the build output — the gate cannot run.

having tested nothing — the exact shape this job exists to prevent. It was
invisible for as long as the gate was unarmed, because the step never ran.

Pack the tester's bin into shard 0 (the artifact the gate already downloads)
rather than rebuilding it in the gate: reusing that artifact is what keeps the
job inside its 12-minute budget, and a restore+build of the framework graph
would blow it. It is the one non-test bin that travels, so the reason is spelled
out where the packing happens.

Inert for the tests: the run loop iterates shard-assign.sh's project list, not
the extracted tree, so an extra bin tree is never mistaken for a test project.

Missing bin is a BUILD fault, not a tolerable absence — the solution builds this
project (MeshWeaver.slnx), so the pack step fails loudly instead of shipping an
artifact the gate cannot use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   4m 54s ⏱️ - 1m 1s
1 977 tests ±0  1 869 ✅ ±0  108 💤 ±0  0 ❌ ±0 
2 126 runs  +1  2 018 ✅ +1  108 💤 ±0  0 ❌ ±0 

Results for commit 6d153d6. ± Comparison against base commit b1aa58d.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 941 tests  +1   1 939 ✅ +1   6m 30s ⏱️ +48s
   11 suites ±0       2 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 6d153d6. ± Comparison against base commit b1aa58d.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 751 tests  +26   1 559 ✅ +26   5m 54s ⏱️ +30s
   11 suites ± 0     192 💤 ± 0 
   11 files   ± 0       0 ❌ ± 0 

Results for commit 6d153d6. ± Comparison against base commit b1aa58d.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 221 tests  +1   1 221 ✅ +1   6m 17s ⏱️ -1s
   11 suites ±0       0 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 6d153d6. ± Comparison against base commit b1aa58d.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   65 files  ± 0     65 suites  ±0   44m 4s ⏱️ + 1m 25s
8 864 tests +28  8 557 ✅ +29  307 💤 ±0  0 ❌  - 1 
9 013 runs  +29  8 706 ✅ +30  307 💤 ±0  0 ❌  - 1 

Results for commit 6d153d6. ± Comparison against base commit b1aa58d.

@rbuergi
rbuergi merged commit cf38cf6 into main Aug 10, 2026
17 checks passed
rbuergi added a commit that referenced this pull request Aug 10, 2026
…its trapdoor

#1067 landed on the same block. Resolved in its favour on everything that is
load-bearing, and against it on the one thing this branch exists to remove:

  KEPT from #1067
    * `ssh-key: ${{ secrets.PLUGINS_REPO_SSH_KEY }}` and the full rationale for a
      read-only deploy key over a PAT or the org App (including how to rotate it).
    * Packing tools/MeshWeaver.PluginTester into shard 0's artifact. Arming the
      gate revealed it could never have run: the artifact packs TEST project bins
      and the tester is a TOOL, so its binary never travelled. Load-bearing.
    * Its fuller account of the #683 outage — moved into the preflight comment so
      the history survives where the rule is now stated.

  DROPPED from #1067
    * `continue-on-error` on the checkout, the "is the credential configured?"
      probe, and the three outcome-gated branches. Those are the trapdoor. The job
      already excludes forks at job level, so they only covered the case that must
      be loud, and `preflight` now proves the credential before the job starts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi deleted the ci/arm-plugin-gate branch August 19, 2026 10:28
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