mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
## 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
250 lines
9.3 KiB
Markdown
250 lines
9.3 KiB
Markdown
# Publishing to npm
|
|
|
|
Low-level reference for how Paperclip packages are prepared and published to npm.
|
|
|
|
For the maintainer workflow, use [doc/RELEASING.md](RELEASING.md). This document focuses on packaging internals.
|
|
|
|
## Current Release Entry Points
|
|
|
|
Use these scripts:
|
|
|
|
- [`scripts/release.sh`](../scripts/release.sh) for canary and stable publish flows
|
|
- [`scripts/create-github-release.sh`](../scripts/create-github-release.sh) after pushing a stable tag
|
|
- [`scripts/rollback-latest.sh`](../scripts/rollback-latest.sh) to repoint `latest`
|
|
- [`scripts/build-npm.sh`](../scripts/build-npm.sh) for 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:
|
|
|
|
```bash
|
|
./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`](../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`](../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:
|
|
|
|
```bash
|
|
# 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`](../ui/package.json), do not republish. Bump the version or use the normal repo-wide release flow in [`scripts/release.sh`](../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:
|
|
|
|
```bash
|
|
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](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`](../scripts/release-package-manifest.json).
|
|
|
|
When you add a new public package:
|
|
|
|
1. add it to the manifest and decide whether CI should publish it immediately
|
|
2. if CI should publish it, bootstrap the package on npm before merge
|
|
3. if CI should not publish it yet, keep `"publishFromCi": false`
|
|
4. only enable `"publishFromCi": true` after 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:
|
|
|
|
```bash
|
|
# 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-build` is passed
|
|
- runs `npm pack --dry-run` in the package directory
|
|
- only runs the real `npm publish --access public` when `--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:
|
|
|
|
1. open `https://www.npmjs.com/package/@paperclipai/adapter-acpx-local`
|
|
2. go to `Settings` → `Trusted publishing`
|
|
3. add repository `paperclipai/paperclip`
|
|
4. set workflow filename to `release.yml`
|
|
5. optionally go to `Settings` → `Publishing access` and enable `Require two-factor authentication and disallow tokens`
|
|
6. keep `publishFromCi: true` in [`scripts/release-package-manifest.json`](../scripts/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:
|
|
|
|
```bash
|
|
./scripts/rollback-latest.sh 2026.318.0
|
|
```
|
|
|
|
This is the fastest way to restore the default install path if a stable release is bad.
|
|
|
|
## Related Files
|
|
|
|
- [`scripts/build-npm.sh`](../scripts/build-npm.sh)
|
|
- [`scripts/generate-npm-package-json.mjs`](../scripts/generate-npm-package-json.mjs)
|
|
- [`scripts/generate-ui-package-json.mjs`](../scripts/generate-ui-package-json.mjs)
|
|
- [`scripts/release-package-map.mjs`](../scripts/release-package-map.mjs)
|
|
- [`cli/esbuild.config.mjs`](../cli/esbuild.config.mjs)
|
|
- [`doc/RELEASING.md`](RELEASING.md)
|