12 KiB
Releasing Paperclip
Maintainer runbook for shipping a full Paperclip release across npm, GitHub, and the website-facing changelog surface.
This document is intentionally practical:
- TL;DR command sequences are at the top.
- Detailed checklists come next.
- Motivation, failure handling, and rollback playbooks follow after that.
Release Surfaces
Every Paperclip release has four separate surfaces:
- Verification — the exact git SHA must pass typecheck, tests, and build.
- npm —
paperclipaiand the public workspace packages are published. - GitHub — the stable release gets a git tag and a GitHub Release.
- Website / announcements — the stable changelog is published externally and announced.
Treat those as related but separate. npm can succeed while the GitHub Release is still pending. GitHub can be correct while the website changelog is stale. A maintainer release is done only when all four surfaces are handled.
TL;DR
Canary release
Use this when you want an installable prerelease without changing latest.
# 0. Start clean
git status --short
# 1. Verify the candidate SHA
pnpm -r typecheck
pnpm test:run
pnpm build
# 2. Draft or update the stable changelog
# releases/vX.Y.Z.md
# 3. Preview the canary release
./scripts/release.sh patch --canary --dry-run
# 4. Publish the canary
./scripts/release.sh patch --canary
# 5. Smoke test what users will actually install
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
# Users install with:
npx paperclipai@canary onboard
Result:
- npm gets a prerelease such as
1.2.3-canary.0under dist-tagcanary latestis unchanged- no git tag is created
- no GitHub Release is created
- the working tree returns to clean after the script finishes
Stable release
Use this only after the canary SHA is good enough to become the public default.
# 0. Start from the vetted commit
git checkout master
git pull
git status --short
# 1. Verify again on the exact release SHA
pnpm -r typecheck
pnpm test:run
pnpm build
# 2. Confirm the stable changelog exists
ls releases/v*.md
# 3. Preview the stable publish
./scripts/release.sh patch --dry-run
# 4. Publish the stable release to npm and create the local release commit + tag
./scripts/release.sh patch
# 5. Push the release commit and tag
git push origin HEAD:master --follow-tags
# 6. Create or update the GitHub Release from the pushed tag
./scripts/create-github-release.sh X.Y.Z
Result:
- npm gets stable
X.Y.Zunder dist-taglatest - a local git commit and tag
vX.Y.Zare created - after push, GitHub gets the matching Release
- the website and announcement steps still need to be handled manually
Emergency rollback
If latest is broken after publish, repoint it to the last known good stable version first, then work on the fix.
# Preview
./scripts/rollback-latest.sh X.Y.Z --dry-run
# Roll back latest for every public package
./scripts/rollback-latest.sh X.Y.Z
This does not unpublish anything. It only moves the latest dist-tag back to the last good stable release.
GitHub Actions release
There is also a manual workflow at .github/workflows/release.yml. It is designed for npm trusted publishing via GitHub OIDC instead of long-lived npm tokens.
Use it from the Actions tab:
- Choose
Release - Choose
channel:canaryorstable - Choose
bump:patch,minor, ormajor - Choose whether this is a
dry_run - Run it from
master
The workflow:
- reruns
typecheck,test:run, andbuild - gates publish behind the
npm-releaseenvironment - can publish canaries without touching
latest - can publish stable, push the release commit and tag, and create the GitHub Release
Release Checklist
Before any publish
- The working tree is clean, including untracked files
- The target branch and SHA are the ones you actually want to release
- The required verification gate passed on that exact SHA
- The bump type is correct for the user-visible impact
- The stable changelog file exists or is ready to be written at
releases/vX.Y.Z.md - You know which previous stable version you would roll back to if needed
Before a canary
- You are intentionally testing something that should be installable before it becomes default
- You are comfortable with users installing it via
npx paperclipai@canary onboard - You understand that each canary is a new immutable npm version such as
1.2.3-canary.1
Before a stable
- The candidate has already passed smoke testing
- The changelog should be the stable version only, for example
v1.2.3 - You are ready to push the release commit and tag immediately after npm publish
- You are ready to create the GitHub Release immediately after the push
- You have a post-release website / announcement plan
After a stable
npm view paperclipai@latest versionmatches the new stable version- The git tag exists on GitHub
- The GitHub Release exists and uses
releases/vX.Y.Z.md - The website changelog is updated
- Any announcement copy matches the shipped release, not the canary
Verification Gate
The repository standard is:
pnpm -r typecheck
pnpm test:run
pnpm build
This matches .github/workflows/pr-verify.yml. Run it before claiming a release candidate is ready.
Versioning Policy
Stable versions
Stable releases use normal semver:
patchfor bug fixesminorfor additive features, endpoints, and additive migrationsmajorfor destructive migrations, removed APIs, or other breaking behavior
Canary versions
Canaries are semver prereleases of the intended stable version:
1.2.3-canary.01.2.3-canary.11.2.3-canary.2
That gives you three useful properties:
- Users can install the prerelease explicitly with
@canary lateststays safe- The stable changelog can remain just
v1.2.3
We do not create separate changelog files for canary versions.
Changelog Policy
The maintainer changelog source of truth is:
releases/vX.Y.Z.md
That file is for the eventual stable release. It should not include -canary in the filename or heading.
Recommended structure:
Breaking Changeswhen neededHighlightsImprovementsFixesUpgrade Guidewhen needed
Package-level CHANGELOG.md files are generated as part of the release mechanics. They are not the main release narrative.
Detailed Workflow
1. Decide the bump
Review the range since the last stable tag:
LAST_TAG=$(git tag --list 'v*' --sort=-version:refname | head -1)
git log "${LAST_TAG}..HEAD" --oneline --no-merges
git diff --name-only "${LAST_TAG}..HEAD" -- packages/db/src/migrations/
git diff "${LAST_TAG}..HEAD" -- packages/db/src/schema/
git log "${LAST_TAG}..HEAD" --format="%s" | grep -E 'BREAKING CHANGE|BREAKING:|^[a-z]+!:' || true
Use the higher bump if there is any doubt.
2. Write the stable changelog first
Create or update:
releases/vX.Y.Z.md
This is deliberate. The release notes should describe the stable story, not the canary mechanics.
3. Publish one or more canaries
Run:
./scripts/release.sh <patch|minor|major> --canary
What the script does:
- Verifies the working tree is clean
- Computes the intended stable version from the last stable tag
- Computes the next canary ordinal from npm
- Versions the public packages to
X.Y.Z-canary.N - Builds the workspace and publishable CLI
- Publishes to npm under dist-tag
canary - Cleans up the temporary versioning state so your branch returns to clean
This means the script is safe to repeat as many times as needed while iterating:
1.2.3-canary.01.2.3-canary.11.2.3-canary.2
The target stable release can still remain 1.2.3.
4. Smoke test the canary
Run the actual install path in Docker:
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
Minimum checks:
npx paperclipai@canary onboardinstalls- onboarding completes without crashes
- the server boots
- the UI loads
- basic company creation and dashboard load work
5. Publish stable from the vetted commit
Once the candidate SHA is good, run the stable flow on that exact commit:
./scripts/release.sh <patch|minor|major>
What the script does:
- Verifies the working tree is clean
- Versions the public packages to the stable semver
- Builds the workspace and CLI publish bundle
- Publishes to npm under
latest - Restores temporary publish artifacts
- Creates the local release commit and git tag
What it does not do:
- it does not push for you
- it does not update the website
- it does not announce the release for you
6. Push the release and create the GitHub Release
After a stable publish succeeds:
git push origin HEAD:master --follow-tags
./scripts/create-github-release.sh X.Y.Z
The GitHub release notes come from:
releases/vX.Y.Z.md
7. Complete the external surfaces
After GitHub is correct:
- publish the changelog on the website
- write the announcement copy
- ensure public docs and install guidance point to the stable version
GitHub Actions and npm Trusted Publishing
If you want GitHub to own the actual npm publish, use .github/workflows/release.yml together with npm trusted publishing.
Recommended setup:
- Configure the GitHub Actions workflow as a trusted publisher for every public package on npm
- Use the
npm-releaseGitHub environment with required reviewers - Run stable publishes from
masteronly - Keep the workflow manual via
workflow_dispatch
Why this is the right shape:
- no long-lived npm token needs to live in GitHub secrets
- reviewers can approve the publish step at the environment gate
- the workflow reruns verification on the release SHA before publish
- stable and canary use the same mechanics
Failure Playbooks
If the canary fails before publish
Nothing shipped. Fix the code and rerun the canary workflow.
If the canary publishes but the smoke test fails
Do not publish stable.
Instead:
- Fix the issue
- Publish another canary
- Re-run smoke testing
The canary version number will increase, but the stable target version can remain the same.
If the stable npm publish succeeds but push fails
This is a partial release. npm is already live.
Do this immediately:
- Fix the git issue
- Push the release commit and tag from the same checkout
- Create the GitHub Release
Do not publish the same version again.
If the stable release is bad after latest moves
Use the rollback script first:
./scripts/rollback-latest.sh <last-good-version>
Then:
- open an incident note or maintainer comment
- fix forward on a new patch release
- update the changelog / release notes if the user-facing guidance changed
If the GitHub Release is wrong
Edit it by re-running:
./scripts/create-github-release.sh X.Y.Z
This updates the release notes if the GitHub Release already exists.
If the website changelog is wrong
Fix the website independently. Do not republish npm just to repair the website surface.
Rollback Strategy
The default rollback strategy is dist-tag rollback, then fix forward.
Why:
- npm versions are immutable
- users need
npx paperclipai onboardto recover quickly - moving
latestback is faster and safer than trying to delete history
Rollback procedure:
- identify the last known good stable version
- run
./scripts/rollback-latest.sh <version> - verify
npm view paperclipai@latest version - fix forward with a new stable release
Scripts Reference
scripts/release.sh— stable and canary npm publish flowscripts/create-github-release.sh— create or update the GitHub Release after pushscripts/rollback-latest.sh— repointlatestto the last good stable releasescripts/docker-onboard-smoke.sh— Docker smoke test for the installed CLI
Related Docs
- doc/PUBLISHING.md — low-level npm build and packaging internals
- skills/release/SKILL.md — agent release coordination workflow
- skills/release-changelog/SKILL.md — stable changelog drafting workflow