A release starts from a tested main commit and ends with a maintainer approving a
staged package on npm with 2FA. Do not create or move a version tag by hand, and do
not run npm publish from a workstation.
Two workflows do the work:
| Workflow | Trigger | Authority |
|---|---|---|
Build & Release (build.yml) |
dispatch on main |
pushes the signed release commit + tag |
Stage npm Release (publish.yaml) |
dispatched at that tag | stages one tarball on npm |
Only publish.yaml's stage job can publish. It checks out nothing, installs no
project dependencies, runs no third-party action, and holds no repository secret.
- The intended commit is on
mainand its CI run is green. CHANGELOG.mdhas the release notes. Run/preflightfirst if upstream Node.js or SQLite sources need syncing.package.json'sversionis untouched —build.ymlbumps it.- npm's Trusted Publisher for
@photostructure/sqlitenamesphotostructure/node-sqliteandpublish.yaml. - Actions can read
SSH_SIGNING_KEY,GIT_USER_NAME, andGIT_USER_EMAIL.
- Open Build & Release in GitHub Actions and Run workflow on
main. - Choose
patch,minor, ormajor. - Wait for lint, the eight prebuilds, the test matrix, and the packed-package checks.
The release job then creates one signed commit and one signed
vX.Y.Zannotated tag, pushes them atomically, and starts Stage npm Release at that tag. - Wait for Stage npm Release. It rebuilds all eight prebuilds from the tag, packs one tarball, installs and loads it on Linux, macOS, Windows, and Alpine across Node.js 22, 24, and 26, and stages that exact tarball on npm.
- Download the
npm-package-vX.Y.Zartifact from the run. Check thatCONTENTS.txtlists all eight prebuilds and thatPACK.jsonnames the tag's version. - Open Staged Packages from the npm user menu.
- Confirm the package name, version, file list, and that provenance identifies this
repository,
publish.yaml, the release tag, and the tag's target commit. - Approve with 2FA, then confirm npm lists the version publicly.
- Confirm the immutable GitHub release exists for the same tag.
The CLI works too, and still requires 2FA: npm stage list, npm stage view <id>,
then npm stage approve <id> or npm stage reject <id>.
The GitHub release is created once npm accepts the stage, so it can exist before the package is publicly visible.
build.ymlpacks, installs, and loads a tarball on every push tomain.publish.yamlis frozen at the tag it runs from, so that procedure must never run for the first time during a release.- The release job changes only
package.jsonandpackage-lock.json, signs the commit and tag, and pushes them atomically. It installs no dependencies. publish.yamlaccepts only a signedvMAJOR.MINOR.PATCHannotated tag whose package name, repository URL, version, and target commit match the workflow ref. It rechecks that identity in the staging job and again before creating the release.- All eight native binaries are rebuilt from the tag, and
packrefuses to package unless exactly those eight are present -- a missing one would silently push consumers on that platform into compiling from source.assertPackedContentsthen confirms each one made it into the tarball. - Artifact integrity between jobs is
actions/download-artifact's job: it hashes every artifact and fails ondigest-mismatchby default. - The packed tarball is installed with
--ignore-scripts, so the load check proves the packed prebuild resolves rather than a local source build.
| Failure | Response |
|---|---|
| A pre-tag job fails | Fix main and dispatch again. No tag exists yet. |
main moved during the run |
Dispatch again from the new head. |
| Tag or signature validation fails | Correct the release identity. Do not bypass validation. |
| The tag exists but dispatch failed | Rerun the dispatch job, or run publish.yaml at that exact tag. Do not bump again. |
| A tagged build or package check fails | Fix main and release a new version. Never move the tag. |
| The staged package is wrong | Reject the stage and release a new version. |
| An approved release is bad | Deprecate it or publish a corrected version. Never overwrite it. |
For the first release following any change to build.yml, publish.yaml, or the
scripts/release-* tooling, record:
- both workflow run URLs;
- the signed tag and its target commit SHA;
- the staged-package approval time;
- the npm package and GitHub release URLs; and
npm view @photostructure/sqlite@X.Y.Z version gitHead dist.integrity --json.
gitHead must equal the release tag's target commit.