实现(08-08-production-deployment-execution): 校准生产身份只读门禁
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-08 17:33:25 +08:00
父节点 6ccb9cd5e3
当前提交 f11e2f2c25
共修改 11 个文件,包含 666 行新增和 30 行删除
+29 -5
查看文件
@@ -42,15 +42,28 @@ deployment_contract_load "$DEPLOY_SERVER" "$DEPLOY_CONTRACT_UNIT" contract \
echo "[deploy-existing] 已读取受控 unit 合同;路径与标识不回显"
if [[ -z "${EXPECTED_PRODUCTION_ORIGIN+x}" ]]; then
if [[ -z "${EXPECTED_PRODUCTION_ORIGIN:-}" ]]; then
fail "生产部署必须显式提供 EXPECTED_PRODUCTION_ORIGIN(仅进程环境)"
fi
if [[ -z "${EXPECTED_PRODUCTION_DB_NAME+x}" ]]; then
fail "生产部署必须显式提供 EXPECTED_PRODUCTION_DB_NAME(仅进程环境)"
if [[ -z "${EXPECTED_PRODUCTION_DB_NAME_SHA256:-}" ]]; then
fail "生产部署必须由身份引导器提供 EXPECTED_PRODUCTION_DB_NAME_SHA256"
fi
if [[ -z "${EXPECTED_PENDING_MIGRATIONS+x}" ]]; then
fail "生产部署必须显式提供 EXPECTED_PENDING_MIGRATIONS(可为空)"
fi
if [[ -z "${EXPECTED_DEPLOY_SERVER:-}" ]]; then
fail "生产部署必须由身份引导器提供 EXPECTED_DEPLOY_SERVER"
fi
if [[ -z "${EXPECTED_REMOTE_DIR:-}" ]]; then
fail "生产部署必须由身份引导器提供 EXPECTED_REMOTE_DIR"
fi
if [[ -z "${EXPECTED_COMPOSE_PROJECT_NAME:-}" ]]; then
fail "生产部署必须由身份引导器提供 EXPECTED_COMPOSE_PROJECT_NAME"
fi
case "${PRODUCTION_READ_ONLY_PREFLIGHT:-0}" in
0|1) ;;
*) fail "PRODUCTION_READ_ONLY_PREFLIGHT 只能为 0 或 1" ;;
esac
[[ -f "$ROOT_DIR/$PROVENANCE_SCRIPT" ]] || fail "缺少发布来源校验脚本"
[[ -f "$ROOT_DIR/$PRODUCTION_PREFLIGHT_SCRIPT" ]] || fail "缺少生产迁移 preflight 脚本"
@@ -64,8 +77,11 @@ source_commit="$(node -e 'const value = JSON.parse(process.argv[1]); process.std
echo ">>> 0.2 生产目标与迁移 preflight(只读,先于 deploy.sh)"
PRODUCTION_PREFLIGHT_REPORT_ROOT="${PRODUCTION_PREFLIGHT_REPORT_ROOT:-artifacts/production-preflight-$(date +%Y%m%d-%H%M%S)}"
EXPECTED_PRODUCTION_ORIGIN="$EXPECTED_PRODUCTION_ORIGIN" \
EXPECTED_PRODUCTION_DB_NAME="$EXPECTED_PRODUCTION_DB_NAME" \
EXPECTED_PRODUCTION_DB_NAME_SHA256="$EXPECTED_PRODUCTION_DB_NAME_SHA256" \
EXPECTED_PENDING_MIGRATIONS="$EXPECTED_PENDING_MIGRATIONS" \
EXPECTED_DEPLOY_SERVER="$EXPECTED_DEPLOY_SERVER" \
EXPECTED_REMOTE_DIR="$EXPECTED_REMOTE_DIR" \
EXPECTED_COMPOSE_PROJECT_NAME="$EXPECTED_COMPOSE_PROJECT_NAME" \
DEPLOY_SERVER="$DEPLOY_SERVER" \
REMOTE_DIR="${contract[REMOTE_DIR]}" \
REMOTE_ENV_FILE="${contract[REMOTE_ENV_FILE]}" \
@@ -78,6 +94,11 @@ PRODUCTION_PREFLIGHT_REPORT_DIR="$PRODUCTION_PREFLIGHT_REPORT_ROOT/pre" \
node "$ROOT_DIR/$PRODUCTION_PREFLIGHT_SCRIPT" \
|| fail "生产目标或迁移 preflight 未通过;未进入 deploy.sh"
if [[ "${PRODUCTION_READ_ONLY_PREFLIGHT:-0}" == "1" ]]; then
echo "[deploy-existing][PASS] 只读身份、迁移与回滚窗口门禁通过;未进入 deploy.sh"
exit 0
fi
DEPLOY_SERVER="$DEPLOY_SERVER" \
REMOTE_DIR="${contract[REMOTE_DIR]}" \
REMOTE_ENV_FILE="${contract[REMOTE_ENV_FILE]}" \
@@ -93,8 +114,11 @@ bash "$ROOT_DIR/deploy/trialrun/deploy.sh"
echo ">>> 6.1 部署后 migration status 与目标只读复核"
EXPECTED_PRODUCTION_ORIGIN="$EXPECTED_PRODUCTION_ORIGIN" \
EXPECTED_PRODUCTION_DB_NAME="$EXPECTED_PRODUCTION_DB_NAME" \
EXPECTED_PRODUCTION_DB_NAME_SHA256="$EXPECTED_PRODUCTION_DB_NAME_SHA256" \
EXPECTED_PENDING_MIGRATIONS="" \
EXPECTED_DEPLOY_SERVER="$EXPECTED_DEPLOY_SERVER" \
EXPECTED_REMOTE_DIR="$EXPECTED_REMOTE_DIR" \
EXPECTED_COMPOSE_PROJECT_NAME="$EXPECTED_COMPOSE_PROJECT_NAME" \
DEPLOY_SERVER="$DEPLOY_SERVER" \
REMOTE_DIR="${contract[REMOTE_DIR]}" \
REMOTE_ENV_FILE="${contract[REMOTE_ENV_FILE]}" \
+95
查看文件
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Read-only identity bootstrap. The caller captures the database identity in
# memory and must never forward this script's stdout to an ordinary log.
set -euo pipefail
fail() {
printf '[production-identity-bootstrap][FAIL] code=%s\n' "$1" >&2
exit 1
}
for key in REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE COMPOSE_PROJECT_NAME \
EXPECTED_PRODUCTION_ORIGIN_SHA256; do
[[ -n "${!key:-}" ]] || fail "${key}_MISSING"
done
[[ "$EXPECTED_PRODUCTION_ORIGIN_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|| fail "PROD_TARGET_ORIGIN_HASH_INVALID"
cd "$REMOTE_DIR"
[[ -f "$REMOTE_ENV_FILE" && ! -L "$REMOTE_ENV_FILE" && -r "$REMOTE_ENV_FILE" ]] \
|| fail "PROD_TARGET_ENV_UNREADABLE"
dotenv_value() {
local key="$1"
local value
value="$(awk -v wanted="$key" '
$0 !~ /^[[:space:]]*#/ && $0 ~ "^[[:space:]]*" wanted "=" {
sub("^[[:space:]]*" wanted "=", "")
print
exit
}
' "$REMOTE_ENV_FILE")" || return 1
value="${value%$'\r'}"
case "$value" in
\"*\") value="${value:1:${#value}-2}" ;;
\'*\') value="${value:1:${#value}-2}" ;;
esac
[[ -n "$value" ]] || return 1
printf '%s' "$value"
}
hash_text() {
if command -v sha256sum >/dev/null 2>&1; then
printf '%s' "$1" | sha256sum | awk '{print $1}'
return 0
fi
if command -v shasum >/dev/null 2>&1; then
printf '%s' "$1" | shasum -a 256 | awk '{print $1}'
return 0
fi
return 1
}
reject_non_production_identity() {
local value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
[[ "$value" != *"<"* && "$value" != *">"* ]] || return 1
[[ "$value" != *"localhost"* && "$value" != *"127.0.0.1"* \
&& "$value" != *"::1"* && "$value" != *"hyb_oa_regression"* \
&& "$value" != *"staging"* && "$value" != *"stage"* \
&& "$value" != *"regression"* && "$value" != *"shadow"* \
&& "$value" != *"/test"* ]]
}
actual_origin="$(dotenv_value APP_ORIGIN)" || fail "PROD_TARGET_ORIGIN_MISSING"
actual_db_name="$(dotenv_value POSTGRES_DB)" || fail "PROD_TARGET_DB_NAME_MISSING"
[[ "$(hash_text "$actual_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|| fail "PROD_TARGET_ORIGIN_MISMATCH"
reject_non_production_identity "$actual_db_name" \
|| fail "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY"
if docker compose version >/dev/null 2>&1; then
compose_cmd=(docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE")
elif command -v docker-compose >/dev/null 2>&1; then
compose_cmd=(docker-compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE")
else
fail "PROD_TARGET_COMPOSE_UNAVAILABLE"
fi
mapfile -t server_ids < <(
docker ps \
--filter "label=com.docker.compose.project=$COMPOSE_PROJECT_NAME" \
--filter "label=com.docker.compose.service=server" \
--format '{{.ID}}' 2>/dev/null || true
)
[[ "${#server_ids[@]}" -eq 1 ]] || fail "PROD_TARGET_RUNTIME_NOT_UNIQUE"
runtime_origin="$(
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "${server_ids[0]}" \
| sed -n 's/^APP_ORIGIN=//p' | tail -n 1
)"
[[ "$(hash_text "$runtime_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|| fail "PROD_TARGET_RUNTIME_ORIGIN_MISMATCH"
# Only the comparison baseline crosses the SSH channel. The database name
# itself remains remote and cannot be reconstructed from task evidence.
printf '[production-identity-bootstrap][DB_SHA256] %s\n' "$(hash_text "$actual_db_name")"
+242
查看文件
@@ -0,0 +1,242 @@
#!/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";
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 remoteDir = parseHistoricalDefault(targetSource, "REMOTE_DIR");
const projectRemoteDir = parseHistoricalDefault(projectSource, "REMOTE_DIR");
const composeProjectName = parseHistoricalDefault(projectSource, "COMPOSE_PROJECT_NAME");
if (projectRemoteDir !== remoteDir) fail("HISTORICAL_REMOTE_DIR_DIVERGED");
if (!SAFE_SSH_TARGET_PATTERN.test(deployServer)) fail("HISTORICAL_DEPLOY_SERVER_INVALID");
if (!SAFE_ABSOLUTE_PATH_PATTERN.test(remoteDir)) fail("HISTORICAL_REMOTE_DIR_INVALID");
if (!SAFE_PROJECT_PATTERN.test(composeProjectName)) fail("HISTORICAL_COMPOSE_PROJECT_INVALID");
return { deployServer, remoteDir, 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 assertExactHistoricalContract(identity, contract) {
if (contract.REMOTE_DIR !== identity.remoteDir) fail("SYSTEMD_REMOTE_DIR_MISMATCH");
if (contract.REMOTE_ENV_FILE !== `${identity.remoteDir}/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 bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
const remoteCommand = [
`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");
}
const output = String(result.stdout ?? "").trim();
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 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);
assertExactHistoricalContract(identity, contract);
const databaseIdentityHash = bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin);
console.log("[production-identity-bootstrap][PASS] exact_identity_group");
const result = spawnSync("bash", [DEPLOY_EXISTING_SCRIPT], {
cwd: ROOT_DIR,
stdio: "inherit",
env: {
...process.env,
DEPLOY_SERVER: identity.deployServer,
DEPLOY_REFRESH_SERVER: refreshTarget,
EXPECTED_DEPLOY_SERVER: identity.deployServer,
EXPECTED_REMOTE_DIR: identity.remoteDir,
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.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();
@@ -65,8 +65,8 @@ reject_non_production_identity() {
[[ "$value" != *"<"* && "$value" != *">"* ]] || return 1
[[ "$value" != *"localhost"* && "$value" != *"127.0.0.1"* \
&& "$value" != *"::1"* && "$value" != *"hyb_oa_regression"* \
&& "$value" != *"trialrun"* && "$value" != *"staging"* \
&& "$value" != *"stage"* && "$value" != *"regression"* \
&& "$value" != *"staging"* && "$value" != *"stage"* \
&& "$value" != *"regression"* \
&& "$value" != *"shadow"* && "$value" != *"/test"* ]]
}
+46 -6
查看文件
@@ -16,7 +16,7 @@ const 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;
const FORBIDDEN_IDENTITY_TOKEN_PATTERN = /(^|[._-])(local|localhost|dev|development|test|testing|stage|staging|trialrun|regression|shadow|example)([._-]|$)/u;
const FORBIDDEN_IDENTITY_TOKEN_PATTERN = /(^|[._/-])(local|localhost|dev|development|test|testing|stage|staging|regression|shadow|example)([._/-]|$)/u;
const PRISMA_STATUS_KNOWN_PATTERN = /(?:database schema is up to date|have not yet been applied|no pending migrations)/iu;
export class ProductionPreflightError extends Error {
@@ -135,6 +135,34 @@ function assertSafeContractValue(name, value, pattern) {
if (!pattern.test(String(value ?? ""))) fail(`${name}_INVALID`);
}
function assertProductionIdentityValue(name, value) {
const normalized = String(value ?? "").trim().toLowerCase();
if (!normalized || normalized.includes("<") || normalized.includes(">") || FORBIDDEN_IDENTITY_TOKEN_PATTERN.test(normalized)) {
fail(`${name}_NON_PRODUCTION_IDENTITY`);
}
}
function assertExactIdentity(name, actual, expected) {
if (expected == null || !String(expected).trim()) fail(`EXPECTED_${name}_MISSING`);
if (actual !== String(expected).trim()) fail(`${name}_MISMATCH`);
}
export function validateExactProductionIdentity({
deployServer,
remoteDir,
composeProjectName,
expectedDeployServer,
expectedRemoteDir,
expectedComposeProjectName,
}) {
assertProductionIdentityValue("DEPLOY_SERVER", deployServer);
assertProductionIdentityValue("REMOTE_DIR", remoteDir);
assertProductionIdentityValue("COMPOSE_PROJECT_NAME", composeProjectName);
assertExactIdentity("DEPLOY_SERVER", deployServer, expectedDeployServer);
assertExactIdentity("REMOTE_DIR", remoteDir, expectedRemoteDir);
assertExactIdentity("COMPOSE_PROJECT_NAME", composeProjectName, expectedComposeProjectName);
}
function gitText(args) {
const result = spawnSync("git", args, {
cwd: ROOT_DIR,
@@ -216,14 +244,18 @@ function main() {
const postgresDataDir = String(process.env.POSTGRES_DATA_DIR ?? "").trim();
const storageHostDir = String(process.env.STORAGE_HOST_DIR ?? "").trim();
const expectedOriginRaw = process.env.EXPECTED_PRODUCTION_ORIGIN;
const expectedDbNameRaw = process.env.EXPECTED_PRODUCTION_DB_NAME;
const expectedDbNameHashRaw = process.env.EXPECTED_PRODUCTION_DB_NAME_SHA256;
const expectedPendingRaw = process.env.EXPECTED_PENDING_MIGRATIONS;
const expectedDeployServer = process.env.EXPECTED_DEPLOY_SERVER;
const expectedRemoteDir = process.env.EXPECTED_REMOTE_DIR;
const expectedComposeProjectName = process.env.EXPECTED_COMPOSE_PROJECT_NAME;
if (expectedOriginRaw == null) fail("EXPECTED_PRODUCTION_ORIGIN_MISSING");
if (expectedDbNameRaw == null) fail("EXPECTED_PRODUCTION_DB_NAME_MISSING");
if (expectedDbNameHashRaw == null) fail("EXPECTED_PRODUCTION_DB_NAME_SHA256_MISSING");
if (expectedPendingRaw == null) fail("EXPECTED_PENDING_MIGRATIONS_MISSING");
const expectedOrigin = validateProductionOrigin(expectedOriginRaw);
const expectedDbName = validateProductionDbName(expectedDbNameRaw);
const expectedDbNameHash = String(expectedDbNameHashRaw).trim();
if (!/^[0-9a-f]{64}$/u.test(expectedDbNameHash)) fail("EXPECTED_PRODUCTION_DB_NAME_SHA256_INVALID");
const expectedPending = parseMigrationList(expectedPendingRaw, {
field: "EXPECTED_PENDING_MIGRATIONS",
});
@@ -234,6 +266,14 @@ function main() {
assertSafeContractValue("STORAGE_HOST_DIR", storageHostDir, SAFE_ABSOLUTE_PATH_PATTERN);
assertSafeContractValue("COMPOSE_FILE", composeFile, SAFE_COMPOSE_FILE_PATTERN);
assertSafeContractValue("COMPOSE_PROJECT_NAME", composeProjectName, SAFE_PROJECT_PATTERN);
validateExactProductionIdentity({
deployServer,
remoteDir,
composeProjectName,
expectedDeployServer,
expectedRemoteDir,
expectedComposeProjectName,
});
const candidateCommit = String(
process.env.CANDIDATE_COMMIT ?? gitText(["rev-parse", "--verify", "HEAD"]),
@@ -266,7 +306,7 @@ function main() {
`POSTGRES_DATA_DIR=${shellQuote(postgresDataDir)}`,
`STORAGE_HOST_DIR=${shellQuote(storageHostDir)}`,
`EXPECTED_PRODUCTION_ORIGIN_SHA256=${shellQuote(sha256Text(expectedOrigin))}`,
`EXPECTED_PRODUCTION_DB_NAME_SHA256=${shellQuote(sha256Text(expectedDbName))}`,
`EXPECTED_PRODUCTION_DB_NAME_SHA256=${shellQuote(expectedDbNameHash)}`,
`CANDIDATE_MIGRATIONS_CSV=${shellQuote(candidateManifest.join(","))}`,
`EXPECTED_PENDING_MIGRATIONS_CSV=${shellQuote(expectedPending.join(","))}`,
"bash -s",
@@ -283,7 +323,7 @@ function main() {
const rawStatus = extractPrismaStatus(stdout);
if (rawStatus != null) {
validatePrismaStatusText(rawStatus);
const sanitizedStatus = sanitizePrismaStatus(rawStatus, [expectedOrigin, expectedDbName]);
const sanitizedStatus = sanitizePrismaStatus(rawStatus, [expectedOrigin]);
writeFileSync(path.join(reportDir, "prisma-migrate-status.txt"), `${sanitizedStatus}\n`, {
encoding: "utf8",
mode: 0o600,
+47 -1
查看文件
@@ -43,6 +43,14 @@ const productionPreflightRemoteScriptPath = path.join(
rootDir,
'deploy/trialrun/production-preflight-remote.sh',
);
const productionIdentityBootstrapScriptPath = path.join(
rootDir,
'deploy/trialrun/production-identity-bootstrap.mjs',
);
const productionIdentityBootstrapRemoteScriptPath = path.join(
rootDir,
'deploy/trialrun/production-identity-bootstrap-remote.sh',
);
const dockerfilePath = path.join(rootDir, 'deploy/trialrun/Dockerfile');
const timerScriptPath = path.resolve(
process.env.ENV_CONTRACT_TIMER_SCRIPT_PATH
@@ -279,6 +287,8 @@ const deploymentRefreshHelperContent = readFile(deploymentRefreshHelperPath);
const remotePreflightScriptContent = readFile(remotePreflightScriptPath);
const productionPreflightScriptContent = readFile(productionPreflightScriptPath);
const productionPreflightRemoteScriptContent = readFile(productionPreflightRemoteScriptPath);
const productionIdentityBootstrapScriptContent = readFile(productionIdentityBootstrapScriptPath);
const productionIdentityBootstrapRemoteScriptContent = readFile(productionIdentityBootstrapRemoteScriptPath);
const dockerfileContent = readFile(dockerfilePath);
const timerScriptContent = readFile(timerScriptPath);
const automaticBackupTimerScriptContent = readFile(automaticBackupTimerScriptPath);
@@ -604,8 +614,12 @@ for (const requiredFragment of [
for (const requiredFragment of [
'EXPECTED_PRODUCTION_ORIGIN',
'EXPECTED_PRODUCTION_DB_NAME',
'EXPECTED_PRODUCTION_DB_NAME_SHA256',
'EXPECTED_PENDING_MIGRATIONS',
'EXPECTED_DEPLOY_SERVER',
'EXPECTED_REMOTE_DIR',
'EXPECTED_COMPOSE_PROJECT_NAME',
'PRODUCTION_READ_ONLY_PREFLIGHT',
'production-preflight.mjs',
'CANDIDATE_COMMIT',
'PRODUCTION_PREFLIGHT_REPORT_DIR',
@@ -616,6 +630,34 @@ for (const requiredFragment of [
}
}
for (const requiredFragment of [
'HISTORICAL_TARGET_COMMIT',
'HISTORICAL_PROJECT_COMMIT',
'discoverRefreshTarget',
'assertExactHistoricalContract',
'bootstrapDatabaseIdentityHash',
'EXPECTED_PRODUCTION_DB_NAME_SHA256',
'DEPLOY_EXISTING_SCRIPT',
]) {
if (!productionIdentityBootstrapScriptContent.includes(requiredFragment)) {
errors.push(`production-identity-bootstrap.mjs 缺少生产身份引导合同: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'EXPECTED_PRODUCTION_ORIGIN_SHA256',
'PROD_TARGET_RUNTIME_ORIGIN_MISMATCH',
'PROD_TARGET_DB_NON_PRODUCTION_IDENTITY',
'DB_SHA256',
]) {
if (!productionIdentityBootstrapRemoteScriptContent.includes(requiredFragment)) {
errors.push(`production-identity-bootstrap-remote.sh 缺少只读身份合同: ${requiredFragment}`);
}
}
if (productionIdentityBootstrapRemoteScriptContent.includes('prisma migrate status')
|| productionIdentityBootstrapRemoteScriptContent.includes('migrate deploy')) {
errors.push('production-identity-bootstrap-remote.sh 不得进入 Prisma status 或迁移写入');
}
for (const requiredFragment of [
'systemctl show',
'DEPLOY_CONTRACT_KEYS',
@@ -735,6 +777,10 @@ if (deployExistingScriptContent.indexOf('node "$ROOT_DIR/$PRODUCTION_PREFLIGHT_S
> deployExistingScriptContent.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"')) {
errors.push('deploy-existing.sh 必须在 deploy.sh 前执行生产目标与迁移 preflight');
}
if (deployExistingScriptContent.indexOf('PRODUCTION_READ_ONLY_PREFLIGHT')
> deployExistingScriptContent.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"')) {
errors.push('deploy-existing.sh 的只读停止点必须位于 deploy.sh 前');
}
if (!deployScriptContent.includes('DEPLOY_STATUS_OUTPUT_MODE')) {
errors.push('deploy.sh 必须支持受控发布 wrapper 的 quiet migration status 输出模式');
}