From ff695beff93b8ba927de8d952dadd93fa28e8bee Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Fri, 22 May 2026 13:45:01 -0700 Subject: [PATCH 1/3] feat: add `npmStaged` publish config for npm staged publishing Adds support for npm's staged publishing feature (`npm stage publish`), which stages packages on npmjs.com and requires manual 2FA approval before going live. - New `npmStaged` boolean in publish config (default: false) - Validates publishManager is "npm" and npm version >= 11.5.1 - Updated docs and added test coverage - Updated frog clipboard image --- docs/configuration.md | 1 + images/frog-clipboard.png | Bin 11527 -> 11532 bytes packages/bumpy/src/core/publish-pipeline.ts | 22 ++++++++- packages/bumpy/src/types.ts | 8 +++ .../bumpy/test/core/publish-pipeline.test.ts | 46 +++++++++++++++++- 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 99976f9..9121236 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -68,6 +68,7 @@ The `publish` object controls how packages are packed and published: | `publishManager` | `string` | `"npm"` | Which tool runs `publish` (npm supports OIDC/provenance) | | `publishArgs` | `string[]` | `[]` | Extra args passed to publish (e.g., `["--provenance"]`) | | `protocolResolution` | `"pack" \| "in-place"` | `"pack"` | How `workspace:` and `catalog:` protocols are resolved | +| `npmStaged` | `boolean` | `false` | Use `npm stage publish` — requires 2FA approval on npmjs.com | ### Version PR config diff --git a/images/frog-clipboard.png b/images/frog-clipboard.png index b77834d919eeb37bfe33a72f7d61f61197357591..df350e084f4a7af4513329d707af5791b346d9fc 100644 GIT binary patch literal 11532 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 diff --git a/packages/bumpy/src/core/publish-pipeline.ts b/packages/bumpy/src/core/publish-pipeline.ts index 7281059..116279b 100644 --- a/packages/bumpy/src/core/publish-pipeline.ts +++ b/packages/bumpy/src/core/publish-pipeline.ts @@ -132,6 +132,24 @@ export async function publishPackages( // Set up npm authentication before publishing setupNpmAuth(rootDir, publishConfig.publishManager); + // Validate staged publishing config + if (publishConfig.npmStaged) { + if (publishConfig.publishManager !== 'npm') { + log.warn('Staged publishing is only supported with publishManager "npm" — ignoring staged option'); + } else { + const npmVersion = tryRunArgs(['npm', '--version']); + if (npmVersion) { + const [major, minor, patch] = npmVersion.split('.').map(Number); + const meetsMinVersion = major! > 11 || (major === 11 && (minor! > 5 || (minor === 5 && patch! >= 1))); + if (!meetsMinVersion) { + log.warn(`Staged publishing requires npm >= 11.5.1 (found ${npmVersion})`); + } else { + log.dim(`Staged publishing enabled — packages will require 2FA approval on npmjs.com`); + } + } + } + } + // Resolve "auto" pack manager to detected PM const packManager = publishConfig.packManager === 'auto' ? detectedPm : publishConfig.packManager; @@ -302,7 +320,9 @@ function buildPublishArgs( const args: string[] = []; // Base command - if (publishManager === 'yarn') { + if (config.publish.npmStaged && publishManager === 'npm') { + args.push('npm', 'stage', 'publish'); + } else if (publishManager === 'yarn') { args.push('yarn', 'npm', 'publish'); } else { args.push(publishManager, 'publish'); diff --git a/packages/bumpy/src/types.ts b/packages/bumpy/src/types.ts index 5181ab6..3a82b99 100644 --- a/packages/bumpy/src/types.ts +++ b/packages/bumpy/src/types.ts @@ -79,6 +79,13 @@ export interface PublishConfig { * Default: "pack" */ protocolResolution: 'pack' | 'in-place' | 'none'; + /** + * Use npm staged publishing (`npm stage publish`). + * Stages the publish on npmjs.com, requiring manual 2FA approval before going live. + * Only works with publishManager "npm" and requires npm >= 11.5.1. + * Default: false + */ + npmStaged: boolean; } export interface BumpyConfig { @@ -157,6 +164,7 @@ export const DEFAULT_PUBLISH_CONFIG: PublishConfig = { packManager: 'auto', publishManager: 'npm', publishArgs: [], + npmStaged: false, protocolResolution: 'pack', }; diff --git a/packages/bumpy/test/core/publish-pipeline.test.ts b/packages/bumpy/test/core/publish-pipeline.test.ts index bae70ba..32ba4b4 100644 --- a/packages/bumpy/test/core/publish-pipeline.test.ts +++ b/packages/bumpy/test/core/publish-pipeline.test.ts @@ -4,7 +4,7 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { writeJson, readJson, ensureDir, writeText } from '../../src/utils/fs.ts'; import { makePkg, gitInDir } from '../helpers.ts'; -import { installShellMock, uninstallShellMock } from '../helpers-shell-mock.ts'; +import { installShellMock, uninstallShellMock, addMockRule, getCallsMatching } from '../helpers-shell-mock.ts'; import { DependencyGraph } from '../../src/core/dep-graph.ts'; import { publishPackages } from '../../src/core/publish-pipeline.ts'; import type { WorkspacePackage, ReleasePlan, BumpyConfig } from '../../src/types.ts'; @@ -15,6 +15,11 @@ const IN_PLACE_CONFIG: BumpyConfig = { publish: { ...DEFAULT_PUBLISH_CONFIG, protocolResolution: 'in-place' }, }; +const STAGED_CONFIG: BumpyConfig = { + ...DEFAULT_CONFIG, + publish: { ...DEFAULT_PUBLISH_CONFIG, npmStaged: true, protocolResolution: 'in-place' }, +}; + describe('publishPackages', () => { let tmpDir: string; @@ -266,4 +271,43 @@ describe('publishPackages', () => { expect(deps.react).toBe('^19.0.0'); expect(deps.jest).toBe('^30.0.0'); }); + + test('staged publishing uses npm stage publish', async () => { + const pkgDir = resolve(tmpDir, 'packages/staged-pkg'); + await ensureDir(pkgDir); + await writeJson(resolve(pkgDir, 'package.json'), { name: 'staged-pkg', version: '1.0.0' }); + await setupGitRepo(); + + // Mock npm --version (for staged validation) and the publish command + addMockRule({ match: 'npm --version', response: '11.5.1' }); + addMockRule({ match: 'npm stage publish', response: '' }); + + const packages = new Map(); + packages.set('staged-pkg', makePkg('staged-pkg', '1.0.0', { dir: pkgDir })); + + const depGraph = new DependencyGraph(packages); + const plan: ReleasePlan = { + bumpFiles: [], + warnings: [], + releases: [ + { + name: 'staged-pkg', + type: 'patch', + oldVersion: '1.0.0', + newVersion: '1.0.1', + bumpFiles: [], + isDependencyBump: false, + isCascadeBump: false, + isGroupBump: false, + bumpSources: [], + }, + ], + }; + + const result = await publishPackages(plan, packages, depGraph, STAGED_CONFIG, tmpDir, {}); + + expect(result.published).toHaveLength(1); + const publishCalls = getCallsMatching('npm stage publish'); + expect(publishCalls.length).toBeGreaterThanOrEqual(1); + }); }); From 9159334468519d6ca0b65fd7889e8173eb7242bb Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Fri, 22 May 2026 13:45:31 -0700 Subject: [PATCH 2/3] chore: add bump file for npm staged publishing --- .bumpy/npm-staged-publishing.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .bumpy/npm-staged-publishing.md diff --git a/.bumpy/npm-staged-publishing.md b/.bumpy/npm-staged-publishing.md new file mode 100644 index 0000000..10c2e5e --- /dev/null +++ b/.bumpy/npm-staged-publishing.md @@ -0,0 +1,5 @@ +--- +'@varlock/bumpy': minor +--- + +Add `npmStaged` publish config option for npm staged publishing (`npm stage publish`), which stages packages on npmjs.com requiring manual 2FA approval before going live. From 88c2eca16970b61bcab23a0e6c0cde610061627c Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Fri, 22 May 2026 20:07:55 -0700 Subject: [PATCH 3/3] docs: add npmStaged examples to README, config docs, and GH Actions docs --- README.md | 2 +- docs/configuration.md | 21 +++++++++++++++++++++ docs/github-actions.md | 2 ++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fff2bee..d0146f7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Fixed locale fallback logic in utils. - **All package managers** - npm, pnpm, yarn, and bun workspaces - **Smart dependency propagation** - configurable rules for how version bumps cascade through your dependency graph (see [version propagation docs](https://github.com/dmno-dev/bumpy/blob/main/docs/version-propagation.md)) -- **Pack-then-publish** - by default, publishes to npm (resolving `workspace:` and `catalog:` protocols, with OIDC/provenance support). Per-package custom publish commands let you target anything - VSCode extensions, Docker images, JSR, private registries, etc. +- **Pack-then-publish** - by default, publishes to npm (resolving `workspace:` and `catalog:` protocols, with OIDC/provenance support). Supports [npm staged publishing](https://docs.npmjs.com/about-staged-publishes) for 2FA-gated releases. Per-package custom publish commands let you target anything - VSCode extensions, Docker images, JSR, private registries, etc. - **Flexible package management** - include/exclude any package individually via per-package config, glob patterns, or `privatePackages` setting - **Non-interactive CLI** - `bumpy add` works fully non-interactively for CI/CD and AI-assisted development - **Aggregated GitHub releases** - optionally create a single consolidated release instead of one per package diff --git a/docs/configuration.md b/docs/configuration.md index 9121236..f5b49b8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -70,6 +70,24 @@ The `publish` object controls how packages are packed and published: | `protocolResolution` | `"pack" \| "in-place"` | `"pack"` | How `workspace:` and `catalog:` protocols are resolved | | `npmStaged` | `boolean` | `false` | Use `npm stage publish` — requires 2FA approval on npmjs.com | +#### Staged publishing + +When `npmStaged` is enabled, bumpy uses `npm stage publish` instead of `npm publish`. This stages packages on npmjs.com, where they must be manually approved with 2FA before going live. This adds an extra security gate to your release process — even if CI credentials are compromised, packages can't be published without maintainer approval. + +Requirements: + +- `publishManager` must be `"npm"` (the default) +- npm >= 11.5.1 +- [npm trusted publishing (OIDC)](https://docs.npmjs.com/trusted-publishers/) configured for your repo + +```json +{ + "publish": { + "npmStaged": true + } +} +``` + ### Version PR config The `versionPr` object customizes the PR that `bumpy ci release` creates: @@ -211,6 +229,9 @@ See the [Changelog Formatters](./changelog-formatters.md) docs for full details "dependencyBumpRules": { "peerDependencies": { "trigger": "minor", "bumpAs": "match" } }, + "publish": { + "npmStaged": true + }, "aggregateRelease": true, "packages": { "@myorg/vscode-extension": { diff --git a/docs/github-actions.md b/docs/github-actions.md index cd23231..b1543dd 100644 --- a/docs/github-actions.md +++ b/docs/github-actions.md @@ -72,6 +72,8 @@ jobs: **Trusted publishing setup:** Configure each package on [npmjs.com](https://docs.npmjs.com/trusted-publishers/) → Package Settings → Trusted Publishers → GitHub Actions. Specify your org/user, repo, and the workflow filename (`bumpy-release.yml`). +> **Staged publishing:** For an extra layer of security, enable `npmStaged` in your [publish config](./configuration.md#staged-publishing). This uses `npm stage publish` to stage packages on npmjs.com, requiring manual 2FA approval before they go live — even if your CI credentials are compromised, nothing gets published without maintainer approval. + ### Token-based auth (NPM_TOKEN) If you can't use trusted publishing, use an npm access token instead: