Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
272 lines
7.7 KiB
JavaScript
272 lines
7.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Local cutover rehearsal (read-only / dry-run).
|
|
*
|
|
* Runs (in order):
|
|
* 1) cutover-deploy-check (goose 039+040+041+042, /readyz, adapter-node notes)
|
|
* 2) migrator -list-legacy-emails (no patch / export)
|
|
* 3) migrator -list-member-memberships
|
|
* 4) migrator -list-companies-without-plans
|
|
* 5) orphan-processed cleanup POST without confirm (report-only)
|
|
*
|
|
* Excludes Clerk patch, SMTP/mailhooks, Stripe, assign/promote -confirm writes.
|
|
* Assign / promote / orphan live deletes stay separate operator commands
|
|
* (-confirm / confirm=true). This script never passes those flags.
|
|
*
|
|
* Usage:
|
|
* node scripts/cutover-local-rehearsal.mjs
|
|
* node scripts/cutover-local-rehearsal.mjs --skip-deploy-check
|
|
* node scripts/cutover-local-rehearsal.mjs --skip-orphan
|
|
* make cutover-rehearsal
|
|
*
|
|
* Orphan step needs a platform-admin session: set DESCRYBE_SMOKE_PASS (or
|
|
* CUTOVER_REHEARSAL_PASS) and optional CUTOVER_REHEARSAL_EMAIL (default
|
|
* demo@descrybe.local). See docs/demo-user.md.
|
|
*/
|
|
import { spawnSync } from "node:child_process";
|
|
import path from "node:path";
|
|
import { DEV_API_PORT, DEV_PUBLIC_API_URL } from "./dev-ports.mjs";
|
|
import { loadRootEnv, repoRoot } from "./root-env.mjs";
|
|
|
|
const args = new Set(process.argv.slice(2));
|
|
const wantHelp = args.has("-h") || args.has("--help");
|
|
const skipDeploy = args.has("--skip-deploy-check");
|
|
const skipLists = args.has("--skip-lists");
|
|
const skipOrphan = args.has("--skip-orphan");
|
|
const deployPassthrough = [];
|
|
if (args.has("--skip-goose")) deployPassthrough.push("--skip-goose");
|
|
if (args.has("--skip-readyz")) deployPassthrough.push("--skip-readyz");
|
|
|
|
if (wantHelp) {
|
|
console.log(`Usage: node scripts/cutover-local-rehearsal.mjs [flags]
|
|
|
|
Local dry-run rehearsal (no Clerk patch / SMTP / Stripe).
|
|
--skip-deploy-check skip scripts/cutover-deploy-check.mjs
|
|
--skip-lists skip migrator list-* steps
|
|
--skip-orphan skip orphan-processed dry-run
|
|
--skip-goose forward to deploy-check
|
|
--skip-readyz forward to deploy-check
|
|
|
|
Env: DATABASE_URL (lists), PUBLIC_API_URL / HEALTH_BASE_URL (orphan),
|
|
DESCRYBE_SMOKE_PASS or CUTOVER_REHEARSAL_PASS (+ optional CUTOVER_REHEARSAL_EMAIL).
|
|
Never passes -confirm / confirm=true (assign/promote/orphan deletes stay separate).`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const env = loadRootEnv(process.env);
|
|
const apiDir = path.join(repoRoot, "apps", "api");
|
|
const failures = [];
|
|
|
|
function section(title) {
|
|
console.log(`\n==> ${title}`);
|
|
}
|
|
|
|
function pass(msg) {
|
|
console.log(`PASS ${msg}`);
|
|
}
|
|
|
|
function fail(msg) {
|
|
console.log(`FAIL ${msg}`);
|
|
failures.push(msg);
|
|
}
|
|
|
|
function info(msg) {
|
|
console.log(`NOTE ${msg}`);
|
|
}
|
|
|
|
function run(cmd, argv, opts = {}) {
|
|
const r = spawnSync(cmd, argv, {
|
|
cwd: opts.cwd ?? repoRoot,
|
|
env,
|
|
encoding: "utf8",
|
|
shell: process.platform === "win32",
|
|
stdio: "inherit",
|
|
});
|
|
if (r.error) {
|
|
fail(`${opts.label ?? cmd}: spawn failed: ${r.error.message}`);
|
|
return false;
|
|
}
|
|
if (r.status !== 0) {
|
|
fail(`${opts.label ?? cmd}: exited ${r.status}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function runMigratorList(flag, label) {
|
|
section(label);
|
|
if (!env.DATABASE_URL) {
|
|
fail(`DATABASE_URL unset - cannot run ${flag}`);
|
|
return;
|
|
}
|
|
const ok = run(
|
|
"go",
|
|
["run", "./cmd/migrator", flag, "-postgres", env.DATABASE_URL],
|
|
{ cwd: apiDir, label: flag },
|
|
);
|
|
if (ok) pass(`${flag} ok`);
|
|
}
|
|
|
|
function cookieMapFromResponse(res, jar) {
|
|
const raw =
|
|
typeof res.headers.getSetCookie === "function"
|
|
? res.headers.getSetCookie()
|
|
: [];
|
|
const single = res.headers.get("set-cookie");
|
|
const lines = raw.length > 0 ? raw : single ? [single] : [];
|
|
for (const line of lines) {
|
|
const part = String(line).split(";")[0];
|
|
const eq = part.indexOf("=");
|
|
if (eq <= 0) continue;
|
|
jar.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
}
|
|
return jar;
|
|
}
|
|
|
|
function cookieHeader(jar) {
|
|
return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
|
|
}
|
|
|
|
async function platformLogin(base) {
|
|
const email =
|
|
env.CUTOVER_REHEARSAL_EMAIL || env.DEMO_EMAIL || "demo@descrybe.local";
|
|
const password = env.CUTOVER_REHEARSAL_PASS || env.DESCRYBE_SMOKE_PASS || "";
|
|
if (!password) {
|
|
return {
|
|
ok: false,
|
|
reason:
|
|
"set DESCRYBE_SMOKE_PASS or CUTOVER_REHEARSAL_PASS for platform-admin orphan dry-run",
|
|
};
|
|
}
|
|
const jar = new Map();
|
|
const me = await fetch(`${base}/api/auth/me`, {
|
|
headers: { Cookie: cookieHeader(jar) },
|
|
});
|
|
cookieMapFromResponse(me, jar);
|
|
const csrf = jar.get("descrybe_csrf");
|
|
if (!csrf) {
|
|
return { ok: false, reason: "missing descrybe_csrf cookie from /api/auth/me" };
|
|
}
|
|
const login = await fetch(`${base}/api/auth/login`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRF-Token": csrf,
|
|
Cookie: cookieHeader(jar),
|
|
},
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
cookieMapFromResponse(login, jar);
|
|
const text = await login.text();
|
|
if (!login.ok) {
|
|
return {
|
|
ok: false,
|
|
reason: `login ${login.status}: ${text.slice(0, 200)}`,
|
|
};
|
|
}
|
|
return { ok: true, jar, email };
|
|
}
|
|
|
|
async function orphanDryRun() {
|
|
section("orphan-processed dry-run (no confirm)");
|
|
if (skipOrphan) {
|
|
info("skipped (--skip-orphan)");
|
|
return;
|
|
}
|
|
const base = (
|
|
env.HEALTH_BASE_URL ||
|
|
env.PUBLIC_API_URL ||
|
|
DEV_PUBLIC_API_URL ||
|
|
`http://127.0.0.1:${DEV_API_PORT}`
|
|
).replace(/\/$/, "");
|
|
|
|
let session;
|
|
try {
|
|
session = await platformLogin(base);
|
|
} catch (err) {
|
|
fail(
|
|
`orphan login failed: ${err instanceof Error ? err.message : err} (is API up?)`,
|
|
);
|
|
return;
|
|
}
|
|
if (!session.ok) {
|
|
fail(`orphan dry-run skipped: ${session.reason}`);
|
|
info(
|
|
`Manual: GET ${base}/api/admin/jobs/orphan-processed or POST …/orphan-processed-cleanup (no confirm=true)`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const csrf = session.jar.get("descrybe_csrf");
|
|
const url = `${base}/api/admin/jobs/orphan-processed-cleanup`;
|
|
let res;
|
|
let text;
|
|
try {
|
|
res = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"X-CSRF-Token": csrf || "",
|
|
Cookie: cookieHeader(session.jar),
|
|
},
|
|
body: "{}",
|
|
});
|
|
text = await res.text();
|
|
} catch (err) {
|
|
fail(`orphan POST failed: ${err instanceof Error ? err.message : err}`);
|
|
return;
|
|
}
|
|
console.log(`POST ${url} -> ${res.status}`);
|
|
console.log(text);
|
|
if (!res.ok) {
|
|
fail(`orphan cleanup HTTP ${res.status}`);
|
|
return;
|
|
}
|
|
pass(`orphan dry-run report-only (as ${session.email}; no confirm)`);
|
|
}
|
|
|
|
console.log(
|
|
"cutover-local-rehearsal (deploy-check + list-* + orphan dry-run; no Clerk/SMTP/Stripe)",
|
|
);
|
|
|
|
if (!skipDeploy) {
|
|
section("deploy-check");
|
|
const ok = run(
|
|
"node",
|
|
[path.join("scripts", "cutover-deploy-check.mjs"), ...deployPassthrough],
|
|
{ label: "cutover-deploy-check" },
|
|
);
|
|
if (ok) pass("deploy-check ok");
|
|
} else {
|
|
section("deploy-check");
|
|
info("skipped (--skip-deploy-check)");
|
|
}
|
|
|
|
if (!skipLists) {
|
|
runMigratorList("-list-legacy-emails", "list-legacy-emails (no patch)");
|
|
runMigratorList("-list-member-memberships", "list-member-memberships");
|
|
runMigratorList(
|
|
"-list-companies-without-plans",
|
|
"list-companies-without-plans",
|
|
);
|
|
} else {
|
|
section("migrator lists");
|
|
info("skipped (--skip-lists)");
|
|
}
|
|
|
|
await orphanDryRun();
|
|
|
|
section("summary");
|
|
if (failures.length === 0) {
|
|
console.log(
|
|
"ok - local rehearsal green (lists + deploy-check + orphan dry-run).",
|
|
);
|
|
console.log(
|
|
"Not included: Clerk patch, SMTP/mailhooks, Stripe, assign/promote -confirm.",
|
|
);
|
|
process.exit(0);
|
|
}
|
|
console.log(`${failures.length} failure(s):`);
|
|
for (const f of failures) console.log(` - ${f}`);
|
|
process.exit(1);
|