完善(08-09-deployment-contract-single-source-harness): 收敛部署合同并补失败路径

这个提交包含在:
2026-08-09 14:40:28 +08:00
父节点 099f58cf3b
当前提交 621934a35e
共修改 13 个文件,包含 648 行新增和 27 行删除
+15 -2
查看文件
@@ -18,6 +18,17 @@ const ALLOWED_UNPUBLISHED_REASONS = new Set([
"controlled-local-recovery",
]);
export function deriveCandidateBuildEnv(commit) {
const normalized = String(commit ?? "").trim();
if (!/^[0-9a-f]{40,64}$/u.test(normalized)) {
throw new SourceProvenanceError("commit_invalid");
}
return Object.freeze({
BUILD_SHA: normalized,
VITE_BUILD_SHA: normalized.slice(0, 12),
});
}
function runGit(rootDir, args) {
return spawnSync("git", args, {
cwd: rootDir,
@@ -50,6 +61,7 @@ export function collectSourceProvenance({ rootDir, allowUnpublished = false }) {
}
const commit = gitText(rootDir, ["rev-parse", "--verify", "HEAD"], "commit_unavailable");
const buildEnv = deriveCandidateBuildEnv(commit);
const branch = gitText(rootDir, ["branch", "--show-current"], "branch_unavailable") || "detached";
const upstreamResult = runGit(rootDir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
const upstream = upstreamResult.status === 0 ? upstreamResult.stdout.trim() : null;
@@ -64,8 +76,9 @@ export function collectSourceProvenance({ rootDir, allowUnpublished = false }) {
return {
status: "PASS",
commit,
shortCommit: commit.slice(0, 12),
commit: buildEnv.BUILD_SHA,
shortCommit: buildEnv.VITE_BUILD_SHA,
buildEnv,
branch,
upstream,
published,
+25 -4
查看文件
@@ -127,8 +127,29 @@ fi
echo ">>> 0. 校验 Git 来源与发布提交"
PROVENANCE_JSON="$(node "${ROOT_DIR}/${PROVENANCE_SCRIPT}")"
SOURCE_COMMIT="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(p.commit)' "$PROVENANCE_JSON")"
SOURCE_SHORT_COMMIT="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(p.shortCommit)' "$PROVENANCE_JSON")"
SOURCE_BUILD_ENV_LINES="$(node -e '
const p = JSON.parse(process.argv[1]);
for (const [key, value] of Object.entries(p.buildEnv ?? {})) {
process.stdout.write(`${key}=${value}\n`);
}
' "$PROVENANCE_JSON")"
declare -A SOURCE_BUILD_ENV=()
while IFS='=' read -r key value; do
[[ -n "$key" ]] || continue
case "$key" in
BUILD_SHA|VITE_BUILD_SHA) ;;
*) echo "[ERROR] 发布来源包含未知候选构建环境键" >&2; exit 1 ;;
esac
[[ -z "${SOURCE_BUILD_ENV[$key]+x}" ]] \
|| { echo "[ERROR] 发布来源包含重复候选构建环境键" >&2; exit 1; }
SOURCE_BUILD_ENV["$key"]="$value"
done <<< "$SOURCE_BUILD_ENV_LINES"
[[ "${#SOURCE_BUILD_ENV[@]}" -eq 2 \
&& -n "${SOURCE_BUILD_ENV[BUILD_SHA]:-}" \
&& -n "${SOURCE_BUILD_ENV[VITE_BUILD_SHA]:-}" ]] \
|| { echo "[ERROR] 发布来源缺少完整候选构建环境对象" >&2; exit 1; }
SOURCE_COMMIT="${SOURCE_BUILD_ENV[BUILD_SHA]}"
SOURCE_SHORT_COMMIT="${SOURCE_BUILD_ENV[VITE_BUILD_SHA]}"
SOURCE_BRANCH="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(p.branch)' "$PROVENANCE_JSON")"
SOURCE_PUBLISHED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.published))' "$PROVENANCE_JSON")"
SOURCE_REMOTE_VERIFIED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.remoteVerified))' "$PROVENANCE_JSON")"
@@ -274,7 +295,7 @@ run_logged_deploy_stage() {
echo "[deploy-stage][ERROR] 阶段名称格式无效" >&2
return 2
}
local log_ref="$DEPLOY_RUNTIME_LOG_REF/${stage}-${BUILD_SHA:0:12}.log"
local log_ref="$DEPLOY_RUNTIME_LOG_REF/${stage}-${VITE_BUILD_SHA}.log"
local log_file="$REMOTE_DIR/$log_ref"
: > "$log_file"
chmod 0600 "$log_file"
@@ -334,7 +355,7 @@ ready_ok=0
for i in $(seq 1 60); do
if ready_payload="$(curl -fsS "http://127.0.0.1:${server_host_port}/ready" 2>/dev/null)"; then
if printf '%s' "$ready_payload" | "${compose_cmd[@]}" exec -T \
-e EXPECTED_BUILD_SHA="${BUILD_SHA:0:12}" server node -e '
-e EXPECTED_BUILD_SHA="${VITE_BUILD_SHA}" server node -e '
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync(0, "utf8") || "{}");
if (payload?.data?.buildSha !== process.env.EXPECTED_BUILD_SHA) process.exit(1);
+16 -12
查看文件
@@ -7,6 +7,7 @@ import { isIP } from "node:net";
import path from "node:path";
import process from "node:process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { deriveCandidateBuildEnv } from "./check-source-provenance.mjs";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(SCRIPT_DIR, "../..");
@@ -275,8 +276,12 @@ function main() {
if (process.env.CANDIDATE_COMMIT == null) fail("CANDIDATE_COMMIT_MISSING");
const candidateCommit = String(process.env.CANDIDATE_COMMIT).trim();
if (!/^[0-9a-f]{40,64}$/iu.test(candidateCommit)) fail("CANDIDATE_COMMIT_INVALID");
const candidateShortCommit = candidateCommit.slice(0, 12);
let candidateBuildEnv;
try {
candidateBuildEnv = deriveCandidateBuildEnv(candidateCommit);
} catch {
fail("CANDIDATE_COMMIT_INVALID");
}
const candidateManifest = collectMigrationManifest(ROOT_DIR, candidateCommit);
const candidateSet = new Set(candidateManifest);
if (expectedPending.some((migration) => !candidateSet.has(migration))) {
@@ -305,8 +310,9 @@ function main() {
`STORAGE_HOST_DIR=${shellQuote(storageHostDir)}`,
`EXPECTED_PRODUCTION_ORIGIN_SHA256=${shellQuote(sha256Text(expectedOrigin))}`,
`EXPECTED_PRODUCTION_DB_NAME_SHA256=${shellQuote(expectedDbNameHash)}`,
`BUILD_SHA=${shellQuote(candidateCommit)}`,
`VITE_BUILD_SHA=${shellQuote(candidateShortCommit)}`,
...Object.entries(candidateBuildEnv).map(
([key, value]) => `${key}=${shellQuote(value)}`,
),
`CANDIDATE_MIGRATIONS_CSV=${shellQuote(candidateManifest.join(","))}`,
`EXPECTED_PENDING_MIGRATIONS_CSV=${shellQuote(expectedPending.join(","))}`,
"bash -s",
@@ -321,14 +327,6 @@ function main() {
});
const stdout = result.stdout || "";
const rawStatus = extractPrismaStatus(stdout);
if (rawStatus != null) {
validatePrismaStatusText(rawStatus);
const sanitizedStatus = sanitizePrismaStatus(rawStatus, [expectedOrigin]);
writeFileSync(path.join(reportDir, "prisma-migrate-status.txt"), `${sanitizedStatus}\n`, {
encoding: "utf8",
mode: 0o600,
});
}
const stableSummary = stableRemoteSummary(stdout);
writeFileSync(path.join(reportDir, "preflight-output.txt"), `${stableSummary}\n`, {
encoding: "utf8",
@@ -343,6 +341,12 @@ function main() {
stderr: result.stderr,
rawStatus,
});
validatePrismaStatusText(rawStatus);
const sanitizedStatus = sanitizePrismaStatus(rawStatus, [expectedOrigin]);
writeFileSync(path.join(reportDir, "prisma-migrate-status.txt"), `${sanitizedStatus}\n`, {
encoding: "utf8",
mode: 0o600,
});
console.log("[production-preflight][PASS] 生产目标与迁移只读门禁通过");
} catch (error) {
const code = error instanceof ProductionPreflightError
+44 -2
查看文件
@@ -287,6 +287,9 @@ const deploymentRefreshHelperContent = readFile(deploymentRefreshHelperPath);
const remotePreflightScriptContent = readFile(remotePreflightScriptPath);
const productionPreflightScriptContent = readFile(productionPreflightScriptPath);
const productionPreflightRemoteScriptContent = readFile(productionPreflightRemoteScriptPath);
const sourceProvenanceScriptContent = readFile(
path.join(rootDir, 'deploy/trialrun/check-source-provenance.mjs'),
);
const productionIdentityBootstrapScriptContent = readFile(productionIdentityBootstrapScriptPath);
const productionIdentityBootstrapRemoteScriptContent = readFile(productionIdentityBootstrapRemoteScriptPath);
const dockerfileContent = readFile(dockerfilePath);
@@ -453,6 +456,34 @@ for (const requiredFragment of [
errors.push(`install-warch3-ops-timer.sh 缺少活动部署环境绑定: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'deriveCandidateBuildEnv',
'BUILD_SHA: normalized',
'VITE_BUILD_SHA: normalized.slice(0, 12)',
'buildEnv,',
]) {
if (!sourceProvenanceScriptContent.includes(requiredFragment)) {
errors.push(`check-source-provenance.mjs 缺少候选构建身份单一合同: ${requiredFragment}`);
}
}
for (const requiredFragment of [
'Object.entries(p.buildEnv ?? {})',
'declare -A SOURCE_BUILD_ENV=()',
'"${#SOURCE_BUILD_ENV[@]}" -eq 2',
'SOURCE_COMMIT="${SOURCE_BUILD_ENV[BUILD_SHA]}"',
'SOURCE_SHORT_COMMIT="${SOURCE_BUILD_ENV[VITE_BUILD_SHA]}"',
]) {
if (!deployScriptContent.includes(requiredFragment)) {
errors.push(`deploy.sh 未完整消费 provenance buildEnv: ${requiredFragment}`);
}
}
if (deployScriptContent.includes('p.shortCommit')
|| deployScriptContent.includes('p.buildEnv.BUILD_SHA')
|| deployScriptContent.includes('p.buildEnv.VITE_BUILD_SHA')
|| deployScriptContent.includes('${BUILD_SHA:0:12}')) {
errors.push('deploy.sh 不得绕过完整对象遍历单独读取候选构建字段');
}
if (productionIdentityBootstrapScriptContent.indexOf('if (identityOnly === "1")')
> productionIdentityBootstrapScriptContent.indexOf('const databaseIdentityHash = bootstrapDatabaseIdentityHash')) {
errors.push('production-identity-bootstrap.mjs 的 identity-only 停止点必须位于数据库身份读取前');
@@ -765,8 +796,9 @@ for (const requiredFragment of [
'collectMigrationManifest',
'EXPECTED_PENDING_MIGRATIONS',
'CANDIDATE_COMMIT_MISSING',
'BUILD_SHA=',
'VITE_BUILD_SHA=',
'deriveCandidateBuildEnv',
'Object.entries(candidateBuildEnv)',
'([key, value]) => `${key}=${shellQuote(value)}`',
'PRISMA_STATUS_OUTPUT_MISSING',
'PRISMA_STATUS_OUTPUT_UNREADABLE',
'sanitizePrismaStatus',
@@ -779,6 +811,16 @@ for (const requiredFragment of [
errors.push(`production-preflight.mjs 缺少生产目标/候选迁移合同: ${requiredFragment}`);
}
}
if (productionPreflightScriptContent.includes('candidateCommit.slice(0, 12)')) {
errors.push('production-preflight.mjs 不得自行派生候选短 SHA');
}
if (productionPreflightScriptContent.includes('CANDIDATE_BUILD_ENV_KEYS')) {
errors.push('production-preflight.mjs 不得以独立键数组重新定义候选构建环境对象');
}
if (productionPreflightScriptContent.lastIndexOf('assertRemotePreflightResult({')
> productionPreflightScriptContent.lastIndexOf('validatePrismaStatusText(rawStatus);')) {
errors.push('production-preflight.mjs 必须先传播远端失败,再校验 Prisma marker 内容');
}
for (const requiredFragment of [
'EXPECTED_PRODUCTION_ORIGIN_SHA256',