Promote a stable branch to each release for the plugin to install from

RO
20h ago
+80-1

What

  • A new promote-stable job in .github/workflows/release.yml fast-forwards a stable branch to the release tag.
    • It needs release, so it runs only once GoReleaser has published.
    • Like sync-skills, it skips prerelease tags and any tag that isn't the latest stable release.
    • It moves the branch through the git refs API with force=false, so GitHub refuses anything but a fast-forward.
  • RELEASING.md covers the new branch: in the release steps, the stable/prerelease table, a short "Plugin install source" section (including what to do if the job fails), and the distribution channels table.
  • internal/release/manifests_test.go gets a wiring test. It requires the job to need release, skip prereleases, have contents: write, skip non-latest releases and pass force=false.

Why

The 37signals marketplace installs this repo's Claude Code and Codex plugin from main. People install the basecamp command from releases, so between releases a fresh plugin install can get skills that call commands no released CLI has. 4, 5 and 8 commits touched skills/ in the last three release cycles.

basecamp/plugins#13 points the marketplace at stable, and this keeps stable on the latest release.

I moved the branch from the release workflow rather than scripts/release.sh for two reasons. Pushing the tag only starts the build, so moving stable there would publish skills before the binaries exist, and would still move it if the build failed. And this way no other repository has to change per release. It replaces basecamp/plugins#12, which moved commit pins with an hourly workflow and needed a merged PR in that repo for each release.

Card: https://app.basecamp.com/2914079/buckets/48521764/card_tables/cards/10423396016

Testing

  • make check passes: bin/ci exited 0 locally.
  • Workflow lint: actionlint and zizmor report nothing on release.yml. zizmor had 3 ignored and 25 suppressed, all already present.
  • The test catches what it's for: it fails if the job's if: loses the prerelease exclusion, and fails if force=false becomes force=true.
  • stable already exists at v0.14.0 (2dbca87a), so #13 can merge first. The refs/heads/stable rule set is only Copilot code review, which doesn't block ref updates.
  • The API behaviour the job relies on, checked against the real stable:
    • Updating it to the commit it already points at succeeds, which is what a re-run does.
    • Updating it to v0.13.2 with force=false was refused with Update is not a fast forward (HTTP 422), and stable stayed at v0.14.0.
  • Not yet exercised: the job itself. It runs on the next stable release, and it should log stable is now vX.Y.Z (<sha>). If it fails, the release is still out, and rerunning the job recovers it.

Summary by cubic

Adds a promote-stable release job that fast-forwards the stable branch to each new stable release, so the 37signals marketplace installs plugin skills from the latest release instead of whatever is on main.

Details

  • The job runs only after the release has published, and only for the latest stable tag; prereleases and older tags leave stable alone.
  • It uses force=false, so GitHub rejects any update that isn't a fast-forward, keeping stable from moving backwards.
  • If the job fails, the release still goes out but plugin users stay on the previous stable; rerunning the job recovers it.
  • Updates RELEASING.md with the new branch behavior and adds a wiring test covering the job's guards.