feat(release): support otp bootstrap publish

This commit is contained in:
Devin Foley committed 2026-05-03 19:10:04 -07:00
1 parent 318718b04a
commit dcd7612b09
3 files changed
+68 -4

No files matched your search

+4
View File
@@ -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 <code>` if your npm account uses authenticator-app 2FA.
After that first publish succeeds:
+52 -4
View File
@@ -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 <code>` 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,
};
+12
View File
@@ -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,
});
});