Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,20 @@ For local-dev beta smoke before a stable release. Uses Changesets prerelease mod
bun run release:beta
```

### Local npm authorization

Run from an interactive terminal without piping or redirecting output. The publisher inherits stdin, stdout, and stderr so npm can handle browser authorization or prompt for an OTP. Browser authorization is available when the registry returns a web challenge; numeric OTP remains the fallback. Account 2FA stays enabled.

Inside agent-sandbox, use the configured host-browser bridge as npm's browser command:

```bash
npm_config_browser="$PLANNOTATOR_BROWSER" bun run release:beta
```

`PLANNOTATOR_BROWSER` must point to an available executable bridge. The existing Plannotator bridge opens on both the host and the forwarded laptop; use a local-only bridge if authorization should open on one machine only. No browser profile or cookie mount is needed.

### Publish flow

What the script does:

1. Refuses to run in CI or without an interactive TTY.
Expand Down
16 changes: 15 additions & 1 deletion scripts/release-beta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ async function runVerify(cwd: string): Promise<void> {
await $`bun run verify:pack`.cwd(cwd).quiet();
}

export async function runBetaPublish(cwd: string): Promise<void> {
// npm needs a real terminal for browser authorization and OTP prompts.
const publisher = Bun.spawn(["bun", "run", "release", "--", "--no-git-tag"], {
cwd,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});
const status = await publisher.exited;
if (status !== 0) {
throw new Error(`publish command exited with code ${status}`);
}
}

async function main() {
if (process.env.CI) abort("refusing to run in CI; beta publishes are local-dev only");
if (!process.stdin.isTTY) abort("refusing to run without an interactive TTY");
Expand Down Expand Up @@ -167,7 +181,7 @@ async function main() {
console.log("\n— publish (no git tag)");
publishAttempted = true;
try {
await $`bun run release -- --no-git-tag`.cwd(tmp);
await runBetaPublish(tmp);
} catch (publishErr) {
// npm publish is irreversible; the CLI may error after the registry
// accepted the package. Probe dist-tags before deciding next steps.
Expand Down
60 changes: 60 additions & 0 deletions test/release-beta-publish.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { runBetaPublish } from "../scripts/release-beta";

function releaseFixture(source: string): string {
const cwd = mkdtempSync(join(tmpdir(), "release-beta-publish-"));
writeFileSync(
join(cwd, "package.json"),
JSON.stringify({ private: true, scripts: { release: "bun release.ts" } }),
);
writeFileSync(join(cwd, "release.ts"), source);
return cwd;
}

test.skipIf(process.platform !== "linux")("beta publish preserves the terminal for npm authorization", () => {
const script = Bun.which("script");
if (!script) throw new Error("PTY regression test requires util-linux script");
const cwd = releaseFixture(`
console.log(JSON.stringify({
stdin: !!process.stdin.isTTY,
stdout: !!process.stdout.isTTY,
stderr: !!process.stderr.isTTY,
}));
console.log(JSON.stringify(process.argv.slice(2)));
console.log("browser=" + process.env.npm_config_browser);
process.exit(process.stdin.isTTY && process.stdout.isTTY ? 0 : 42);
`);
try {
const moduleUrl = new URL("../scripts/release-beta.ts", import.meta.url).href;
const driver = `import { runBetaPublish } from ${JSON.stringify(moduleUrl)}; await runBetaPublish(${JSON.stringify(cwd)});`;
const command = [process.execPath, "--eval", driver]
.map((arg) => `'${arg.replaceAll("'", "'\\''")}'`)
.join(" ");
const result = Bun.spawnSync([script, "-qec", command, "/dev/null"], {
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, npm_config_browser: "fixture-browser" },
});
const output = result.stdout.toString() + result.stderr.toString();
expect(output).toContain('{"stdin":true,"stdout":true,"stderr":true}');
expect(output).toContain('["--no-git-tag"]');
expect(output).toContain("browser=fixture-browser");
expect(result.exitCode).toBe(0);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});

test("beta publish rejects a nonzero child exit for the registry-state probe", async () => {
const cwd = releaseFixture("process.exit(23);");
try {
await expect(runBetaPublish(cwd)).rejects.toThrow("publish command exited with code 23");
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
Loading