#!/usr/bin/env bun // scripts/release.ts // One command to release Peach to both Chrome Web Store and Firefox AMO. // Usage: npm run release // npm run release:chrome (Chrome only — skips AMO) // // Required environment variables (set via setup-chrome-release.sh and shell profile): // CWS_CLIENT_ID, CWS_CLIENT_SECRET, CWS_REFRESH_TOKEN, CWS_EXTENSION_ID // AMO_JWT_ISSUER, AMO_JWT_SECRET (not needed for --chrome-only) import { execSync } from "child_process"; import { readFileSync, existsSync, mkdirSync, rmSync } from "fs"; import { join } from "path"; const ROOT = process.cwd(); const CHROME_ONLY = process.argv.includes("--chrome-only"); const BOLD = "\x1b[1m"; const DIM = "\x1b[2m"; const ACCENT = "\x1b[38;5;208m"; const GREEN = "\x1b[32m"; const RED = "\x1b[31m"; const RESET = "\x1b[0m"; function run(cmd: string, label?: string, env?: Record) { console.log(`\n${ACCENT}▶${RESET} ${label ?? cmd}`); execSync(cmd, { stdio: "inherit", cwd: ROOT, env: { ...process.env, ...env } }); } function success(msg: string) { console.log(`${GREEN}✓ ${msg}${RESET}`); } function fail(msg: string): never { console.error(`${RED}✗ ${msg}${RESET}`); process.exit(1); } function check(name: string): string { const val = process.env[name]; if (!val) fail(`${name} not set. Run setup-chrome-release.sh and reload your shell.`); return val!; } // ─── Header ────────────────────────────────────────────────────────────────── console.clear(); console.log(""); console.log(`${ACCENT}${BOLD} ░░ peach release ░░${RESET}`); console.log(""); // ─── Read version ───────────────────────────────────────────────────────────── const manifest = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf8")); const version = manifest.version; if (!version) fail("Could not read version from manifest.json"); const targets = CHROME_ONLY ? "Chrome" : "Chrome + Firefox"; console.log(`${BOLD}Releasing Peach v${version} to ${targets}${RESET}`); console.log(`${DIM}Stores update automatically for existing users.${RESET}`); // ─── Verify environment ─────────────────────────────────────────────────────── console.log(`\n${BOLD}Checking credentials…${RESET}`); const CWS_CLIENT_ID = check("CWS_CLIENT_ID"); const CWS_CLIENT_SECRET = check("CWS_CLIENT_SECRET"); const CWS_REFRESH_TOKEN = check("CWS_REFRESH_TOKEN"); const CWS_EXTENSION_ID = check("CWS_EXTENSION_ID"); const CWS_PUBLISHER_ID = check("PUBLISHER_ID"); let AMO_JWT_ISSUER = ""; let AMO_JWT_SECRET = ""; if (!CHROME_ONLY) { AMO_JWT_ISSUER = check("AMO_JWT_ISSUER"); AMO_JWT_SECRET = check("AMO_JWT_SECRET"); } success("All credentials present"); // ─── CI gate ────────────────────────────────────────────────────────────────── run("npm run ci", "Running CI gate"); success("All tests pass"); // ─── Build ──────────────────────────────────────────────────────────────────── if (CHROME_ONLY) { run("npm run build:chrome", "Building Chrome target"); } else { run("npm run build", "Building all targets (Chrome + Firefox)"); } success("Build complete"); // ─── Ensure artifacts dir ───────────────────────────────────────────────────── if (!existsSync(join(ROOT, "artifacts"))) { mkdirSync(join(ROOT, "artifacts")); } // ─── Chrome release ─────────────────────────────────────────────────────────── console.log(`\n${ACCENT}${BOLD}Chrome Web Store${RESET}`); const chromeZip = join(ROOT, "artifacts", `peach-chrome-${version}.zip`); if (existsSync(chromeZip)) rmSync(chromeZip); run( `cd dist-chrome && zip -r "${chromeZip}" . --exclude '*.map' --exclude '.DS_Store' --exclude '__MACOSX/*'`, `Packaging Chrome extension → peach-chrome-${version}.zip`, ); run( [ "npx chrome-webstore-upload-cli upload", `--source "${chromeZip}"`, `--extension-id "${CWS_EXTENSION_ID}"`, "--auto-publish", ].join(" "), "Uploading and publishing to Chrome Web Store", // Credentials via child env, not inline command-line assignments, so they // are not visible in the process table. The CLI reads these from env. { CLIENT_ID: CWS_CLIENT_ID, CLIENT_SECRET: CWS_CLIENT_SECRET, REFRESH_TOKEN: CWS_REFRESH_TOKEN, PUBLISHER_ID: CWS_PUBLISHER_ID, }, ); success(`Chrome v${version} submitted — review typically takes minutes to hours`); // ─── Firefox release ────────────────────────────────────────────────────────── if (!CHROME_ONLY) { console.log(`\n${ACCENT}${BOLD}Firefox Add-ons (AMO)${RESET}`); const sourceZip = `peach-source-${version}.zip`; run( [ "zip -r", sourceZip, "src", "scripts", "public", // PB-26-004: the shipped .wasm is transpiled from crypto-core/src/lib.rs // — AMO reviewers must be able to inspect and rebuild it (npm run // build:noise-core). Build cache and generated output are excluded. "crypto-core", "manifest.json", "manifest.firefox.json", "package.json", "bun.lock", "tsconfig.json", "vite.config.ts", "--exclude 'node_modules/*'", "--exclude 'dist-*/*'", "--exclude 'crypto-core/target/*'", "--exclude 'crypto-core/generated/*'", "--exclude '*.zip'", "--exclude '.DS_Store'", "--exclude '__MACOSX/*'", ].join(" "), "Packaging source archive for AMO", ); run( [ "npx web-ext sign", "--source-dir dist-firefox", "--artifacts-dir artifacts", "--channel=listed", ].join(" "), "Signing and submitting to Firefox AMO", // web-ext reads WEB_EXT_API_KEY/WEB_EXT_API_SECRET from env — pass via // child env so the JWT credentials are not visible in the process table. { WEB_EXT_API_KEY: AMO_JWT_ISSUER, WEB_EXT_API_SECRET: AMO_JWT_SECRET, }, ); rmSync(join(ROOT, sourceZip), { force: true }); success(`Firefox v${version} submitted — review typically takes minutes`); } // ─── Done ───────────────────────────────────────────────────────────────────── console.log(""); console.log(`${ACCENT}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`); console.log(`${ACCENT}${BOLD} Peach v${version} released to ${targets}.${RESET}`); console.log(`${ACCENT}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}`); console.log(""); if (!CHROME_ONLY) { console.log(` Chrome: under review — existing users update automatically`); console.log(` Firefox: under review — existing users update automatically`); } else { console.log(` Chrome: under review — existing users update automatically`); } console.log(""); console.log(`${DIM}Check status:${RESET}`); console.log(` Chrome: https://chrome.google.com/webstore/devconsole`); if (!CHROME_ONLY) { console.log(` Firefox: https://addons.mozilla.org/developers/`); } console.log("");