feat(pw): add bun pw for prod env dev testing

Adds a lightweight script to run an existing dw worktree with production
environment variables from Infisical. Key behaviors:

- Reuses dw registry for worktree number / port / alias discovery
- Temporarily stashes .env.local files so preload-env.ts cannot
  override Infisical-injected prod secrets with dev Neon branch URLs
- Restores stashed .env.local files on exit (SIGINT/SIGTERM/child exit)
- Only overrides SQS queue URLs to local ElasticMQ (avoids consuming
  prod messages); everything else (DB, Redis, Stripe) flows from Infisical
- Provides pw identify and pw restore subcommands
This commit is contained in:
amianthus
2026-05-20 13:17:44 +01:00
parent a5f37de3fa
commit 2d0cf2611d
2 changed files with 208 additions and 0 deletions

View File

@@ -87,6 +87,8 @@
"dw:identify": "ENV_FILE=.env infisical run --env=dev --recursive -- bun scripts/dw/index.ts identify",
"dw:setup": "ENV_FILE=.env infisical run --env=dev --recursive -- bun scripts/dw/index.ts setup",
"dw:run": "ENV_FILE=.env infisical run --env=dev --recursive -- bun scripts/dw/index.ts run",
"pw": "ENV_FILE=.env.prod infisical run --env=prod --recursive -- bun scripts/pw/index.ts",
"pw:identify": "ENV_FILE=.env.prod infisical run --env=prod --recursive -- bun scripts/pw/index.ts identify",
"dw:enable": "bun scripts/dw/index.ts enable",
"dw:disable": "bun scripts/dw/index.ts disable",
"d": "ENV_FILE=.env infisical run --env=dev --recursive -- bun scripts/dev.ts",

206
scripts/pw/index.ts Normal file
View File

