Releases are automatic. Merge to master and the release cuts itself — no button,
no manual bump. Two workflows do everything:
bump.yml— runs on every push tomaster(and still available as Actions → "Bump version" → Run workflow for a manual bump). It runs the tests, bumps every version field withauto, rotates the CHANGELOG, commitschore(release): vX.Y.Z, tagsvX.Y.Z, pushes both, and kicks off the release workflow on the new tag.release.yml— runs on anyv*tag: tests → npm publish (with provenance, ifNPM_TOKENis set) → GitHub Release with auto-generated notes.
On a push to master, bump.yml runs scripts/bump.mjs auto:
- Something shippable landed (a
feat:,fix:,perf:, or breakingtype!:/BREAKING CHANGEcommit since the last tag, or a hand-written[Unreleased]section): it bumps, rotates the CHANGELOG, tags, publishes, and cuts the Release. - Nothing shippable (only
chore/docs/test/ci/style/build/refactorcommits and an empty[Unreleased]):bump.mjsexits3and the workflow skips cleanly — no tag, no publish, CI stays green. So a docs-only or chore-only merge never spams the registry. - No hand-written notes? When
[Unreleased]is empty but shippable commits exist,bump.mjssynthesizes the CHANGELOG body from the commit subjects (feat:→Added,fix:→Fixed,perf/refactor/revert→Changed, breaking flagged), so every auto-release still describes itself. Writing your own[Unreleased]entry as you work always beats the synthesized one — do that when you can.
The bot's own chore(release): commit does not re-trigger a release (GitHub's
GITHUB_TOKEN recursion guard, plus an explicit actor/subject skip), so there's no loop.
Go to Actions → Bump version → Run workflow and pick a bump type:
| choice | effect |
|---|---|
auto |
derived from conventional commits since the last tag: BREAKING CHANGE / type!: → major, feat: → minor, anything else → patch. Falls back to the CHANGELOG [Unreleased] body (BREAKING → major, ### Added → minor, other content → patch). |
patch / minor / major |
explicit |
What happens, in order:
npm ci && npm test— a broken tree is never tagged.node scripts/bump.mjs <choice>updates every version field:package.json,package-lock.json(both fields),.claude-plugin/plugin.json,.codex-plugin/plugin.json,CITATION.cff(version + release date), the landing page footer, and moves the CHANGELOG[Unreleased]section under## [X.Y.Z] - <today>(compare links included).- Commit
chore(release): vX.Y.Z, annotated tagvX.Y.Z, push commit + tag. gh workflow run release.yml --ref vX.Y.Z. (This explicit dispatch exists because pushes made with the defaultGITHUB_TOKENintentionally do not trigger other workflows — GitHub's recursion guard. A tag pushed by a human still triggersrelease.ymlthe normal way.)
release.yml then re-runs the tests, asserts the tag matches package.json, publishes
@codewithjuber/forgekit@X.Y.Z to public npm with provenance, and creates the GitHub
Release. Verify:
- npm: https://www.npmjs.com/package/@codewithjuber/forgekit
- releases: https://github.com/CodeWithJuber/forgekit/releases
Publishing needs one repo secret:
- Create an npmjs.com account that can publish to the
@codewithjuberscope. - Generate an Automation access token (npm → Access Tokens → Generate → Automation).
- Add it as a repo secret: Settings → Secrets and variables → Actions → New repository
secret, name
NPM_TOKEN.
Soft-skip behavior: if NPM_TOKEN is missing, release.yml does not fail — it
skips the npm publish with a loud warning and still creates the GitHub Release. Add the
token later and re-run the workflow from the release tag: npm publish refuses to
overwrite an existing version, so re-runs are safe and only the missing step takes effect.
npm run bump -- patch # or minor / major / auto — edits files, prints new version
npm run bump -- auto --dry-run # compute only, write nothing
node scripts/bump.mjs check # assert all version fields agree (same guard CI runs)
npm pack --dry-run # inspect exactly what would ship to npmIf you bump locally instead of via the Actions tab, finish the job by hand — commit,
tag, push (a human-pushed tag does trigger release.yml):
V="v$(node -p "require('./package.json').version")"
git add -A && git commit -m "chore(release): $V"
git tag -a "$V" -m "$V"
git push origin master "$V"- CI version-drift guard:
node scripts/bump.mjs checkfails CI ifpackage.json,package-lock.json,.claude-plugin/plugin.json,.codex-plugin/plugin.json, orCITATION.cffdisagree about the version. - Tag/version assert:
release.ymlrefuses a tag that doesn't matchpackage.json(hand-rolled tags that skipped the bump script fail fast with a clear error). scripts/bump.mjsrefuses to rotate the CHANGELOG onto a version that already has a section. Whenautofinds nothing shippable it exits3(a graceful skip the auto-release workflow keys off), not a hard error — so a no-op merge never fails CI.
NPM_TOKEN(above) — npm publish; missing = publish skipped, release still cut.ADMIN_TOKEN— only used byrepo-settings.yml(repo description/topics/Discussions need a fine-grained PAT with Administration: write; the defaultGITHUB_TOKENcannot get that scope). Missing = that workflow skips with a warning; the equivalentghcommands are in its header comment.
patch = fixes, minor = new commands/flags (backward compatible), major = breaking
changes. Pre-1.0, breaking changes may ship in a minor. Consumers install with no
token: npm install -g @codewithjuber/forgekit.