Semantic release automation
Skill stealth-engine/skills/skills/semantic-release-automation
Agent skills we use daily in the studio — reusable, on-demand context for Claude Code, Cursor & other skills.sh-compatible AI agents. Install: npx skills add stealth-engine/skills
npx -y skills add stealth-engine/skills --skill semantic-release-automationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Automate versioning, changelog, tags, GitHub Releases, and npm publishing from Conventional Commits with semantic-release. Use when setting up or debugging automated releases, wiring a `.releaserc` / `release` config and the plugin pipeline (commit-analyzer, release-notes-generator, changelog, npm, git, github), making `main` cut a version on merge, generating CHANGELOG.md, publishing to npm or creating a GitHub Release per release, doing per-package releases in a monorepo (per-package tags + paths-filter matrix), pooling commits into a less-frequent release, or fixing a release that didn't fire / a CI loop from the release commit. Covers the single-package and monorepo flavors and the GitHub Actions workflow.
SKILL.md
14.5 KB, as published. Nobody here has run it
semantic-release automation
semantic-release reads
Conventional Commits, computes the next semver
version, writes the changelog, tags, and registers a GitHub Release (and
optionally publishes to npm) — all in CI, no manual version bumps. This skill is
the tooling that consumes the commit format; read conventional-commits first for
how the bump is decided.
Copy-paste configs: templates/ — a single-package config, a
monorepo per-package config, and the GitHub Actions workflow.
How a release happens
- You merge a PR to
main(squash; the PR title is the Conventional Commit). - The workflow runs
semantic-release, which:- commit-analyzer → reads commits since the last tag, decides major/minor/patch (or no release).
- release-notes-generator → builds the notes from those commits.
- changelog → writes/updates
CHANGELOG.md. - npm (optional) → bumps
package.jsonand publishes to npm. - git → commits
CHANGELOG.md/package.jsonback aschore(release): X.Y.Z(the version tag itself is created by semantic-release core, not this plugin). - github → creates the GitHub Release (the canonical record of what shipped).
If no commit since the last release warrants a bump (only chore/docs/…), it
does nothing — correct, not a failure.
Outputs are à la carte — pick any subset
Those six steps read like one bundle, but the outputs are independent: keep only
the plugins for what you actually want. commit-analyzer + release-notes-generator
are the baseline (they compute the version and notes) — keep them; everything below
is opt-in.
| Output | Plugin(s) | Needs |
|---|---|---|
| git tag | (semantic-release core — no plugin) | — (created on every release) |
| GitHub Release | @semantic-release/github | workflow grants write permissions — the template sets contents + issues + pull-requests (the plugin's default success and failure issue/PR comments need the latter two; see the plugin docs before trimming them); default GITHUB_TOKEN then suffices — a PAT/bot token only if the Release must trigger a downstream on: release workflow |
CHANGELOG.md committed to the repo | @semantic-release/changelog + @semantic-release/git | release bot can commit to main (bypass branch protection) |
package.json version bump (no publish) | @semantic-release/npm ("npmPublish": false) + @semantic-release/git to commit it back | without @semantic-release/git the bump is made in CI then discarded — package.json in the repo stays unchanged |
| npm publish | @semantic-release/npm | NPM_TOKEN; libraries only |
The git tag always happens (core); each other output appears only if its plugin is
present — drop @semantic-release/npm and package.json is never bumped; drop
@semantic-release/git and there's no in-repo CHANGELOG.md/version commit (tag-only).
- A deployed app typically wants
CHANGELOG.md+ GitHub Release but not npm publish — drop@semantic-release/npm(or"npmPublish": falseto still bumppackage.json). A library adds npm publish on top. - If your deploy gate fires on the published GitHub Release — both the
release-event-driven deploy and the Promotion Branch gate do (
on: releasewithtypes: [published], seeproduction-release-gating) — then@semantic-release/githubis required: no Release, no deploy. (Only Vercel's build-skipignoreCommandgate needs no Release — but it matches on thechore(release): X.Y.Zcommit, so it requires@semantic-release/gitinstead; a tag-only config leaves it nothing to detect.)
Where config lives
Either a .releaserc.json at the repo/package root, or a "release" key
in package.json. Both are equivalent; pick one. Plugin order matters — it's
the execution pipeline, and npm must run before git so the bumped
package.json is what gets committed.
Flavor 1 — single package (npm or app)
Use templates/releaserc.single-package.json.
- Publishing to npm: keep
@semantic-release/npm. - Not publishing (a deployed app, or a private package): drop
@semantic-release/npm(or set["@semantic-release/npm", { "npmPublish": false }]to still bumppackage.jsonwithout publishing).
Flavor 2 — monorepo, per-package releases
Each package gets its own .releaserc.json with a package-scoped
tagFormat (my-app-v${version}) so versions/tags don't collide — see
templates/releaserc.monorepo-package.json.
The workflow detects which packages changed with dorny/paths-filter and runs
semantic-release once per changed package (a matrix), max-parallel: 1 with a
git pull --rebase retry so concurrent tag pushes don't collide.
-
Replace
my-appin bothtagFormatand the git commitmessagewith the real package name — otherwise every package shares one tag and the deploy gate can't match the scope. The template'sexecstep bumpspackage.jsoninline (no external script to create); swap in a script only if you need extra prepare steps. (It reserialisespackage.jsonwith 2-space indent — if your repo uses other formatting, use a script or a targeted replace to avoid a noisy diff.) -
The shipped
templates/release.ymlis single-package. For a monorepo, wrap that samesemantic-releasecall in adorny/paths-filter→ matrix job (max-parallel: 1+ agit pull --rebaseretry so concurrent tag pushes don't collide):jobs: detect: # which packages changed? runs-on: ubuntu-latest outputs: changed: ${{ steps.f.outputs.changes }} steps: - uses: actions/checkout@v7 - id: f uses: dorny/paths-filter@v4 with: filters: | # name each key after the package's directory apps/web: ['apps/web/**'] packages/lib: ['packages/lib/**'] release: needs: detect if: ${{ needs.detect.outputs.changed != '[]' }} strategy: max-parallel: 1 fail-fast: false matrix: pkg: ${{ fromJson(needs.detect.outputs.changed) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: { fetch-depth: 0 } # add persist-credentials: false to use GH_TOKEN below (private repos then need it wired into the git pull, e.g. a URL with the token) - uses: actions/setup-node@v6 with: { node-version: lts/*, cache: npm } - run: npm ci - working-directory: ${{ matrix.pkg }} # filter key == package dir env: { GITHUB_TOKEN: '${{ secrets.GH_TOKEN || github.token }}' } # An earlier matrix job may have pushed its release commit; rebase + retry # so this job isn't behind main when it pushes its own tag. # --repository-url (as in the single-package template) guards against an # org/repo rename desyncing package.json's repository field (EMISMATCHGITHUBURL). run: | R="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" # checkout defaults to detached HEAD on push — land on the branch so the # rebase-retry has a branch to rebase onto (and to push the tag from). git checkout "$GITHUB_REF_NAME" git pull --rebase origin "$GITHUB_REF_NAME" || true npx semantic-release --repository-url "$R" || { git pull --rebase origin "$GITHUB_REF_NAME"; npx semantic-release --repository-url "$R"; }dorny/paths-filteremitschangesas a JSON array of the matched filter keys; naming each key after the package dir letsworking-directory: ${{ matrix.pkg }}resolve. Each package uses its own.releaserc(above). -
Which package releases comes from changed file paths (paths-filter), and the bump from the commit/PR-title type. Keep a PR to one package so the squash commit maps cleanly. For strict per-package commit attribution, add
semantic-release-monorepo(it filters commits to those touching the package); plain semantic-release reads repo-wide history. -
The monorepo template's git message carries
[skip ci]and uses a[skip ci]- aware deploy gate — seeproduction-release-gating.
The GitHub Actions workflow
Use templates/release.yml. Non-negotiables:
fetch-depth: 0— semantic-release needs full history + tags.- Don't loop: the
chore(release): …commit it pushes would re-trigger the workflow. Guard withif: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }}(this template — the${{ }}wrapper is required; a bare leading!is invalid YAML) or put[skip ci]in the release commit message (the monorepo template) if your CI honours it. The template's guard also restricts theworkflow_dispatch(manual / pooled) path to the default branch, so a manual run can't accidentally cut a release from a feature branch. - Token: the built-in
GITHUB_TOKENworks for tags/Releases, but commits it makes won't trigger other workflows. If a release must kick off a downstream deploy viaon: push/on: release, use a PAT/botGH_TOKEN. (Seeproduction-release-gatingfor theon: release(types: [published]) pattern, which sidesteps this.)
Pool commits into fewer releases
Don't want a release on every merge? Keep merging to main continuously (trunk),
but change the trigger: drop on: push and run the release on
workflow_dispatch (a manual "cut a release" button) and/or a schedule:.
semantic-release batches every commit since the last tag into one larger release —
no release branch needed. For continuous prereleases, add a next/beta branch to
branches (channel releases) and fast-forward to main for the stable cut.
This is the short version. For the full treatment — the manual / scheduled /
prerelease-channel models, when a release branch is (rarely) worth it, the gotchas,
and copy-paste workflow + channel-config templates — use the dedicated
pooled-release skill (it reuses this exact pipeline;
only the trigger changes).
Gotchas
- Plugin order is the pipeline.
commit-analyzer→notes→changelog→npm→git→github.changelog,npm, and (in the monorepo flavor) theexecbump must all come beforegit—gitcommits the files they produce/bump, so a wrong order commits a staleCHANGELOG.md/package.json. EMISMATCHGITHUBURLafter an org/repo rename —package.json'srepositoryfield desyncs from the live URL. Pass--repository-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git"(the template does —GITHUB_SERVER_URLrather than a hard-codedgithub.comkeeps it working on GitHub Enterprise Server).- Nothing published? Check: was the merged commit/PR-title a releasable type
(
feat/fix/breaking, notchore)? Isfetch-depth: 0set? Is the branch inbranches? A non-conventional title silently yields no release. NPM_TOKENneeds publish rights (and 2FA set to "automation"/auth-token, not OTP) for@semantic-release/npm.- Don't hand-write
chore(release):commits — they're the bot's output. - The
gitplugin pushes the release commit straight tomain. That's by design — release automation is the one sanctioned committer tomain(it bypasses the feature-branch + PR rule that applies to humans). Ifmainhas branch protection requiring PRs/reviews, give the release token (a bot/PAT) bypass permission, or drop@semantic-release/gitand run tag-only (no changelog/version commit back — you lose the in-repoCHANGELOG.mdbump).
Verify
npx semantic-release --dry-run prints the next version and release notes
without publishing — the fastest way to confirm your config and that the
commits produce the bump you expect. Run it on a branch listed in branches
(e.g. main); on any other branch semantic-release logs "skipping" and prints no
version — pass --branches "$(git branch --show-current)" to force it on a feature
branch.
See also
conventional-commits— the input format that decides the version bump.pooled-release— want fewer, batched releases instead of one per merge? The "release train" variant — same pipeline, the trigger changes (on-demand / scheduled / prerelease channels).production-release-gating— deploy only on a real release (the GitHub Release /chore(release):commit this produces).git-trunk-branch-and-pr-automation— squash + semantic PR title that becomes the commit analysed here.
Sources
- semantic-release docs & plugin pipeline: https://semantic-release.gitbook.io/semantic-release/
- Default release rules (
angularpreset): https://github.com/semantic-release/commit-analyzer/blob/master/lib/default-release-rules.js - Patterns generalised from production repos: a published npm CLI (single-package, npm
publish,
config in package.json, no-loopifguard,--repository-urlfix) and a production monorepo (per-package.releaserc+tagFormat,dorny/paths-filtermatrix,@semantic-release/execprepare step,[skip ci]release commit).