ci: move the Squad CLI activation pin when the release publishes - #1858
Conversation
The pin in `workflows/shared/squad.md` decides which Squad CLI a brand-new repository installs during activation. Nothing about publishing touched it, so every release silently made it stale -- it once ran 8 days behind and was caught only because someone happened to cold-start a throwaway repo (PR #1818). PR #1855 added the daily backstop that *detects* that state. This adds the half that prevents it: after `publish-cli` succeeds, the pin moves to the version that same run made installable, and the change arrives as a pull request against `dev`. Both halves are load-bearing. Detection alone leaves a red build with no fix path; prevention alone re-breaks the moment someone publishes out-of-band. Three things worth knowing: - The version is confirmed against two independent sources before anything is rewritten -- the version this run published, and npm's `dist-tags.latest`. The bumper never reads `packages/squad-cli/package.json`, which holds the next *unreleased* version and is exactly the E404 that PR #1818 fixed by hand. - There was a third copy of the pin nobody was guarding, in `docs/src/content/docs/guide/gh-aw.md`. Neither the drift guard nor the existing tests looked at it, so it could sit stale while everything reported green. It is now bumped with the other two and asserted equal on every pull request. - The rewrite is a script rather than an inline `run:` block. Its patterns contain backticks and pipes that are hostile to shell quoting -- the SC2016 problem PR #1855 had to suppress -- and as a file it can be executed by the test suite. That last point is what makes this guard non-decorative: `squad-cli-pin.test.ts` runs the real bumper against the version already pinned. That is an identity rewrite, so it touches nothing, but it proves all three patterns still match. A pattern that quietly stops matching would turn the bump into a no-op, which is the same silent decay this issue was filed about. Verified by mutation: reshaping the pin, drifting the docs copy, and retargeting the pull request at `main` each turn the suite red, and the bumper fails closed with a named error when a site goes missing or is duplicated. Closes #1825
🔒 Security Review🔒 Security review: 1 error(s), 2 info.
Automated security review — informational only. |
🛫 PR Readiness Check
PR Scope: 🔧 Infrastructure
|
| Status | Check | Details |
|---|---|---|
| ✅ | Single commit | 1 commit — clean history |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | No source files changed — changeset not required |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ❌ | Copilot threads resolved | 3 unresolved Copilot thread(s) — fix and resolve before merging |
| ✅ | CI passing | All checks passing |
Files Changed (3 files, +377 −0)
| File | +/− |
|---|---|
.github/workflows/squad-npm-publish.yml |
+114 −0 |
scripts/bump-activation-pin.mjs |
+176 −0 |
test/squad-cli-pin.test.ts |
+87 −0 |
Total: +377 −0
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
🏗️ Architectural Review
Automated architectural review — informational only. |
🟡 Impact Analysis — PR #1858Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedci-workflows (1 file)
scripts (1 file)
tests (1 file)
This report is generated automatically for every PR. See #733 for details. |
There was a problem hiding this comment.
🟡 Changes recommended
There are release-automation correctness/reliability issues (PR body generation can emit malformed Markdown, and the new workflow job should pin Node via setup-node) plus a test that relies on cross-test ordering assumptions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR completes the “prevention” half of #1825 by wiring a release-time activation-pin bump into the npm publish workflow, ensuring workflows/shared/squad.md (and the docs copy) are updated automatically when a new Squad CLI release is published.
Changes:
- Add
bump-activation-pinjob to.github/workflows/squad-npm-publish.ymlthat opens a PR againstdevafter a successful CLI publish. - Introduce
scripts/bump-activation-pin.mjsto rewrite all pin sites and fail closed when patterns drift. - Expand
test/squad-cli-pin.test.tsto cover docs consistency, publish-workflow wiring, and to exercise the bumper script.
File summaries
| File | Description |
|---|---|
test/squad-cli-pin.test.ts |
Adds assertions for docs pin consistency, publish-workflow wiring, and executes the bumper script to prove patterns still match. |
scripts/bump-activation-pin.mjs |
New script to rewrite activation pin sites and generate a PR body, failing closed if rewrite targets can’t be located uniquely. |
.github/workflows/squad-npm-publish.yml |
Adds a post-publish job that verifies npm dist-tags.latest, rewrites pins on dev, and opens a PR with the changes. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const previous = matches[0][0]; | ||
| const after = before.replace(pattern, (_full, head, tail) => `${head}${target}${tail}`); | ||
|
|
||
| if (after !== before) { | ||
| applied.push({ file, label, previous: previous.trim() }); | ||
| dirty.add(file); | ||
| } | ||
| sources.set(file, after); |
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 | ||
| with: | ||
| # The pin lives on the default branch, but releases are cut from `main`. | ||
| # Bumping a `main` checkout would edit a file `dev` never sees, and the next | ||
| # merge would quietly restore the stale value. | ||
| ref: dev | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Confirm the version is published and current |
| it('can still find every place it has to rewrite', () => { | ||
| const pin = effectivePin(readPinFile()); | ||
|
|
||
| // Runs the real bumper against the version already pinned. That is an identity | ||
| // rewrite, so it touches nothing — but it exercises all three patterns for real, | ||
| // which is the only way to prove they still match. A pattern that quietly stops | ||
| // matching turns the release-time bump into a no-op, and the whole failure mode | ||
| // #1825 describes is a guard that decays without saying anything. | ||
| const env = { ...process.env, TARGET_VERSION: pin as string }; | ||
| delete env.GITHUB_OUTPUT; | ||
| delete env.PR_BODY_FILE; | ||
|
|
Completes #1825. PR #1855 shipped the daily backstop that detects a stale
activation pin; this is the half that prevents one.
What was still broken
workflows/shared/squad.mddecides which Squad CLI a brand-new repository installsduring activation. Nothing about publishing touched it, so every release silently
made it stale. It once ran 8 days behind and was caught only because someone
happened to cold-start a throwaway repo (PR #1818).
The issue asked for both halves, and said why:
What this adds
A
bump-activation-pinjob insquad-npm-publish.yml, gated onpublish-cli. Itmoves the pin to the version that same run made installable and opens a pull request
against
dev.A third copy nobody was guarding
docs/src/content/docs/guide/gh-aw.mdstates the same default. Neither the driftguard's npm comparison nor the existing tests ever looked at it, so it could sit
stale indefinitely while every other guard reported green. It is now bumped with the
other two and asserted equal on every pull request.
Why the rewrite is a script, not a
run:blockIts patterns contain backticks (the docs table) and pipes (the YAML
||fallback),both hostile to shell quoting — that is the SC2016 problem #1855 had to suppress. As
a file it is also executable by the test suite, which is what makes the guard
non-decorative:
squad-cli-pin.test.tsruns the real bumper against the version already pinned.That is an identity rewrite, so it touches nothing, but it proves all three patterns
still match. A pattern that quietly stopped matching would turn the release-time bump
into a no-op — the same silent decay this issue is about.
Fails closed
dist-tags.latestdisagrees with the published versionIt never reads
packages/squad-cli/package.json— that holds the next unreleasedversion and is exactly the E404 PR #1818 had to fix by hand. A test asserts this.
Verification
test/squad-cli-pin.test.ts: 10 passed (was 6)npx eslintclean;actionlintclean with shellcheck on PATHprerelease and empty input rejected
maininstead ofdev(1 test fails)Note for reviewers
The pull request this job opens will arrive without CI — GitHub does not fire
pull_requestworkflows for pull requests opened withGITHUB_TOKEN. That isconfirmed behaviour observed during E2E fixture work today, not a guess. The rewrite
is therefore verified inside the job that produces it, and the generated body says so.