Label-driven weekly release automation for tanem-owned repos. It derives a semver bump from the labels on the week's merged pull requests, versions and tags the repo, publishes release notes to GitHub Releases, and publishes the package to npm β with no human step.
It replaces tanem-scripts, whose
release command it supersedes.
- Reads the repo's tags and merged pull requests, and derives the bump from the labels on everything merged since the last release.
npm version <version>β the version-bump commit and itsvX.Y.Ztag, made asgithub-actions[bot], pushed together withgit push --follow-tags.- Creates the GitHub Release, with notes GitHub generates from the same
labels β see
.github/release.yml. npm publish --access public. Provenance comes from npm's trusted publishing, so no--provenanceflag and noregistry-urlare needed.
With publish: false β how this repo releases itself β steps 2 and 4 are
skipped: the run tags the checked-out commit and creates the GitHub Release,
and nothing is committed or published.
name: Release
on:
schedule:
- cron: '0 9 * * 1'
workflow_dispatch:
permissions:
contents: write
id-token: write
pull-requests: read
concurrency:
group: ${{ github.workflow }}
jobs:
release:
runs-on: ubuntu-latest
if: github.ref_name == github.event.repository.default_branch
steps:
- uses: actions/checkout@<sha>
with:
fetch-depth: 0
- uses: actions/setup-node@<sha>
with:
node-version-file: '.nvmrc'
- run: npm ci
- run: npm test
- uses: tanem/release-action@<sha>The npm ci and npm test steps are load-bearing: npm publish runs the
package's own prepublishOnly build, and the tests gate the release. The
action sets up its own Node 24 internally, so the version above is the one your
build and tests run on, not the one the release runs on.
The if: guard is load-bearing too, and it is the one line here most likely to
look redundant. The cron only ever fires on your default branch β but
workflow_dispatch can target any branch. Without the guard, dispatching the
release against a long-lived version branch bumps, tags and publishes that
branch to npm as latest, shipping the very major you were staging away from
the default branch. Narrowing your CI workflow's branch filter is not a
substitute: dispatch reaches any branch regardless of what CI builds.
It reads the default branch rather than naming one, so it is right on a master
repo and a main repo without an edit. A guard that names the branch works too
β github.ref == 'refs/heads/master' β but it is a hazard in a template: name a
branch the repo doesn't have and every release skips, silently and forever.
The push reuses the credentials actions/checkout persists, so leave
persist-credentials at its default in a publishing workflow. contents: write
covers the push and the release; id-token: write is what npm's trusted
publishing mints provenance from; pull-requests: read is how the bump gets
derived. Spelling out a permissions: block sets every scope you leave out to
none, so each of the three has to be listed β a public repo may well let the
token read pull requests without being told to, but a release that depends on
that is a release that fails on a Monday morning with nobody watching.
| Input | Default | What it does |
|---|---|---|
token |
${{ github.token }} |
Reads pull requests and tags; creates the GitHub Release. |
dry-run |
false |
Computes and logs the release this run would make, changing nothing. |
publish |
true |
Bumps, commits and publishes to npm. false tags and releases only. |
| Output | Value |
|---|---|
version |
The version released, e.g. 8.1.0. Empty when the run skipped. |
status |
released or skipped. |
A dry run reports the release it would have made, so status is released
even though nothing was.
Every merged pull request must carry exactly one release label. The strongest bump across the week's merged PRs wins:
| Label | Bump |
|---|---|
breaking |
major |
enhancement |
minor |
| any other | patch |
The safe to test label authorises CI runs and is ignored entirely β a PR
labelled only safe to test counts as unlabelled.
A PR that is unlabelled, or that carries more than one release label, fails the release run rather than guessing at a version. A week with no merged PRs is a clean skip.
The convention is hardcoded. There are no inputs to change it.
Releases are semver tags (vX.Y.Z). There is no floating major tag: consumers
pin the action by commit SHA and Renovate rolls those pins forward.
This repo dogfoods itself β a weekly cron runs its own action with publishing disabled, so a broken change surfaces here before it reaches any consumer.
Requires Node 24, which runs the TypeScript directly by stripping types. There is no build step and no runtime dependencies.
npm ci
npm test # node --test, hermetic: no network, no token
npm run typecheckTo preview any repo's next release from a laptop β read-only, and no token
ceremony beyond being signed in to the gh CLI:
GITHUB_REPOSITORY=tanem/react-svg INPUT_DRY_RUN=true node run.tsAGENTS.md has the conventions this repo holds itself to: why
there is no build step and no dependency list, the injected-seam pattern that
keeps the tests hermetic, and what is deliberately absent. Read it before
changing anything here.