feat(08-08-production-deployment-validation): 增加生产目标与迁移只读门禁

这个提交包含在:
2026-08-08 11:11:19 +08:00
父节点 6bfa239410
当前提交 5e2d038938
共修改 6 个文件,包含 965 行新增和 3 行删除
+85
查看文件
@@ -38,6 +38,11 @@ const deploymentRefreshHelperPath = path.join(
'scripts/refresh-deployment-contract.sh',
);
const remotePreflightScriptPath = path.join(rootDir, 'deploy/trialrun/preflight-remote.sh');
const productionPreflightScriptPath = path.join(rootDir, 'deploy/trialrun/production-preflight.mjs');
const productionPreflightRemoteScriptPath = path.join(
rootDir,
'deploy/trialrun/production-preflight-remote.sh',
);
const dockerfilePath = path.join(rootDir, 'deploy/trialrun/Dockerfile');
const timerScriptPath = path.resolve(
process.env.ENV_CONTRACT_TIMER_SCRIPT_PATH
@@ -272,6 +277,8 @@ const deploymentPreflightScriptContent = readFile(deploymentPreflightScriptPath)
const deploymentRefreshInstallerContent = readFile(deploymentRefreshInstallerPath);
const deploymentRefreshHelperContent = readFile(deploymentRefreshHelperPath);
const remotePreflightScriptContent = readFile(remotePreflightScriptPath);
const productionPreflightScriptContent = readFile(productionPreflightScriptPath);
const productionPreflightRemoteScriptContent = readFile(productionPreflightRemoteScriptPath);
const dockerfileContent = readFile(dockerfilePath);
const timerScriptContent = readFile(timerScriptPath);
const automaticBackupTimerScriptContent = readFile(automaticBackupTimerScriptPath);
@@ -582,17 +589,33 @@ for (const requiredFragment of [
'check-existing-deployment.sh',
'deployment_contract_load',
'DEPLOY_DEFER_FINAL_SUCCESS=1',
'DEPLOY_STATUS_OUTPUT_MODE=quiet',
'ssh "$DEPLOY_REFRESH_SERVER" refresh',
'DEPLOYED_BUT_CONTRACT_REFRESH_FAILED',
'DEPLOY_DATA_MODE=existing',
'SYNC_ENV=0',
'路径与标识不回显',
'生产部署验证不接受未进入上游的提交',
]) {
if (!deployExistingScriptContent.includes(requiredFragment)) {
errors.push(`deploy-existing.sh 缺少既有部署合同: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'EXPECTED_PRODUCTION_ORIGIN',
'EXPECTED_PRODUCTION_DB_NAME',
'EXPECTED_PENDING_MIGRATIONS',
'production-preflight.mjs',
'CANDIDATE_COMMIT',
'PRODUCTION_PREFLIGHT_REPORT_DIR',
'未进入 deploy.sh',
]) {
if (!deployExistingScriptContent.includes(requiredFragment)) {
errors.push(`deploy-existing.sh 缺少生产迁移前置门禁: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'systemctl show',
'DEPLOY_CONTRACT_KEYS',
@@ -669,6 +692,68 @@ if (deployExistingScriptContent.indexOf('ssh "$DEPLOY_REFRESH_SERVER" refresh')
errors.push('deploy-existing.sh 必须在应用部署成功后刷新备份合同');
}
for (const requiredFragment of [
'validateProductionOrigin',
'validateProductionDbName',
'collectMigrationManifest',
'EXPECTED_PENDING_MIGRATIONS',
'PRISMA_STATUS_OUTPUT_MISSING',
'PRISMA_STATUS_OUTPUT_UNREADABLE',
'sanitizePrismaStatus',
'stableRemoteSummary',
'PRODUCTION_PREFLIGHT_REPORT_NOT_FRESH',
'prisma-migrate-status.txt',
'PRODUCTION_PREFLIGHT_REPORT_DIR',
]) {
if (!productionPreflightScriptContent.includes(requiredFragment)) {
errors.push(`production-preflight.mjs 缺少生产目标/候选迁移合同: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'EXPECTED_PRODUCTION_ORIGIN_SHA256',
'EXPECTED_PRODUCTION_DB_NAME_SHA256',
'PRISMA_STATUS_BEGIN',
'FROM "_prisma_migrations"',
'logged_failure',
'PROD_MIGRATION_STATUS_UNKNOWN_PENDING',
'PROD_DATABASE_URL_HOST_NON_PRODUCTION_IDENTITY',
'PROD_MIGRATION_PENDING_ALLOWLIST_MISMATCH',
'rollbackWindow=OPEN',
'PROD_TARGET_ORIGIN_MISMATCH',
]) {
if (!productionPreflightRemoteScriptContent.includes(requiredFragment)) {
errors.push(`production-preflight-remote.sh 缺少只读生产门禁合同: ${requiredFragment}`);
}
}
if (productionPreflightRemoteScriptContent.includes('migrate reset')
|| productionPreflightRemoteScriptContent.includes('db push')
|| productionPreflightRemoteScriptContent.includes('migrate resolve')) {
errors.push('production-preflight-remote.sh 不得包含 reset/db push/resolve 写入路径');
}
if (deployExistingScriptContent.indexOf('node "$ROOT_DIR/$PRODUCTION_PREFLIGHT_SCRIPT"')
> deployExistingScriptContent.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"')) {
errors.push('deploy-existing.sh 必须在 deploy.sh 前执行生产目标与迁移 preflight');
}
if (!deployScriptContent.includes('DEPLOY_STATUS_OUTPUT_MODE')) {
errors.push('deploy.sh 必须支持受控发布 wrapper 的 quiet migration status 输出模式');
}
if (deployScriptContent.indexOf("DEPLOY_STATUS_OUTPUT_MODE='${DEPLOY_STATUS_OUTPUT_MODE}'") === -1) {
errors.push('deploy.sh 必须将 DEPLOY_STATUS_OUTPUT_MODE 传入远端部署 shell');
}
if (deployExistingScriptContent.indexOf('EXPECTED_PENDING_MIGRATIONS=""')
< deployExistingScriptContent.indexOf('bash "$ROOT_DIR/deploy/trialrun/deploy.sh"')) {
errors.push('deploy-existing.sh 必须在 deploy.sh 后执行空 pending 的 post-status 复核');
}
if (productionPreflightRemoteScriptContent.indexOf('database_url_identity=')
> productionPreflightRemoteScriptContent.indexOf('prisma_status_output=')) {
errors.push('production-preflight-remote.sh 必须先确认 DATABASE_URL host/db 身份,再执行 migration status');
}
if (deployExistingScriptContent.indexOf('ssh "$DEPLOY_REFRESH_SERVER" refresh')
< deployExistingScriptContent.indexOf('EXPECTED_PENDING_MIGRATIONS=""')) {
errors.push('deploy-existing.sh 必须在 post-status 复核后刷新备份合同');
}
for (const requiredFragment of [
'REMOTE_DIR 是现存仓库根目录',
'SYNC_ENV=0 要求同步前已有普通远端 .env 文件',