Staged Npm Publish
DL#158 — Staged npm Publish
Written for AI agents. See Log Methodology Note below for details.
Background
The monorepo has ~40 publishable @jay-framework/* packages. Previously, publishing required passing an OTP to every yarn npm publish call. With 2FA enabled, OTPs expire in 30 seconds — not enough time for sequential publishing of all packages. The old scripts either failed mid-way or required disabling 2FA.
npm introduced a staging workflow (npm 11+ / Node 22+, Yarn 4.17+): packages are uploaded without 2FA, then bulk-approved with a single OTP after all uploads complete.
Problem
- OTP expires before all packages are published
- Partial publish leaves the registry in an inconsistent state (some packages updated, others not)
- Old
publish.cjsscript requiredNPM_OTPenv var upfront — wasted if staging took too long
Design
Two-phase publish
Phase 1: Stage (no OTP) Phase 2: Approve (single OTP)
┌─────────────────────┐ ┌─────────────────────┐
│ yarn npm publish │ │ yarn npm stage │
│ --staged │ ──────► │ approve <id> │
│ --access public │ │ --otp <otp> │
│ (per workspace) │ │ (per staged pkg) │
└─────────────────────┘ └─────────────────────┘
Resumable flow
On start, check for already-staged packages. If found, skip directly to approve. This handles:
- Ctrl-C after staging but before OTP entry
- OTP entered incorrectly — re-run picks up staged packages
OTP prompt timing
OTP is requested interactively right before the approve step, not as a CLI argument. This maximizes the time window for the OTP to remain valid.
Implementation
Script: scripts/publish-staged.mjs
1. yarn npm whoami — verify npm login
2. yarn npm stage list — check for existing staged packages
├── found → skip to step 4
└── empty → continue
3. yarn workspaces foreach -A --no-private --topological-dev
exec "yarn npm publish --staged --access public --tolerate-republish"
4. yarn npm stage list --json — collect stage IDs
5. prompt: Enter OTP
6. yarn npm stage approve <id> --otp <otp> — for each staged package
package.json scripts
{
"publish": "node scripts/publish-staged.mjs",
"publish:legacy": "node scripts/publish.cjs"
}
Old publish:interactive and publish:manual removed — the staged flow replaces both.
Requirements
- Node 22+ (
nvm use v22) - Yarn 4.17+ (supports
--staged,yarn npm stage list/approve)
Key flags
| Flag | Purpose |
|---|---|
--staged |
Upload without 2FA, defer to approve step |
--access public |
Required for @jay-framework/* scoped packages |
--tolerate-republish |
Skip already-published versions without error |
--topological-dev |
Publish dependencies before dependents |
--json |
NDJSON output from stage list for script parsing |
Trade-offs
| Choice | Pro | Con |
|---|---|---|
| Interactive OTP prompt vs CLI arg | OTP entered at last moment, maximizes validity window | Cannot be fully non-interactive |
| Resume from staged vs always re-stage | Handles interruptions gracefully | Stale staged packages could surprise if left from a previous session |
Yarn --staged vs yarn pack + npm stage publish |
Single tool, resolves workspace:* automatically |
Requires Yarn 4.17+ |
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.