Releasing speckit-eye
Releasing speckit-eye
A release takes three actions from the maintainer: start Bump version,
merge the pull request it opens, and approve the release. Everything else,
the tag, the checks, the npm publish and the GitHub release, is done by
GitHub Actions. Nobody runs npm publish or types a tag in the normal
process; npm publish from a working copy is refused by the package’s
prepublishOnly script.
Actions → Bump version (on main)
│ release.js bump: package.json version + CHANGELOG.md section
▼
PR "Release vX.Y.Z" from release/next ── CI runs on it
│ review, merge
▼
Release workflow (push to main)
├─ prepare is this a merge from release/next with a new version?
├─ ci full unit + E2E suites on the release commit
├─ verify on main · tag = package.json = changelog · not on npm yet
│ · pnpm pack + package contents check · release notes
▼
approval environment "npm": the repository owner reviews the run
▼
publish create and push tag vX.Y.Z · npm publish the checked tarball
│ with provenance, dist-tag latest or next
▼
github-release vX.Y.Z with the changelog section as notes
The pieces:
scripts/release/release.js: the rules (next version, changelog rewrite, checks). It runs locally too, for examplenode scripts/release/release.js notes --version 1.0.0..github/workflows/bump.yml: Bump version, opens or updates the release pull request..github/workflows/release.yml: Release, checks, tags, publishes and creates the GitHub release.
One-time setup
Do these once, before the first release. All of them are repository or npm settings; none of them is stored in the code.
- The npm name.
npm view speckit-eyemust answerE404(the name is free) or show a package you own. If someone else has taken it, see The npm name is taken. - The
npmenvironment. Settings → Environments → New environmentnpm:- Required reviewers: the repository owner only. Leave “Prevent self-review” off, so the owner can approve releases they started. Adding more reviewers later changes nothing else in the process.
- Deployment branches and tags: “Selected branches and tags”, add the
branch rule
mainand the tag rulev*.
- The first-release token. On npmjs.com → Access Tokens → Generate New
Token → Granular Access Token: packages and scopes “Read and write”,
expiration 1 day. Add it as the environment secret
NPM_TOKENof thenpmenvironment (not as a repository secret), so only an approved release job can read it. You delete it right after the first release (see The first release). If npm lets you configure a trusted publisher for a package that does not exist yet by the time you release, skip the token and configure the trusted publisher now instead. - Pull requests from Actions. Settings → Actions → General → Workflow permissions: turn on “Allow GitHub Actions to create and approve pull requests”. Bump version needs it to open the release pull request.
- GitHub Pages. Settings → Pages → Build and deployment → Source: “GitHub Actions”.
- Protect the version tags. Settings → Rules → Rulesets → New tag
ruleset: target
v*, enforcement “Active”, rules “Restrict updates” and “Restrict deletions”. A published version’s tag can then not be moved or removed. - Immutable releases. Settings → General → Releases: turn on immutable releases, so a published GitHub release and its assets cannot be changed.
- Private vulnerability reporting. Settings → Security → turn on “Private vulnerability reporting”.
Cutting a release
Check that
## [Unreleased]inCHANGELOG.mdlists what the release contains. Every user-visible change adds its line there when it is merged; Bump version refuses to run on an empty Unreleased section.Actions → Bump version → Run workflow, on branch
main. Choose the kind:Kind From 1.2.3Use for patch1.2.4fixes only minor1.3.0new features, nothing breaks major2.0.0breaking changes explicitthe version you type the first release ( 1.0.0), or a version the kinds cannot expressThe run updates
versioninpackage.json, moves the Unreleased entries under## [X.Y.Z] - <today>, rewrites the comparison links at the bottom of the changelog, commitschore(release): vX.Y.Zto the branchrelease/nextand opens the pull request Release vX.Y.Z, with the release notes as its description, and starts CI on it. Running it again before merging replaces the branch and updates the same pull request.Review the pull request: the version, the dated changelog section and the green CI checks. Merge it.
The merge starts Release.
prepare,ciandverifyrun by themselves; then thepublishjob waits for approval. Open the run, choose “Review deployments”, ticknpmand approve. Every check has already passed at this point.After approval the run creates the annotated tag
vX.Y.Zon the merge commit, publishes the tarball thatverifychecked, and creates the GitHub release. Then verify the release.
Cutting a pre-release
Use a pre-release to let people try a version before it becomes the default install. In Bump version, choose one of:
| Kind | From 1.2.3 |
From 1.3.0-rc.1 |
|---|---|---|
prepatch |
1.2.4-rc.0 |
1.3.1-rc.0 |
preminor |
1.3.0-rc.0 |
1.4.0-rc.0 |
premajor |
2.0.0-rc.0 |
2.0.0-rc.0 |
prerelease |
1.2.4-rc.0 |
1.3.0-rc.2 |
preid (default rc) names the pre-release, for example beta gives
1.3.0-beta.0; prerelease with another id than the current one starts again
at .0. Merge and approve as for a normal release.
A version with a - is published under the npm dist-tag next, never
latest, and its GitHub release is marked as a pre-release. npx speckit-eye
keeps installing the latest stable version. To try the pre-release:
npx speckit-eye@next --serve .
npx speckit-eye@1.3.0-rc.0 --serve . # or name the exact version
To finish a pre-release, bump with patch, minor or major: from
1.3.0-rc.1, patch and minor give 1.3.0, major gives 2.0.0.
The first release
The first release publishes 1.0.0, the version already in package.json.
- Finish the one-time setup, including the
NPM_TOKENenvironment secret. - Actions → Bump version, kind
explicit, version1.0.0. The version does not change; the changelog gets its## [1.0.0]section. - Merge the pull request and approve the release as usual. The npm CLI
uses
NPM_TOKENbecause no trusted publisher exists yet. - Right after
1.0.0is on npm, switch to trusted publishing, so no publish credential is stored anywhere:- On npmjs.com → the
speckit-eyepackage → Settings → Trusted Publisher → GitHub Actions: organization or userSerafAC, repositoryspeckit-eye, workflow filenamerelease.yml, environmentnpm. - In the same settings, Publishing access: “Require two-factor authentication and disallow tokens”.
- Revoke the granular token on npmjs.com → Access Tokens.
- Delete the
NPM_TOKENsecret from thenpmenvironment.
- On npmjs.com → the
The workflow does not change: the npm CLI tries trusted publishing (OIDC)
first and uses the token only when there is no trusted publisher. From the
second release on, publishing uses a short-lived identity token issued to the
approved publish job.
Verifying a release
After the run is green, check that everything names the same version:
npm view speckit-eye version # X.Y.Z (a stable release)
npm view speckit-eye dist-tags # latest: X.Y.Z, and next: for a pre-release
git fetch --tags && git show vX.Y.Z --no-patch # the tag is on the merge commit
- On
https://www.npmjs.com/package/speckit-eye, the version shows a Provenance section that links to therelease.ymlrun and the commit it was built from. - In any project that installed the package,
npm audit signaturesreports verified registry signatures and attestations forspeckit-eye. - The GitHub release
vX.Y.Zexists and its notes are the version’s changelog section (marked as a pre-release for a-version). CHANGELOG.mdonmainhas an empty## [Unreleased], the## [X.Y.Z] - <date>section and the updated links at the bottom.npx speckit-eye@X.Y.Z --versionprintsX.Y.Z.
When something goes wrong
Nothing is tagged or published before the approval, so a failure before it leaves nothing to clean up.
A check failed
A ci or verify step failed. The run stops before the approval
and nothing is published. The failing step names the check, for example
tag v1.2.0 does not match package.json version 1.1.0 or
speckit-eye@1.2.0 is already on npm.
- A flaky test or an outage: open the run and choose “Re-run failed jobs”.
- A real problem in the code: fix it with a normal pull request to
main. The version is not out yet, so if the fix is user-visible, add its entry to the## [X.Y.Z]section, not to Unreleased. Then release the fixed commit by pushing its tag by hand (see below).
The approval was rejected
Nothing was tagged or published. To release the same commit later, open the rejected run and choose “Re-run all jobs”, or push the tag by hand.
The publish succeeded but the GitHub release failed
Open the run and choose “Re-run failed jobs”. Only github-release runs
again; the publish is never repeated.
A tag pushed by hand
Pushing a v* tag starts Release on that tag,
with every check: the commit must be on main, package.json and the newest
changelog section must name the tag’s version, and the version must not be
on npm. This is also the way to release a commit when the automatic run
did not:
git switch main && git pull
git tag -a vX.Y.Z -m vX.Y.Z # on the commit to release
git push origin vX.Y.Z
A tag pushed by mistake publishes nothing unless you approve it: reject the run. The tag ruleset blocks deleting it; to remove it, disable the ruleset, delete the tag, and enable the ruleset again.
Re-running a release
Actions → Release → Run workflow, and under
“Use workflow from” choose the tag vX.Y.Z (the workflow refuses to run on a
branch). It runs every check again, so a version that is already on npm
fails in verify.
Rehearsing a release
There is no dry-run mode. The rehearsal is the real run up to the approval step: all checks have run by then, and rejecting the approval publishes nothing and leaves no tag.
A broken version was published
npm versions cannot be reused, even after an unpublish, so never unpublish and never republish the same number. Deprecate the version, then ship a fixed patch release:
npm deprecate speckit-eye@X.Y.Z "Broken: <reason>. Use X.Y.(Z+1) or later."
Anyone installing that exact version then sees the message. If the broken
version is the latest dist-tag, the patch release moves latest on.
The npm name is taken
If npm view speckit-eye shows a package you do
not own, the release fails in verify (the version is already on npm) or in
publish (no permission), and nothing is published. Choose another name, for example the scoped @serafac/speckit-eye:
change name in package.json and the npx commands in README.md and
docs/, add a changelog entry, and release again.
Why it is safe
- Only a reviewed merge releases (FR-009). Release publishes only a
commit merged from
release/nextwhose tag does not exist yet, or av*tag; the commit must be onmain. A hand-edited version in any other pull request does not release. The tag is created by the release run itself. - Versions agree and are new (FR-010). The tag,
package.jsonand the newest changelog section must name the same version, the section must have entries, and the version must not exist on npm. - What is tested is what is published (FR-011, FR-017). The full unit and
E2E suites run on the release commit; the tarball is packed once, its file
list is checked, and exactly that file is published. Every install uses the
committed lockfile (
pnpm install --frozen-lockfile). - A person approves every publish (FR-012). The
publishjob runs in thenpmenvironment, whose only required reviewer is the repository owner, and only after every check has passed. - No stored publish credential (FR-013). Publishing uses npm trusted publishing with a short-lived identity token; the package carries verifiable build provenance. The one-day token of the first release is revoked right after it.
- Least privilege (FR-014). Every workflow starts with no permissions and
each job gets only what it needs (
id-token: writeonly inpublish). Third-party actions are pinned to full commit SHAs. - Release notes come from the changelog (FR-015). The GitHub release notes are the version’s changelog section.
- Pre-releases never become the default install (FR-016). The dist-tag is
derived from the version:
nextfor a version with a-, elselatest. - Published history is fixed. Tag rules stop
v*tags from being moved or deleted, immutable releases stop GitHub releases from being edited, and npm never reuses a version number.