agmsg's version lives in one place: the VERSION file at the
repo root. The two files that also carry the version string — package.json
(npm) and .claude-plugin/plugin.json (Claude Code plugin marketplace) — are
derived from it via scripts/release/sync-version.sh.
The npm package agmsg is published directly from this repo via npm's
Trusted Publisher (OIDC) binding — there is no NPM_TOKEN to leak.
(Earlier releases came from a separate fujibee/agmsg-npm bootstrapper
repo; that repo is now archived — see "History" below.)
One command does everything:
scripts/release/cut-release.sh 1.0.4 # semver, no leading "v"It bumps VERSION, syncs the derived files, regenerates CHANGELOG.md from
Conventional Commits (via git-cliff), opens a
release: <version> PR, auto-merges it once the required checks pass, then tags
the merged commit and pushes the tag.
Why a PR and not a direct push: main is a protected branch with required
status checks, so the release commit must land through a PR — a direct push is
rejected. Tags aren't protected, so the tag push is direct.
Prerequisite: install git-cliff once — brew install git-cliff (or
cargo install git-cliff, or grab a binary from the
releases). The changelog format
is configured in cliff.toml.
The tag push fires .github/workflows/release.yml,
which:
- Verifies the tag matches
VERSIONand that derived files are in sync (sync-version.sh --check). - Waits for a reviewer to approve the
productionenvironment. - Runs
npm publish --access public --provenance. - Generates the release notes for the tag with git-cliff and creates a GitHub Release from them.
# On an up-to-date main, on a release branch:
git switch -c release/v1.0.4
echo 1.0.4 > VERSION
./scripts/release/sync-version.sh
git-cliff --tag v1.0.4 -o CHANGELOG.md
git add VERSION package.json .claude-plugin/plugin.json CHANGELOG.md
git commit -m "release: 1.0.4"
git push -u origin release/v1.0.4
gh pr create --fill && gh pr merge --squash --auto --delete-branch
# After it merges:
git switch main && git pull --ff-only
git tag v1.0.4 && git push origin v1.0.4# (after the release commit is on main via PR)
npm publish --access public --provenance
git-cliff --latest --strip header -o RELEASE_NOTES.md
gh release create "v$(cat VERSION)" --title "v$(cat VERSION)" --notes-file RELEASE_NOTES.mdThe pipeline layers four defenses against silent drift and malicious publish:
- npm Trusted Publisher (OIDC). npmjs.com only accepts a publish from a
GitHub Actions run that proves (via OIDC) it was triggered from this repo,
this workflow file, and the
productionenvironment. There is no long-livedNPM_TOKENto steal. Package settings on npmjs.com are also set to require 2FA and disallow tokens, so the only publish path is this workflow. productionenvironment with required reviewer. A pushed tag pauses at the publish step until a maintainer approves the deployment. A compromised tag-push alone cannot ship to npm.--provenanceattestation. Every published tarball is signed by GitHub and linked back to this workflow run. A tarball without provenance — or with provenance pointing elsewhere — is distinguishable on npmjs.com.verify-versions.yml. Runssync-version.sh --checkon every push and PR tomain. A hand-edit ofpackage.jsonorplugin.jsonwithout aVERSIONbump fails CI before merge.
None — auth to npm is via OIDC.
The Trusted Publisher binding on npmjs.com keys off three things that all must match:
| Field | Value |
|---|---|
| Repository | fujibee/agmsg |
| Workflow filename | release.yml |
| Environment | production |
If any of these is renamed, update the npm Trusted Publisher settings in lockstep.
VERSION must be semver (MAJOR.MINOR.PATCH[-prerelease]). sync-version.sh
rejects anything else, including a leading v. The tag is always
v$(cat VERSION).
The npm agmsg package was originally published from a separate repo,
fujibee/agmsg-npm, during the
name-registration sprint (issue #80). That repo only contained a thin
JavaScript bootstrapper that downloaded and ran setup.sh from this repo.
Keeping it separate added a cross-repo sync surface and bought nothing,
so it was folded back here. The bootstrapper now lives at bin/agmsg.js.