Cut a release
Skill strapi-community/plugin-rest-cache/.claude/skills/cut-a-release
Publish this plugin to npm - a production release, a pre-release, or an experimental build from a branch. Use when preparing or shipping a release, updating the changelog, or debugging the publish workflow.From its SKILL.md
npx -y skills add strapi-community/plugin-rest-cache --skill cut-a-releaseAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
8.6 KB, as published. Nobody here has run it
Cutting a release
Publishing is always manual. There is no trigger that publishes on merge, and there must never be one. Two humans gates stand in front of npm: pressing "Publish release" on a draft, and approving the deployment environment.
The pieces
release-please.yml | Watches main, maintains one rolling release PR, and creates draft GitHub Releases. Publishes nothing. |
publish.yml | The only thing that talks to npm. Never fires on merge. |
release-please-config.json | Changelog sections, package list, draft: true. |
.release-please-manifest.json | Current version per package. release-please owns this; do not hand-edit. |
Production release
- Merge work to
mainusing conventional commits. release-please keeps achore: release X.Y.ZPR up to date, with the changelog grouped by change type. - Review that PR. The version comes from the commit types since the last
release —
featgives a minor,fixa patch,!or aBREAKING CHANGE:footer a major. If the proposed version is wrong, the commit messages are wrong; fix them rather than editing the version. - Merge it. release-please tags and creates draft Releases.
- Go to Releases, review, press Publish release.
publish.ymlstarts and waits on thenpm-latestenvironment. A maintainer approves. It packs and publishes to thelatesttag.
The draft step is not ceremony: a Release created by GITHUB_TOKEN does not
trigger other workflows, so an auto-published one would silently never reach
npm.
Pre-release
release-please only ever proposes stable versions. A beta is not something
it produces, and there is deliberately no prerelease mode in its config - that
made every ordinary release a beta and left the repository unable to reach a
stable version without a Release-As: override.
So a beta is a manual dispatch. Run the publish workflow from the Actions tab, with the branch selector on main:
mode:nextversion: the exact version, e.g.5.2.0-beta.0target_branch: the branch to build, defaults tomain
It stamps that version across all three packages, builds, and publishes to the
next dist-tag through the npm-prerelease environment. Users install it with
npm install @strapi-community/plugin-rest-cache@next.
version must carry a prerelease suffix. scripts/stamp-version.mjs refuses a
stable version, so no dispatch can put one on npm - stable versions come only
from release-please and the version already committed to package.json.
Nothing is tagged and no GitHub Release is created: a dispatched beta is a build of a branch, not a point in the release history. The run summary records the commit.
If a GitHub Release is marked as a pre-release and published, that also routes
to next - publish.yml reads github.event.release.prerelease. That path
publishes the version release-please committed, and never stamps over it.
Moving off a prerelease version
If the last release was a prerelease, release-please will not graduate on its
own. The default versioning strategy keeps the suffix, so 5.1.0-beta plus a
fix: becomes 5.1.1-beta, not 5.1.0.
Land a commit on main whose message carries the version as a footer:
chore: graduate to a stable release
Release-As: 5.1.0
It applies to that one release and leaves nothing behind in the config.
Note where the footer has to be. GitHub builds a squash commit from the pull request title and body only when the branch has more than one commit; with a single commit it uses that commit's own message. A footer written in the pull request description is silently dropped in that case, and the next release comes out as another prerelease.
Experimental build from a pull request
The easy path. Add the publish-experimental label to a pull request. It
publishes 0.0.0-experimental.<pr head sha> to the experimental dist-tag and
comments on the PR with the exact install command.
The workflow then removes the label again, including when the publish
failed. The label is a one-shot request, not a mode: to cut another build after
pushing more commits, add it back. It deliberately no longer fires on
synchronize, which used to republish silently on every push for as long as
the label happened to still be attached.
Same-repo pull requests only; a fork PR never reaches the publish job.
Experimental build from a branch
When there is no pull request, or you want a specific branch.
Run the publish workflow from the Actions tab:
- keep the branch selector on main — this matters, see below;
mode:experimental;target_branch: the branch to build.
It publishes 0.0.0-experimental.<sha> to the experimental dist-tag. Install
by exact version, not by tag — the tag is clobbered by the next experimental
build:
npm install @strapi-community/[email protected].<sha>
Why the branch selector must stay on main, and why the PR trigger is pull_request_target
npm trusted publishing is not branch-scoped. It checks the repository, the
workflow filename and the environment, and discards the ref. Since
workflow_dispatch runs the workflow file from the branch you pick, choosing a
feature branch would hand that branch's own YAML a publish-capable token — and
npm publish defaults to --tag latest.
The label trigger has the same problem in a different shape: plain
pull_request would run the PR branch's copy of the workflow, with publish
credentials in scope. That is what Strapi's own experimental workflow does, and
it is a real accepted risk on their side. This one uses
pull_request_target, which runs the workflow file as it exists on the default
branch, so neither the environment gate nor the version assertion can be edited
by the branch being published.
The usual pull_request_target trap - checking out untrusted code in a job
holding secrets - does not apply here, because the build job holds neither
secrets nor an id-token.
So the workflow takes the branch as an input instead. The build job checks it
out but has no id-token and no environment; the publish job runs from main's
YAML and asserts the version matches 0.0.0-experimental.<40 hex> before
publishing. Both halves are needed: without the assertion, a poisoned build
could stamp 5.99.0, which every ^5.0.0 range would resolve regardless of
dist-tag.
Things that will break a release
Everything publishing lives in publish.yml and must stay there. npm
validates the workflow filename and allows only one trusted-publisher config
per package, so a second publishing workflow file would fail authentication.
Never npm publish from a package directory. npm does not understand the
workspace: protocol and would publish workspace:* literally, producing a
manifest nobody can install. The workflow runs pnpm pack first and publishes
the tarball. The plugin depends on the memory provider this way, so this
affects the primary package.
The build must run before packing, and in order. The providers typecheck
against the plugin's emitted declarations, so pnpm run build builds the
plugin first, then the providers. files is ["dist"] — an unbuilt package
publishes an empty directory.
Node 24 or newer in the publish job. Trusted publishing needs npm ≥ 11.5.1 and no Node 22 release ships one.
If a publish fails
- Waiting for approval — expected. Someone with reviewer rights approves the deployment.
- 401/403 from npm — the trusted publisher config on npmjs.com does not match this workflow's filename or environment name. Both are exact, case-sensitive strings.
ERESOLVEinstalling an experimental build — the internal peer ranges were not rewritten.scripts/stamp-experimental-version.mjspins them to the exact experimental version because a^5.xpeer cannot satisfy0.0.0-*.- Version assertion failed — the build produced a version that does not match the mode. Treat this as a real signal, not a check to relax.
Never
- Add a trigger that publishes on a push to a branch, or on a merge. The
labelled-PR trigger is deliberate and only ever produces
0.0.0-experimentalbuilds; nothing may reach thelatesttag without a human pressing publish. - Change the PR trigger from
pull_request_targettopull_request. - Give the build job
id-token: write, an environment, or any secret. - Remove the version-shape assertion.
- Add a second workflow file that publishes.