实现(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 行删除
+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,