@@ -0,0 +1,206 @@
import { existsSync, renameSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { homedir } from "node:os";
import { fileURLToPath } from "node:url";
import { fatal, log } from "../dw/helpers/shell.ts";
import { getCurrentWorktree } from "../dw/helpers/git.ts";
import { loadRegistry } from "../dw/helpers/registry.ts";
import {
killOwnPorts,
aliasesFor,
elasticMqPortFor,
} from "../dw/helpers/ports.ts";
import { killTmuxSession, tmuxSessionName } from "../dw/helpers/tmux.ts";
import { ENV_LOCAL_TARGETS } from "../dw/constants.ts";
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = join(SCRIPT_DIR, "../..");
const STASH_SUFFIX = ".pw-stash";
/* ------------------------------------------------------------------ */
// Stash / restore .env.local files
/* ------------------------------------------------------------------ */
const stashedThisRun: string[] = [];
function stashEnvLocalFiles(): void {
for (const rel of ENV_LOCAL_TARGETS) {
const abs = join(PROJECT_ROOT, rel);
const backup = abs + STASH_SUFFIX;
if (!existsSync(abs)) continue;
if (existsSync(backup)) {
log(
`warning: ${rel}${STASH_SUFFIX} already exists (previous pw run crashed?), skipping stash`,
);
continue;
}
renameSync(abs, backup);
stashedThisRun.push(abs);
log(`stashed ${rel}${rel}${STASH_SUFFIX}`);
}
}
function restoreEnvLocalFiles(): void {
for (const abs of stashedThisRun) {
const backup = abs + STASH_SUFFIX;
if (!existsSync(backup)) continue;
renameSync(backup, abs);
log(`restored ${basename(abs)}`);
}
}
/* ------------------------------------------------------------------ */
// Commands
/* ------------------------------------------------------------------ */
async function cmdRun(): Promise<void> {
const cwd = getCurrentWorktree();
const registry = loadRegistry();
const entry = registry[cwd];
if (!entry) {
fatal(`no registered worktree at ${cwd}. Run 'bun dw setup' first.`);
}
const { worktreeNum } = entry;
// Clean up any lingering dw dev server / tmux session on these ports.
killOwnPorts(worktreeNum);
killTmuxSession(tmuxSessionName(worktreeNum));
// Temporarily hide .env.local files so preload-env.ts can't override
// Infisical-injected prod secrets with dev Neon branch URLs.
stashEnvLocalFiles();
const offset = (worktreeNum - 1) * 100;
const env: Record<string, string> = {
...(process.env as Record<string, string>),
SERVER_PORT: String(8080 + offset),
VITE_PORT: String(3000 + offset),
CHECKOUT_PORT: String(3001 + offset),
};
if (worktreeNum > 1) {
const aliases = aliasesFor(worktreeNum);
env.BETTER_AUTH_URL = aliases.apiUrl;
env.CLIENT_URL = aliases.viteUrl;
env.VITE_BACKEND_URL = aliases.apiUrl;
env.VITE_FRONTEND_URL = aliases.viteUrl;
const mqPort = elasticMqPortFor(worktreeNum);
env.SQS_QUEUE_URL_V2 = `http://localhost:${mqPort}/000000000000/autumn.fifo`;
env.TRACK_SQS_QUEUE_URL = `http://localhost:${mqPort}/000000000000/autumn-track.fifo`;
}
const portlessCa = join(homedir(), ".portless", "ca.pem");
if (existsSync(portlessCa) && !env.NODE_EXTRA_CA_CERTS) {
env.NODE_EXTRA_CA_CERTS = portlessCa;
}
log(`starting dev with prod env (worktree=${worktreeNum})`);
const proc = Bun.spawn(
[
"bun",
"scripts/dev.ts",
"--worktree",
String(worktreeNum),
...process.argv.slice(3),
],
{
cwd: PROJECT_ROOT,
env,
stdout: "inherit",
stderr: "inherit",
},
);
const cleanupAndExit = (code: number | null) => {
restoreEnvLocalFiles();
process.exit(code ?? 0);
};
process.on("SIGINT", () => proc.kill("SIGINT"));
process.on("SIGTERM", () => proc.kill("SIGTERM"));
// Ensure restore runs even if the child exits on its own.
proc.exited.then(cleanupAndExit);
}
function cmdIdentify(): void {
const cwd = getCurrentWorktree();
const registry = loadRegistry();
const entry = registry[cwd];
if (!entry) {
console.error(`[pw] no registered worktree at ${cwd}`);
console.error(` run 'bun dw setup' here first`);
process.exit(1);
}
const { worktreeNum, branchName } = entry;
const offset = (worktreeNum - 1) * 100;
const serverPort = 8080 + offset;
const vitePort = 3000 + offset;
const [serverUrl, viteUrl] =
worktreeNum === 1
? [
`http://localhost:${serverPort}`,
`http://localhost:${vitePort}`,
]
: [
aliasesFor(worktreeNum).apiUrl,
aliasesFor(worktreeNum).viteUrl,
];
console.log(`Worktree #${worktreeNum} (${entry.path})`);
console.log(` Branch: ${branchName ?? "(canonical)"}`);
console.log(` Server URL: ${serverUrl}`);
console.log(` Vite URL: ${viteUrl}`);
console.log(` Server port: ${serverPort}`);
console.log(` Vite port: ${vitePort}`);
console.log();
console.log(`PW_WORKTREE_NUM=${worktreeNum}`);
console.log(`PW_SERVER_URL=${serverUrl}`);
console.log(`PW_VITE_URL=${viteUrl}`);
console.log(`PW_SERVER_PORT=${serverPort}`);
console.log(`PW_VITE_PORT=${vitePort}`);
}
function cmdRestore(): void {
let restored = 0;
for (const rel of ENV_LOCAL_TARGETS) {
const abs = join(PROJECT_ROOT, rel);
const backup = abs + STASH_SUFFIX;
if (!existsSync(backup)) continue;
renameSync(backup, abs);
log(`restored ${rel}`);
restored++;
}
if (restored === 0) {
log("no stashed .env.local files found");
}
}
/* ------------------------------------------------------------------ */
// Main
/* ------------------------------------------------------------------ */
async function main(): Promise<void> {
const sub = process.argv[2];
if (!sub || sub === "run") {
await cmdRun();
return;
}
switch (sub) {
case "identify":
cmdIdentify();
break;
case "restore":
cmdRestore();
break;
default:
fatal(
`unknown subcommand: ${sub} (use: run | identify | restore)`,
);
}
}
await main();