paperclip/doc/PUBLISHING.md
Devin Foley a0f5cbffd7
Harden release flow with registry verification and dist-tag checks (#4800)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies
> - Paperclip is distributed as npm packages, including plugins like
`plugin-e2b`
> - The release process publishes canary and stable builds via npm
dist-tags
> - But there was no automated verification that published packages
actually landed with the correct dist-tags, and broken canary publishes
could silently ship to users
> - This PR adds a registry verification script that checks published
packages match their expected dist-tags, and wires it into PR CI so
regressions are caught before merge
> - The benefit is release integrity is verified automatically, and
broken dist-tag states are caught early

## What Changed

- Added `scripts/verify-release-registry-state.mjs` — verifies that
published npm packages have correct dist-tag assignments and detects
orphaned or mispointed tags
- Added `scripts/verify-release-registry-state.test.mjs` — test coverage
for the verification logic
- Updated `scripts/release.sh` to include canary dist-tag safety checks
before publishing
- Updated `.github/workflows/pr.yml` to run registry verification as a
CI step
- Updated `doc/PUBLISHING.md` and `doc/RELEASING.md` with the new
verification workflow

## Verification

- `pnpm test` — all tests pass including new verification script tests
- `node scripts/verify-release-registry-state.mjs` — runs against the
live npm registry and reports current state
- CI: the new PR workflow step runs on every PR push

## Risks

- Low risk. This is additive CI and tooling — no runtime code changes.
The registry verification is read-only (queries npm, does not publish).
The release script changes add safety checks that abort before
publishing if state is unexpected.

## Model Used

Codex GPT 5.4 high via Paperclip.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [ ] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-04-29 15:56:20 -07:00

6.6 KiB

Publishing to npm

Low-level reference for how Paperclip packages are prepared and published to npm.

For the maintainer workflow, use doc/RELEASING.md. This document focuses on packaging internals.

Current Release Entry Points

Use these scripts:

Paperclip no longer uses release branches or Changesets for publishing.

Why the CLI needs special packaging

The CLI package, paperclipai, imports code from workspace packages such as:

  • @paperclipai/server
  • @paperclipai/db
  • @paperclipai/shared
  • adapter packages under packages/adapters/

Those workspace references are valid in development but not in a publishable npm package. The release flow rewrites versions temporarily, then builds a publishable CLI bundle.

build-npm.sh

Run:

./scripts/build-npm.sh

This script:

  1. runs the forbidden token check unless --skip-checks is supplied
  2. runs pnpm -r typecheck
  3. bundles the CLI entrypoint with esbuild into cli/dist/index.js
  4. verifies the bundled entrypoint with node --check
  5. rewrites cli/package.json into a publishable npm manifest and stores the dev copy as cli/package.dev.json
  6. copies the repo README.md into cli/README.md for npm metadata

After the release script exits, the dev manifest and temporary files are restored automatically.

Package discovery and versioning

Public packages are discovered from:

  • packages/
  • server/
  • ui/
  • cli/

The version rewrite step now uses scripts/release-package-map.mjs, which:

  • finds all public packages
  • sorts them topologically by internal dependencies
  • rewrites each package version to the target release version
  • rewrites internal workspace:* dependency references to the exact target version
  • updates the CLI's displayed version string

Those rewrites are temporary. The working tree is restored after publish or dry-run.

@paperclipai/ui packaging

The UI package publishes prebuilt static assets, not the source workspace.

The ui package uses scripts/generate-ui-package-json.mjs during prepack to swap in a lean publish manifest that:

  • keeps the release-managed name and version
  • publishes only dist/
  • omits the source-only dependency graph from downstream installs

After packing or publishing, postpack restores the development manifest automatically.

Manual first publish for @paperclipai/ui

If you need to publish only the UI package once by hand, use the real package name:

  • @paperclipai/ui

Recommended flow from the repo root:

# optional sanity check: this 404s until the first publish exists
npm view @paperclipai/ui version

# make sure the dist payload is fresh
pnpm --filter @paperclipai/ui build

# confirm your local npm auth before the real publish
npm whoami

# safe preview of the exact publish payload
cd ui
pnpm publish --dry-run --no-git-checks --access public

# real publish
pnpm publish --no-git-checks --access public

Notes:

  • Publish from ui/, not the repo root.
  • prepack automatically rewrites ui/package.json to the lean publish manifest, and postpack restores the dev manifest after the command finishes.
  • If npm view @paperclipai/ui version already returns the same version that is in ui/package.json, do not republish. Bump the version or use the normal repo-wide release flow in scripts/release.sh.

If the first real publish returns npm E404, check npm-side prerequisites before retrying:

  • npm whoami must succeed first. An expired or missing npm login will block the publish.
  • For an organization-scoped package like @paperclipai/ui, the paperclipai npm organization must exist and the publisher must be a member with permission to publish to that scope.
  • The initial publish must include --access public for a public scoped package.
  • npm also requires either account 2FA for publishing or a granular token that is allowed to bypass 2FA.

Version formats

Paperclip uses calendar versions:

  • stable: YYYY.MDD.P
  • canary: YYYY.MDD.P-canary.N

Examples:

  • stable: 2026.318.0
  • canary: 2026.318.1-canary.2

Publish model

Canary

Canaries publish under the npm dist-tag canary.

Example:

  • paperclipai@2026.318.1-canary.2

This keeps the default install path unchanged while allowing explicit installs with:

npx paperclipai@canary onboard

The release script now verifies two things after a canary publish:

  • the canary dist-tag resolves to the version that was just published
  • every published internal @paperclipai/* dependency referenced by that manifest exists on npm

It also treats latest -> canary as a failure by default, because npm metadata can otherwise leave the default install path pointing at an unreleased canary dependency graph. Only pass ./scripts/release.sh canary --allow-canary-latest when that latest behavior is explicitly intended.

Stable

Stable publishes use the npm dist-tag latest.

Example:

  • paperclipai@2026.318.0

Stable publishes do not create a release commit. Instead:

  • package versions are rewritten temporarily
  • packages are published from the chosen source commit
  • git tag vYYYY.MDD.P points at that original commit

Trusted publishing

The intended CI model is npm trusted publishing through GitHub OIDC.

That means:

  • no long-lived NPM_TOKEN in repository secrets
  • GitHub Actions obtains short-lived publish credentials
  • trusted publisher rules are configured per workflow file

See doc/RELEASE-AUTOMATION-SETUP.md for the GitHub/npm setup steps.

Rollback model

Rollback does not unpublish anything.

It repoints the latest dist-tag to a prior stable version:

./scripts/rollback-latest.sh 2026.318.0

This is the fastest way to restore the default install path if a stable release is bad.