#!/usr/bin/env node import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { homedir } from "node:os"; import path from "node:path"; import process from "node:process"; import { spawnSync } from "node:child_process"; import { fileURLToPath, pathToFileURL } from "node:url"; import { deploymentReadOnlyMode, requireReleaseReceipt, ReleaseGateError } from "../../scripts/qa/release-gate-result.mjs"; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = path.resolve(SCRIPT_DIR, "../.."); const HISTORICAL_TARGET_COMMIT = "34181e3"; const HISTORICAL_PROJECT_COMMIT = "5e2d038938265c926e06d3cc6d0841f6dcc06148"; const DEPLOY_SCRIPT_AT_COMMIT = "deploy/trialrun/deploy.sh"; const RUNBOOK_PATH = path.join(ROOT_DIR, "docs/trialrun/OPS7运维交接Runbook.md"); const REMOTE_BOOTSTRAP_SCRIPT = path.join(SCRIPT_DIR, "production-identity-bootstrap-remote.sh"); const DEPLOY_EXISTING_SCRIPT = path.join(SCRIPT_DIR, "deploy-existing.sh"); const DEFAULT_CONTRACT_UNIT = "hyb-oa-automatic-backup-daily.service"; const EXACT_REFRESH_PROBE = "[deployment-contract-refresh][PASS] probe"; const SAFE_SSH_TARGET_PATTERN = /^(?:[A-Za-z0-9._-]+@)?[A-Za-z0-9._-]+$/u; const SAFE_ABSOLUTE_PATH_PATTERN = /^\/[A-Za-z0-9._/-]+$/u; const SAFE_COMPOSE_FILE_PATTERN = /^[A-Za-z0-9._/-]+$/u; const SAFE_PROJECT_PATTERN = /^[a-z0-9][a-z0-9_-]*$/u; export class ProductionIdentityBootstrapError extends Error { constructor(code) { super(code); this.name = "ProductionIdentityBootstrapError"; this.code = code; } } function fail(code) { throw new ProductionIdentityBootstrapError(code); } function run(command, args, options = {}) { return spawnSync(command, args, { cwd: ROOT_DIR, encoding: "utf8", stdio: [options.input == null ? "ignore" : "pipe", "pipe", "pipe"], input: options.input, env: options.env ?? process.env, }); } function requiredCommandOutput(command, args, code) { const result = run(command, args); if (result.status !== 0 || !String(result.stdout ?? "").trim()) fail(code); return String(result.stdout).trim(); } function parseHistoricalDefault(source, key) { const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); const match = source.match(new RegExp(`^${escapedKey}="\\$\\{${escapedKey}:-([^}]+)\\}"$`, "mu")); if (!match?.[1]) fail(`HISTORICAL_${key}_UNAVAILABLE`); return match[1].trim(); } export function parseHistoricalDeploymentIdentity(targetSource, projectSource) { const deployServer = parseHistoricalDefault(targetSource, "DEPLOY_SERVER"); const composeProjectName = parseHistoricalDefault(projectSource, "COMPOSE_PROJECT_NAME"); if (!SAFE_SSH_TARGET_PATTERN.test(deployServer)) fail("HISTORICAL_DEPLOY_SERVER_INVALID"); if (!SAFE_PROJECT_PATTERN.test(composeProjectName)) fail("HISTORICAL_COMPOSE_PROJECT_INVALID"); return { deployServer, composeProjectName }; } export function parseExpectedProductionOrigin(runbook) { const matches = [...String(runbook).matchAll(/curl\s+-fsS\s+(https:\/\/[^\s/]+)\/ready\b/gu)] .map((match) => match[1]); const unique = [...new Set(matches)]; if (unique.length !== 1) fail("EXPECTED_PRODUCTION_ORIGIN_NOT_UNIQUE"); return unique[0]; } export function parseConcreteSshHosts(config) { const hosts = []; for (const match of String(config).matchAll(/^\s*Host\s+(.+)$/gmi)) { for (const host of match[1].trim().split(/\s+/u)) { if (host && !/[*!?]/u.test(host) && SAFE_SSH_TARGET_PATTERN.test(host)) hosts.push(host); } } return [...new Set(hosts)]; } export function selectUniqueRefreshTarget(probeResults) { const matches = probeResults.filter((item) => item.status === 0 && item.stdout.trim() === EXACT_REFRESH_PROBE); if (matches.length === 0) fail("REFRESH_IDENTITY_NOT_FOUND"); if (matches.length !== 1) fail("REFRESH_IDENTITY_NOT_UNIQUE"); return matches[0].target; } function discoverRefreshTarget() { const sshConfigPath = path.join(homedir(), ".ssh", "config"); let config; try { config = readFileSync(sshConfigPath, "utf8"); } catch { fail("SSH_CONFIG_UNREADABLE"); } const candidates = parseConcreteSshHosts(config); if (candidates.length === 0) fail("SSH_CONFIG_HAS_NO_CONCRETE_HOSTS"); const probeResults = candidates.map((target) => { const result = run("ssh", [ "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", target, "probe", ]); return { target, status: result.status, stdout: String(result.stdout ?? "") }; }); return selectUniqueRefreshTarget(probeResults); } export function parseDeploymentContract(raw) { const allowed = new Set([ "REMOTE_DIR", "REMOTE_ENV_FILE", "COMPOSE_FILE", "COMPOSE_PROJECT_NAME", "POSTGRES_DATA_DIR", "STORAGE_HOST_DIR", ]); const contract = {}; for (const entry of String(raw).trim().split(/\s+/u)) { const separator = entry.indexOf("="); if (separator <= 0) continue; const key = entry.slice(0, separator); if (allowed.has(key)) contract[key] = entry.slice(separator + 1); } for (const key of allowed) { if (!contract[key]) fail("SYSTEMD_CONTRACT_INCOMPLETE"); } if (!SAFE_ABSOLUTE_PATH_PATTERN.test(contract.REMOTE_DIR)) fail("SYSTEMD_REMOTE_DIR_INVALID"); if (!SAFE_ABSOLUTE_PATH_PATTERN.test(contract.REMOTE_ENV_FILE)) fail("SYSTEMD_REMOTE_ENV_FILE_INVALID"); if (!SAFE_ABSOLUTE_PATH_PATTERN.test(contract.POSTGRES_DATA_DIR)) fail("SYSTEMD_POSTGRES_DATA_DIR_INVALID"); if (!SAFE_ABSOLUTE_PATH_PATTERN.test(contract.STORAGE_HOST_DIR)) fail("SYSTEMD_STORAGE_HOST_DIR_INVALID"); if (!SAFE_COMPOSE_FILE_PATTERN.test(contract.COMPOSE_FILE)) fail("SYSTEMD_COMPOSE_FILE_INVALID"); if (!SAFE_PROJECT_PATTERN.test(contract.COMPOSE_PROJECT_NAME)) fail("SYSTEMD_COMPOSE_PROJECT_INVALID"); return contract; } export function assertContractForOriginVerification(identity, contract) { if (contract.REMOTE_ENV_FILE !== `${contract.REMOTE_DIR}/deploy/trialrun/.env`) { fail("SYSTEMD_REMOTE_ENV_FILE_MISMATCH"); } if (contract.COMPOSE_FILE !== "deploy/trialrun/docker-compose.yml") { fail("SYSTEMD_COMPOSE_FILE_MISMATCH"); } if (contract.COMPOSE_PROJECT_NAME !== identity.composeProjectName) { fail("SYSTEMD_COMPOSE_PROJECT_MISMATCH"); } } function sha256Text(value) { return createHash("sha256").update(String(value), "utf8").digest("hex"); } function shellQuote(value) { return `'${String(value).replaceAll("'", "'\\''")}'`; } function runRemoteIdentityBootstrap(mode, identity, contract, expectedOrigin) { const remoteCommand = [ `IDENTITY_BOOTSTRAP_MODE=${shellQuote(mode)}`, `REMOTE_DIR=${shellQuote(contract.REMOTE_DIR)}`, `REMOTE_ENV_FILE=${shellQuote(contract.REMOTE_ENV_FILE)}`, `COMPOSE_FILE=${shellQuote(contract.COMPOSE_FILE)}`, `COMPOSE_PROJECT_NAME=${shellQuote(contract.COMPOSE_PROJECT_NAME)}`, `EXPECTED_PRODUCTION_ORIGIN_SHA256=${shellQuote(sha256Text(expectedOrigin))}`, "bash -s", ].join(" "); const remoteScript = readFileSync(REMOTE_BOOTSTRAP_SCRIPT, "utf8"); const result = run("ssh", [identity.deployServer, remoteCommand], { input: remoteScript }); if (result.status !== 0) { const code = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/code=([A-Z0-9_]+)/u)?.[1]; fail(code || "REMOTE_IDENTITY_BOOTSTRAP_FAILED"); } return String(result.stdout ?? "").trim(); } function verifyOriginIdentity(identity, contract, expectedOrigin) { const output = runRemoteIdentityBootstrap("origin", identity, contract, expectedOrigin); if (output !== "[production-identity-bootstrap][PASS] origin_identity") { fail("REMOTE_ORIGIN_IDENTITY_PAYLOAD_INVALID"); } } function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) { const output = runRemoteIdentityBootstrap("database", identity, contract, expectedOrigin); const match = output.match(/^\[production-identity-bootstrap\]\[DB_SHA256\] ([0-9a-f]{64})$/u); if (!match) fail("REMOTE_DB_IDENTITY_PAYLOAD_INVALID"); return match[1]; } function main() { try { const identityOnly = process.env.PRODUCTION_IDENTITY_ONLY ?? "0"; if (!["0", "1"].includes(identityOnly)) fail("PRODUCTION_IDENTITY_ONLY_INVALID"); const { preflightOnly } = deploymentReadOnlyMode(process.env, true); // Reject before SSH configuration discovery, target probes or database reads. const release = identityOnly === "1" || preflightOnly ? null : requireReleaseReceipt(); const historicalTargetScript = requiredCommandOutput( "git", ["show", `${HISTORICAL_TARGET_COMMIT}:${DEPLOY_SCRIPT_AT_COMMIT}`], "HISTORICAL_IDENTITY_UNAVAILABLE", ); const historicalProjectScript = requiredCommandOutput( "git", ["show", `${HISTORICAL_PROJECT_COMMIT}:${DEPLOY_SCRIPT_AT_COMMIT}`], "HISTORICAL_PROJECT_IDENTITY_UNAVAILABLE", ); const identity = parseHistoricalDeploymentIdentity(historicalTargetScript, historicalProjectScript); const expectedOrigin = parseExpectedProductionOrigin(readFileSync(RUNBOOK_PATH, "utf8")); const refreshTarget = discoverRefreshTarget(); const contractRaw = requiredCommandOutput( "ssh", [identity.deployServer, `systemctl show '${DEFAULT_CONTRACT_UNIT}' -p Environment --value`], "SYSTEMD_CONTRACT_UNAVAILABLE", ); const contract = parseDeploymentContract(contractRaw); assertContractForOriginVerification(identity, contract); verifyOriginIdentity(identity, contract, expectedOrigin); const expectedRemoteDir = contract.REMOTE_DIR; if (identityOnly === "1") { console.log("[production-identity-bootstrap][PASS] origin_verified_remote_dir_baseline=systemd_contract"); return; } const databaseIdentityHash = bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin); console.log("[production-identity-bootstrap][PASS] exact_identity_group"); if (release) requireReleaseReceipt(release.directory, release.commit); const result = spawnSync("bash", [DEPLOY_EXISTING_SCRIPT], { cwd: ROOT_DIR, stdio: "inherit", env: { ...process.env, ...(release ? { RELEASE_GATE_RECEIPT_DIR: release.directory, RELEASE_GATE_EXPECTED_COMMIT: release.commit } : {}), DEPLOY_SERVER: identity.deployServer, DEPLOY_REFRESH_SERVER: refreshTarget, EXPECTED_DEPLOY_SERVER: identity.deployServer, EXPECTED_REMOTE_DIR: expectedRemoteDir, EXPECTED_COMPOSE_PROJECT_NAME: identity.composeProjectName, EXPECTED_PRODUCTION_ORIGIN: expectedOrigin, EXPECTED_PRODUCTION_DB_NAME_SHA256: databaseIdentityHash, EXPECTED_PENDING_MIGRATIONS: process.env.EXPECTED_PENDING_MIGRATIONS ?? "", PRODUCTION_READ_ONLY_PREFLIGHT: process.env.PRODUCTION_READ_ONLY_PREFLIGHT ?? "0", }, }); if (result.status !== 0) fail("DEPLOY_EXISTING_FAILED"); } catch (error) { const code = error instanceof ProductionIdentityBootstrapError || error instanceof ReleaseGateError ? error.code : "PRODUCTION_IDENTITY_BOOTSTRAP_UNEXPECTED_ERROR"; console.error(`[production-identity-bootstrap][PAUSED] code=${code}`); process.exitCode = 1; } } const isMain = process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href; if (isMain) main();