Make every suite in tests/plugins/ run on every pull request and actually block a merge when it fails — which takes both a new workflow and a required status check in the repo's branch ruleset — and fix the two plan-agent version guards that fail on a clean main, which currently make that wiring impossible.
A red smoke test sat under an all-green PR because nothing runs tests/plugins/ on pull requests — this wires every suite into PR CI and makes the check actually block a merge, after first fixing the two suites that are red on main by construction.
Read and implement all steps in the plan at docs/plans/wire-plugin-tests-into-ci.md — Wire tests/plugins/ into PR CI and fix the version guards blocking it. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/wire-plugin-tests-into-ci.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
More ways to run this plan — goal & workflow prompts, file path
Achieve this goal: Wire tests/plugins/ into PR CI and fix the version guards blocking it. The plan at docs/plans/wire-plugin-tests-into-ci.md describes one approach — use it as reference, but optimize for the outcome. Fan out across parallel subagents where that serves the outcome. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/wire-plugin-tests-into-ci.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
Run a workflow to implement the plan at docs/plans/wire-plugin-tests-into-ci.md — Wire tests/plugins/ into PR CI and fix the version guards blocking it. Brief subagents with the plan file at docs/plans/wire-plugin-tests-into-ci.md. Reserve a final verification phase for the lead agent, not a subagent. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/wire-plugin-tests-into-ci.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
wire-plugin-tests-into-ci.html
docs/plans/wire-plugin-tests-into-ci.html
docs/plans/wire-plugin-tests-into-ci.md
Context
The story behind this plan — what prompted the work and why it matters now.
This closes [#408](https://github.com/shawn-sandy/agentics/issues/408) and its
blocker [#409](https://github.com/shawn-sandy/agentics/issues/409) in one
change, because #408 cannot land alone: enabling PR CI while two suites are
red on main would fail every incoming pull request on arrival.
The gap was proven on PR #405: a marketplace.json version bump broketests/plugins/test-artifact-tools.sh, yet all seven PR checks stayed green —
only a review bot running the file by hand caught it.
PR #410 has since added check-plugin-versions.yml, the repo's firstpull_request test workflow — it runs scripts/check-plugin-versions.mjs
(which fails a PR when a plugin changed without its marketplace version going
up) plus that guard's own unit test under tests/publish/. It does not touchtests/plugins/. So the gap this plan closes is unchanged: of the 19 suites intests/plugins/, four run on a nightly cron via publish-dist.yml and the
other 15 run nowhere. Both guards this plan fixes are still red on main —
re-confirmed after #410 landed.
Two things follow from #410 rather than being invented here. Its guard already
owns "is this plugin's version right?", already reads marketplace.json, and
already exports parseSemver/isHigher — so the CHANGELOG-agreement invariant
belongs inside it rather than in a second script named one letter apart. And it
sets this repo's pull_request workflow conventions (permissions: contents:,
readfetch-depth: 0, node 20), which the new workflow follows.
The two suites red on main are test-build-proposal.sh (check 12) andtest-setup-sites.sh (check 10). Each asserts the plan-agent version inmarketplace.json is strictly above origin/main — true only on a branch
that bumps plan-agent, false everywhere else including main itself. The fix
replaces that branch-dependent comparison with the invariant PRs #405 and
#407 independently converged on for artifact-tools: the marketplace version
must equal the newest release heading in the plugin's CHANGELOG. One format
trap: plan-agent's CHANGELOG headings are ## 3.0.0 — Title (date), not the## [1.2.0] bracketed form artifact-tools uses, so the heading regex must
accept both. One preservation duty: each guard also asserts the plugin
description mentions its skill (build-proposal / setup-sites) — those
assertions stay.
publish-dist.yml keeps its own pre-publish test steps untouched; it gates
publishing, not merging, and is out of scope here. That leaves four suites
running in both places (the nightly publish gate and the new PR gate) whiletests/publish/ stays PR-invisible — deliberate, not an oversight.
A workflow alone does not block anything. A check that reports red still
lets a pull request merge unless it is marked required. This repo protectsmain with two active rulesets (main and main-branch), and neither carries
a required_status_checks rule — they enforce only deletion,non_fast_forward, copilot_code_review, and pull_request. Note the legacy
branch-protection API reports main as unprotected (gh api returns 404), which is
repos/shawn-sandy/agentics/branches/main/protection
misleading: rulesets supersede it, and adding classic branch protection here
would create a third, competing mechanism. Step 9 therefore adds the required
check to the existing ruleset.
Two risks come with that gate. Open pull requests (#384 and #266 today) predate
the workflow file, so GitHub will never run it for them; once the check is
required they stick at "Expected — waiting for status to be reported" until
their authors merge main in. And enforcement is a repo setting, not a commit,
so the rollback for a misfiring check is an admin removing the ruleset rule —
not a revert of this work.
One more property worth recording: .claude-plugin/marketplace.json has a
custom merge driver that keeps the higher semver, but GitHub's web merge button
does not run local merge drivers, and no driver covers CHANGELOG.md at all.
Concurrent version bumps therefore surface as visible conflicts rather than
silent bad merges. The CHANGELOG-agreement guard is less race-prone than theorigin/main comparison it replaces — but a careless conflict resolution can
still land a mismatch, which is caught only once the check is genuinely
required.
Files that change
Every file this plan touches, and what happens to each one.
scripts/check-plugin-versions.mjsmodified extend #410's guard with an exported CHANGELOG-agreement check accepting both heading forms, plus a--changelog <plugin>entry point the suites calltests/publish/test-check-plugin-versions.mjsmodified extend #410's unit test to cover the new export- tests/plugins/
test-build-proposal.shmodified replace the above-origin/main version check with a call to the shared guard, preserving the file's FAILURES-counter idiom and its build-proposal description assertiontest-setup-sites.shmodified same replacement, preserving the setup-sites description assertiontest-artifact-tools.shmodified swap its inline copy of the same invariant for the shared guard
scripts/run-plugin-tests.shnew the run-all glob loop over an optional suite directory, invoked identically by CI and by hand.github/workflows/plugin-tests.ymlnew PR-triggered workflow whose job id is plugin-tests, calling scripts/run-plugin-tests.shtests/plugins/test-plugin-ci-wiring.shnew objective-verification test covering the workflow's shape and both guards' pass and fail pathstests/fixtures/changelog-formats/new two minimal CHANGELOG fixtures, bracketed and unbracketed, proving both heading forms parsetests/fixtures/dummy-suites/new throwaway passing and failing suites the wiring test drives the runner over, so it never points the runner at its own directoryREADME.mdmodified add CI/CD table rows for both plugin-tests.yml and check-plugin-versions.yml (#410 shipped without one)
Steps
The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.
changelogAgrees(plugin, marketplaceDoc, changelogText) asserting the plugin's marketplace version equals the newest release heading in kit/plugins/<name>/CHANGELOG.md — a heading regex accepting both the bracketed ## [1.2.0] form artifact-tools uses and the unbracketed ## 3.0.0 — Title (date) form plan-agent uses — plus a --changelog <plugin> CLI entry point exiting non-zero on disagreement, and extend tests/publish/test-check-plugin-versions.mjs to cover it
node scripts/check-plugin-versions.mjs --changelog plan-agent and ... --changelog artifact-tools both exit 0 against the current tree; each exits non-zero against the disagreeing fixture; and node tests/publish/test-check-plugin-versions.mjs passes with the new cases.ls tests/fixtures/changelog-formats/ shows both forms, and the helper reports agreement for the agreeing pair and disagreement for the other.node scripts/check-plugin-versions.mjs --changelog plan-agent, and swap test-artifact-tools.sh's inline copy for the same call against artifact-tools — preserving each file's own idiom (the echo "N. ..." + FAILURES counter in the first two, fail()/ok() in artifact-tools) and every existing description assertion (build-proposal, setup-sites)
git grep -l "origin/main" tests/plugins/ returns nothing, and each suite's check count is unchanged from before the edit.tests/plugins) and running every *.sh in it via bash and every *.mjs via node, counting failures rather than stopping at the first, printing each failing suite by name, and exiting non-zero if any failed
bash scripts/run-plugin-tests.sh exits 0 listing every suite as passing, and bash scripts/run-plugin-tests.sh tests/fixtures/dummy-suites exits non-zero naming only the deliberately-failing fixture.name: Plugin Tests, the single job's id set to plugin-tests, a pull_request trigger, permissions: contents: read, actions/checkout@v4, actions/setup-node@v4 (node 20, matching publish-dist.yml), timeout-minutes: 20, a concurrency group keyed to the PR ref with cancel-in-progress: true, and one step invoking bash scripts/run-plugin-tests.sh
claude-code-review.yml is named Claude Code Review while the check it reports is claude-review, its job id. Every other workflow here also declares its own least-privilege permissions block, and a read-only PR trigger against untrusted branches is the last place to inherit defaults; 20 minutes leaves headroom for a suite list designed to grow by globactionlint .github/workflows/plugin-tests.yml passes (fall back to a plain YAML parse if actionlint is unavailable), the file contains no hand-listed suite paths, and after the first run the check appears in the PR's list literally as plugin-tests — confirm with gh pr checks <n> rather than assuming..sh, one deliberately failing .sh, one passing .mjs — then add tests/plugins/test-plugin-ci-wiring.sh asserting the workflow exists with its pull_request trigger, job id plugin-tests, permissions, timeout-minutes, and concurrency; that it delegates to scripts/run-plugin-tests.sh rather than hand-listing suites; that run-plugin-tests.sh tests/fixtures/dummy-suites names the failing fixture, still runs and reports the other two, and exits non-zero; and that the extended guard exits non-zero on the disagreeing CHANGELOG fixture
tests/plugins/, so the runner executes it — pointing it at its own directory would make it invoke the runner that is invoking it, recursing until it hangs; a fixture directory gives the same behavioural proof (pickup by glob, failure named, run-all continues) with no self-reference, and a committed test is what stops a future edit silently dropping the trigger or weakening a guard to always-exit-0bash tests/plugins/test-plugin-ci-wiring.sh exits 0 in under a few seconds, and bash scripts/run-plugin-tests.sh completes without hanging — the specific symptom recursion would produce.Workflow | Trigger | Purpose shape
grep -c '| \.*\.yml\ |' README.md equals the number of files in .github/workflows/, and both new rows name their pull_request trigger.bash scripts/run-plugin-tests.sh and confirm all 21 suites (19 existing, plus the wiring test, plus any added here) exit 0
gh pr checks <n> and add that string as a required status check on the existing main ruleset via gh api repos/shawn-sandy/agentics/rulesets/13160559 (or 14869592 — confirm which is authoritative first), adding a required_status_checks rule; then merge main into the two open pull requests (#384, #266) so they can report the new check
gh api repos/shawn-sandy/agentics/rulesets/13160559 --jq '[.rules[].type]' includes required_status_checks, the required context matches gh pr checks output exactly, and a pull request with a deliberately failing suite shows a blocked merge button.Tests
The tests that prove the change does what it promises.
## [1.2.0] heading agrees, unbracketed ## 3.0.0 — Title (date) heading agrees, disagreeing version exits non-zero, missing CHANGELOG exits non-zero with a named reasonDefinition of done
The plan counts as done when every statement below is true — check each one off as you verify it.
Final check
One last pass to confirm the whole change works end to end.
Open a pull request containing this change and confirm a check namedplugin-tests appears in its check list and concludes green — that single
observation proves the trigger fires, the runner script finds every suite, and
all of them pass on a GitHub runner rather than only on a laptop.
Then prove the gate actually gates, which is the part a green check cannot
show. With plugin-tests marked required on the main ruleset, push a commit
that deliberately breaks one suite and confirm two things on the pull request:
the check goes red naming that suite, and the merge button is blocked rather
than merely decorated. Revert the commit and confirm the button unblocks. A
plan that stopped at "the check is green" would have shipped a badge, not a
gate.
Finally, confirm git grep -l "origin/main" tests/plugins/ returns nothing —
the branch-dependent comparison is gone, so no suite needs fetch history to
pass — and run bash scripts/run-plugin-tests.sh locally to confirm it reports
the same result CI did, since a divergence between those two is what makes a
green local run stop meaning anything.
Wrapping up
Three gates that must all pass before this plan is marked completed.
Completion Report
No items to report — all requirements met.