## Thinking Path > - Paperclip is a control plane for autonomous agent companies, so its release automation is part of the core operator trust boundary. > - The affected subsystem is npm/GitHub Actions release publishing for the public monorepo packages. > - The concrete failure was that a newly added package reached `master`, the canary workflow attempted its first publish, and npm trusted publishing was not yet bootstrapped for that package. > - That means the problem is not just one broken run; it is a missing pre-merge guard that lets release-ineligible packages land and only fail once `publish_canary` runs. > - This pull request makes release enrollment explicit, validates that enrollment in CI, and adds a PR-time bootstrap check against npm for changed release-enabled package manifests. > - The result is that we keep trusted publishing, avoid teaching CI to `npm adduser`, and move this class of failure from post-merge canary time to pre-merge review time. ## What Changed - Added `scripts/release-package-manifest.json` so release-managed public packages are explicitly enrolled instead of being inferred from every non-private workspace package. - Hardened `scripts/release-package-map.mjs` to validate the manifest before release workflows rewrite versions or assemble publish payloads. - Added `scripts/check-release-package-bootstrap.mjs` and wired it into `.github/workflows/pr.yml` so PRs that change a release-enabled package manifest fail if that package does not already exist on npm. - Added release-package manifest coverage tests to `scripts/release-package-map.test.mjs` and included them in `pnpm run test:release-registry`. - Wired manifest validation into `.github/workflows/release.yml` and documented the first-publish bootstrap policy in `doc/PUBLISHING.md` and `doc/RELEASE-AUTOMATION-SETUP.md`. ## Verification - `pnpm run test:release-registry` - `./scripts/release.sh canary --skip-verify --dry-run` - Confirmed the committed diff contains no obvious PII/secrets via targeted pattern scan before pushing. ## Risks - Low risk overall: this is CI/release-policy code, not product runtime logic. - The new PR bootstrap check depends on npm metadata availability, so a transient npm outage could block a PR that changes a release-enabled package manifest. - The manifest introduces a new source of truth that must stay aligned with public package additions, but that is intentional and now enforced. ## Model Used - OpenAI Codex via the `codex_local` Paperclip adapter; GPT-5-based coding agent with tool use, terminal execution, git, and GitHub CLI. Exact served model ID/context window are not exposed by the local runtime. ## 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 - [x] 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
9.3 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:
scripts/release.shfor canary and stable publish flowsscripts/create-github-release.shafter pushing a stable tagscripts/rollback-latest.shto repointlatestscripts/build-npm.shfor the CLI packaging build
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:
- runs the forbidden token check unless
--skip-checksis supplied - runs
pnpm -r typecheck - bundles the CLI entrypoint with esbuild into
cli/dist/index.js - verifies the bundled entrypoint with
node --check - rewrites
cli/package.jsoninto a publishable npm manifest and stores the dev copy ascli/package.dev.json - copies the repo
README.mdintocli/README.mdfor 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
nameandversion - 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. prepackautomatically rewritesui/package.jsonto the lean publish manifest, andpostpackrestores the dev manifest after the command finishes.- If
npm view @paperclipai/ui versionalready returns the same version that is inui/package.json, do not republish. Bump the version or use the normal repo-wide release flow inscripts/release.sh.
If the first real publish returns npm E404, check npm-side prerequisites before retrying:
npm whoamimust succeed first. An expired or missing npm login will block the publish.- For an organization-scoped package like
@paperclipai/ui, thepaperclipainpm organization must exist and the publisher must be a member with permission to publish to that scope. - The initial publish must include
--access publicfor 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
canarydist-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.Ppoints at that original commit
Trusted publishing
The intended CI model is npm trusted publishing through GitHub OIDC.
That means:
- no long-lived
NPM_TOKENin 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.
Release enrollment for new public packages
Paperclip does not auto-publish every non-private workspace package anymore.
CI publishing is controlled by scripts/release-package-manifest.json.
When you add a new public package:
- add it to the manifest and decide whether CI should publish it immediately
- if CI should publish it, bootstrap the package on npm before merge
- if CI should not publish it yet, keep
"publishFromCi": false - only enable
"publishFromCi": trueafter npm trusted publishing is configured for that package
PR CI now checks changed release-enabled package manifests against npm. That catches a missing first-publish bootstrap before the change reaches master.
One-time bootstrap sequence for a new package
The first publish of a brand-new package still needs one human maintainer with npm write access. After that, trusted publishing can take over.
Example for @paperclipai/adapter-acpx-local from the repo root:
# safe preview
pnpm run release:bootstrap-package -- @paperclipai/adapter-acpx-local
# one-time first publish from an authenticated maintainer machine
pnpm run release:bootstrap-package -- @paperclipai/adapter-acpx-local --publish --otp 123456
The helper script:
- checks that the package does not already exist on npm
- builds the target package unless
--skip-buildis passed - runs
npm pack --dry-runin the package directory - only runs the real
npm publish --access publicwhen--publish --otp <code>is provided
For the real --publish step, the maintainer machine must already be authenticated to npm.
If npm whoami returns 401, first run npm logout --registry=https://registry.npmjs.org/ to clear any stale local auth, then run npm login or npm adduser locally as an npm org member, and finally rerun the helper.
That local human auth is fine for the one-time bootstrap publish; we just do not want the same auth model inside CI.
The helper now requires --otp <code> up front for --publish, so it fails before the real publish attempt if the one-time password is missing.
After that first publish succeeds:
- open
https://www.npmjs.com/package/@paperclipai/adapter-acpx-local - go to
Settings→Trusted publishing - add repository
paperclipai/paperclip - set workflow filename to
release.yml - optionally go to
Settings→Publishing accessand enableRequire two-factor authentication and disallow tokens - keep
publishFromCi: trueinscripts/release-package-manifest.json
Once those steps are done, future canary and stable publishes for that package are automated through GitHub OIDC. The manual step is only the first package creation on npm.
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.