From dcd7612b09b755c6c8b097bb58d70da1dd73b99e Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Sun, 3 May 2026 19:10:04 -0700 Subject: [PATCH] feat(release): support otp bootstrap publish --- doc/PUBLISHING.md | 4 ++ scripts/bootstrap-npm-package.mjs | 56 ++++++++++++++++++++++++-- scripts/bootstrap-npm-package.test.mjs | 12 ++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/doc/PUBLISHING.md b/doc/PUBLISHING.md index 311b43dfd88..a7b6652157e 100644 --- a/doc/PUBLISHING.md +++ b/doc/PUBLISHING.md @@ -203,6 +203,9 @@ 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 + +# if npm requires an authenticator-app code for publish +pnpm run release:bootstrap-package -- @paperclipai/adapter-acpx-local --publish --otp 123456 ``` The helper script: @@ -215,6 +218,7 @@ The helper script: 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. +If `npm publish` returns `EOTP`, complete the browser auth URL that npm prints or rerun the helper with `--otp ` if your npm account uses authenticator-app 2FA. After that first publish succeeds: diff --git a/scripts/bootstrap-npm-package.mjs b/scripts/bootstrap-npm-package.mjs index d887b6f6bc4..8a7131552fd 100644 --- a/scripts/bootstrap-npm-package.mjs +++ b/scripts/bootstrap-npm-package.mjs @@ -30,8 +30,10 @@ function usage() { function parseArgs(argv) { const flags = new Set(); let selector = null; + let otp = null; - for (const arg of argv) { + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; if (arg === "--") { continue; } @@ -41,8 +43,18 @@ function parseArgs(argv) { continue; } + if (arg === "--otp") { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error("expected a one-time password after --otp"); + } + otp = value; + index += 1; + continue; + } + if (arg === "--help" || arg === "-h") { - return { help: true, selector: null, publish: false, skipBuild: false }; + return { help: true, selector: null, publish: false, skipBuild: false, otp: null }; } if (arg.startsWith("--")) { @@ -61,6 +73,7 @@ function parseArgs(argv) { selector, publish: flags.has("--publish"), skipBuild: flags.has("--skip-build"), + otp, }; } @@ -92,6 +105,10 @@ function runChecked(command, args, options = {}) { } } +function formatCommand(command, args) { + return `${command} ${args.join(" ")}`; +} + function ensureNpmAuth() { const result = runCommand("npm", ["whoami"]); const stdout = result.stdout ?? ""; @@ -170,8 +187,38 @@ function printNextSteps(pkg) { ); } +function publishPackage(pkg, otp) { + const publishArgs = ["publish", "--access", "public"]; + if (otp) { + publishArgs.push("--otp", otp); + } + + const result = runCommand("npm", publishArgs, { cwd: join(repoRoot, pkg.dir) }); + const stdout = result.stdout ?? ""; + const stderr = result.stderr ?? ""; + const output = `${stdout}\n${stderr}`.trim(); + + if (stdout) process.stdout.write(stdout); + if (stderr) process.stderr.write(stderr); + + if (result.status === 0) { + return; + } + + if (/\bEOTP\b|one-time password/i.test(output)) { + throw new Error( + [ + "npm publish reached the publish-time 2FA check.", + "Complete the browser auth URL printed by npm and rerun the helper, or rerun with `--otp ` if your npm account uses authenticator-app codes.", + ].join(" "), + ); + } + + throw new Error(`${formatCommand("npm", publishArgs)} failed with status ${result.status ?? "unknown"}`); +} + function main(argv) { - const { help, selector, publish, skipBuild } = parseArgs(argv); + const { help, selector, publish, skipBuild, otp } = parseArgs(argv); if (help) { usage(); @@ -219,7 +266,7 @@ function main(argv) { } process.stdout.write(`Publishing ${pkg.name}...\n`); - runChecked("npm", ["publish", "--access", "public"], { cwd: join(repoRoot, pkg.dir) }); + publishPackage(pkg, otp); printNextSteps(pkg); } @@ -238,5 +285,6 @@ export { ensureNpmAuth, inspectNpmPackage, parseArgs, + publishPackage, resolveTargetPackage, }; diff --git a/scripts/bootstrap-npm-package.test.mjs b/scripts/bootstrap-npm-package.test.mjs index af5db1a8fae..254bcd89b2d 100644 --- a/scripts/bootstrap-npm-package.test.mjs +++ b/scripts/bootstrap-npm-package.test.mjs @@ -9,6 +9,17 @@ test("parseArgs recognizes publish and skip-build flags", () => { selector: "@paperclipai/adapter-acpx-local", publish: true, skipBuild: true, + otp: null, + }); +}); + +test("parseArgs accepts an explicit otp value", () => { + assert.deepEqual(parseArgs(["packages/adapters/acpx-local", "--publish", "--otp", "123456"]), { + help: false, + selector: "packages/adapters/acpx-local", + publish: true, + skipBuild: false, + otp: "123456", }); }); @@ -18,6 +29,7 @@ test("parseArgs returns help mode", () => { selector: null, publish: false, skipBuild: false, + otp: null, }); });