Files
paperclip/doc/RELEASING.md
2026-03-12 07:36:14 -05:00

12 KiB

Releasing Paperclip

Maintainer runbook for shipping a full Paperclip release across npm, GitHub, and the website-facing changelog surface.

The release model is branch-driven:

  1. Start a release train on release/X.Y.Z
  2. Draft the stable changelog on that branch
  3. Publish one or more canaries from that branch
  4. Publish stable from that same branch head
  5. Push the branch commit and tag
  6. Create the GitHub Release
  7. Merge release/X.Y.Z back to master without squash or rebase

Release Surfaces

Every release has four separate surfaces:

  1. Verification — the exact git SHA passes typecheck, tests, and build
  2. npmpaperclipai and public workspace packages are published
  3. GitHub — the stable release gets a git tag and GitHub Release
  4. Website / announcements — the stable changelog is published externally and announced

A release is done only when all four surfaces are handled.

Core Invariants

  • Canary and stable for X.Y.Z must come from the same release/X.Y.Z branch.
  • The release scripts must run from the matching release/X.Y.Z branch.
  • Once vX.Y.Z exists locally, on GitHub, or on npm, that release train is frozen.
  • Do not squash-merge or rebase-merge a release branch PR back to master.
  • The stable changelog is always releases/vX.Y.Z.md. Never create canary changelog files.

The reason for the merge rule is simple: the tag must keep pointing at the exact published commit. Squash or rebase breaks that property.

TL;DR

1. Start the release train

Use this to compute the next version, create or resume the branch, create or resume a dedicated worktree, and push the branch to GitHub.

./scripts/release-start.sh patch

That script:

  • fetches the release remote and tags
  • computes the next stable version from the latest v* tag
  • creates or resumes release/X.Y.Z
  • creates or resumes a dedicated worktree
  • pushes the branch to the remote by default
  • refuses to reuse a frozen release train

2. Draft the stable changelog

From the release worktree:

VERSION=X.Y.Z
claude --print --output-format stream-json --verbose --dangerously-skip-permissions --model claude-opus-4-6 "Use the release-changelog skill to draft or update releases/v${VERSION}.md for Paperclip. Read doc/RELEASING.md and .agents/skills/release-changelog/SKILL.md, then generate the stable changelog for v${VERSION} from commits since the last stable tag. Do not create a canary changelog."

3. Verify and publish a canary

./scripts/release-preflight.sh canary patch
./scripts/release.sh patch --canary --dry-run
./scripts/release.sh patch --canary
PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh

Users install canaries with:

npx paperclipai@canary onboard

4. Publish stable

./scripts/release-preflight.sh stable patch
./scripts/release.sh patch --dry-run
./scripts/release.sh patch
git push public-gh HEAD --follow-tags
./scripts/create-github-release.sh X.Y.Z

Then open a PR from release/X.Y.Z to master and merge without squash or rebase.

Release Branches

Paperclip uses one release branch per target stable version:

  • release/0.3.0
  • release/0.3.1
  • release/1.0.0

Do not create separate per-canary branches like canary/0.3.0-1. A canary is just a prerelease snapshot of the same stable train.

Script Entry Points

Detailed Workflow

1. Start or resume the release train

Run:

./scripts/release-start.sh <patch|minor|major>

Useful options:

./scripts/release-start.sh patch --dry-run
./scripts/release-start.sh minor --worktree-dir ../paperclip-release-0.4.0
./scripts/release-start.sh patch --no-push

The script is intentionally idempotent:

  • if release/X.Y.Z already exists locally, it reuses it
  • if the branch already exists on the remote, it resumes it locally
  • if the branch is already checked out in another worktree, it points you there
  • if vX.Y.Z already exists locally, remotely, or on npm, it refuses to reuse that train

2. Write the stable changelog early

Create or update:

  • 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 Changes when needed
  • Highlights
  • Improvements
  • Fixes
  • Upgrade Guide when needed
  • Contributors — @-mention every contributor by GitHub username (no emails)

Package-level CHANGELOG.md files are generated as part of the release mechanics. They are not the main release narrative.

3. Run release preflight

From the release/X.Y.Z worktree:

./scripts/release-preflight.sh canary <patch|minor|major>
# or
./scripts/release-preflight.sh stable <patch|minor|major>

The preflight script now checks all of the following before it runs the verification gate:

  • the worktree is clean, including untracked files
  • the current branch matches the computed release/X.Y.Z
  • the release train is not frozen
  • the target version is still free on npm
  • the target tag does not already exist locally or remotely
  • whether the remote release branch already exists
  • whether releases/vX.Y.Z.md is present

Then it runs:

pnpm -r typecheck
pnpm test:run
pnpm build

4. Publish one or more canaries

Run:

./scripts/release.sh <patch|minor|major> --canary --dry-run
./scripts/release.sh <patch|minor|major> --canary

Result:

  • npm gets a prerelease such as 1.2.3-canary.0 under dist-tag canary
  • latest is unchanged
  • no git tag is created
  • no GitHub Release is created
  • the worktree returns to clean after the script finishes

Guardrails:

  • the script refuses to run from the wrong branch
  • the script refuses to publish from a frozen train
  • the canary is always derived from the next stable version
  • if the stable notes file is missing, the script warns before you forget it

Concrete example:

  • if the latest stable is 0.2.7, a patch canary targets 0.2.8-canary.0
  • 0.2.7-canary.N is invalid because 0.2.7 is already stable

5. Smoke test the canary

Run the actual install path in Docker:

PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh

Useful isolated variants:

HOST_PORT=3232 DATA_DIR=./data/release-smoke-canary PAPERCLIPAI_VERSION=canary ./scripts/docker-onboard-smoke.sh
HOST_PORT=3233 DATA_DIR=./data/release-smoke-stable PAPERCLIPAI_VERSION=latest ./scripts/docker-onboard-smoke.sh

If you want to exercise onboarding from the current committed ref instead of npm, use:

./scripts/clean-onboard-ref.sh
PAPERCLIP_PORT=3234 ./scripts/clean-onboard-ref.sh
./scripts/clean-onboard-ref.sh HEAD

Minimum checks:

  • npx paperclipai@canary onboard installs
  • onboarding completes without crashes
  • the server boots
  • the UI loads
  • basic company creation and dashboard load work

If smoke testing fails:

  1. stop the stable release
  2. fix the issue on the same release/X.Y.Z branch
  3. publish another canary
  4. rerun smoke testing

6. Publish stable from the same release branch

Once the branch head is vetted, run:

./scripts/release.sh <patch|minor|major> --dry-run
./scripts/release.sh <patch|minor|major>

Stable publish:

  • publishes X.Y.Z to npm under latest
  • creates the local release commit
  • creates the local tag vX.Y.Z

Stable publish refuses to proceed if:

  • the current branch is not release/X.Y.Z
  • the remote release branch does not exist yet
  • the stable notes file is missing
  • the target tag already exists locally or remotely
  • the stable version already exists on npm

Those checks intentionally freeze the train after stable publish.

7. Push the stable branch commit and tag

After stable publish succeeds:

git push public-gh HEAD --follow-tags
./scripts/create-github-release.sh X.Y.Z

The GitHub Release notes come from:

  • releases/vX.Y.Z.md

8. Merge the release branch back to master

Open a PR:

  • base: master
  • head: release/X.Y.Z

Merge rule:

  • allowed: merge commit or fast-forward
  • forbidden: squash merge
  • forbidden: rebase merge

Post-merge verification:

git fetch public-gh --tags
git merge-base --is-ancestor "vX.Y.Z" "public-gh/master"

That command must succeed. If it fails, the published tagged commit is not reachable from master, which means the merge strategy was wrong.

9. Finish the external surfaces

After GitHub is correct:

  • publish the changelog on the website
  • write and send the announcement copy
  • ensure public docs and install guidance point to the stable version

GitHub Actions Release

There is also a manual workflow at .github/workflows/release.yml.

Use it from the Actions tab on the relevant release/X.Y.Z branch:

  1. Choose Release
  2. Choose channel: canary or stable
  3. Choose bump: patch, minor, or major
  4. Choose whether this is a dry_run
  5. Run it from the release branch, not from master

The workflow:

  • reruns typecheck, test:run, and build
  • gates publish behind the npm-release environment
  • can publish canaries without touching latest
  • can publish stable, push the stable branch commit and tag, and create the GitHub Release

It does not merge the release branch back to master for you.

Release Checklist

Before any publish

  • The release train exists on release/X.Y.Z
  • The working tree is clean, including untracked files
  • If package manifests changed, the CI-owned pnpm-lock.yaml refresh is already merged on master before the train is cut
  • The required verification gate passed on the exact branch head you want to publish
  • The bump type is correct for the user-visible impact
  • The stable changelog file exists or is ready at releases/vX.Y.Z.md
  • You know which previous stable version you would roll back to if needed

Before a stable

  • The candidate has already passed smoke testing
  • The remote release/X.Y.Z branch exists
  • You are ready to push the stable branch commit and tag immediately after npm publish
  • You are ready to create the GitHub Release immediately after the push
  • You are ready to open the PR back to master

After a stable

  • npm view paperclipai@latest version matches the new stable version
  • The git tag exists on GitHub
  • The GitHub Release exists and uses releases/vX.Y.Z.md
  • vX.Y.Z is reachable from master
  • The website changelog is updated
  • Announcement copy matches the stable release, not the canary

Failure Playbooks

If the canary publishes but the smoke test fails

Do not publish stable.

Instead:

  1. fix the issue on release/X.Y.Z
  2. publish another canary
  3. rerun smoke testing

If stable npm publish succeeds but push or GitHub release creation fails

This is a partial release. npm is already live.

Do this immediately:

  1. fix the git or GitHub issue from the same checkout
  2. push the stable branch commit and tag
  3. create the GitHub Release

Do not republish the same version.

If latest is broken after stable publish

Preview:

./scripts/rollback-latest.sh X.Y.Z --dry-run

Roll back:

./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.

Then fix forward with a new patch release.

If the GitHub Release notes are wrong

Re-run:

./scripts/create-github-release.sh X.Y.Z

If the release already exists, the script updates it.