实现(08-08-production-deployment-execution): 校准生产身份只读门禁
这个提交包含在:
@@ -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,
|
||||
|
||||
在新工单中引用
屏蔽一个用户