接入发布候选回执强制校验(09-06-release-gate-integrity)

这个提交包含在:
2026-09-19 14:25:33 +08:00
父节点 1f8d5b658a
当前提交 16b9cad98c
共修改 15 个文件,包含 722 行新增和 91 行删除
+16 -1
查看文件
@@ -29,6 +29,19 @@ if [[ "${DEPLOY_ALLOW_UNPUBLISHED_COMMIT:-0}" == "1" || -n "${DEPLOY_UNPUBLISHED
fail "生产部署验证不接受未进入上游的提交;灾备 override 必须走独立获批流程"
fi
case "${PRODUCTION_IDENTITY_ONLY-0}" in
0) ;;
*) fail "identity-only 必须使用生产身份引导器,不能进入发布包装器" ;;
esac
case "${PRODUCTION_READ_ONLY_PREFLIGHT-0}" in
0)
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
;;
1) ;;
*) fail "PRODUCTION_READ_ONLY_PREFLIGHT 只能为 0 或 1" ;;
esac
declare -A contract=()
echo "[deploy-existing] 开始只读聚合体检"
DEPLOY_SERVER="$DEPLOY_SERVER" \
@@ -60,7 +73,7 @@ fi
if [[ -z "${EXPECTED_COMPOSE_PROJECT_NAME:-}" ]]; then
fail "生产部署必须由身份引导器提供 EXPECTED_COMPOSE_PROJECT_NAME"
fi
case "${PRODUCTION_READ_ONLY_PREFLIGHT:-0}" in
case "${PRODUCTION_READ_ONLY_PREFLIGHT-0}" in
0|1) ;;
*) fail "PRODUCTION_READ_ONLY_PREFLIGHT 只能为 0 或 1" ;;
esac
@@ -99,6 +112,8 @@ if [[ "${PRODUCTION_READ_ONLY_PREFLIGHT:-0}" == "1" ]]; then
exit 0
fi
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$source_commit" >/dev/null
DEPLOY_SERVER="$DEPLOY_SERVER" \
REMOTE_DIR="${contract[REMOTE_DIR]}" \
REMOTE_ENV_FILE="${contract[REMOTE_ENV_FILE]}" \
+6
查看文件
@@ -125,6 +125,9 @@ if [[ ! -f "${ROOT_DIR}/${PROVENANCE_SCRIPT}" ]]; then
exit 1
fi
RELEASE_GATE_EXPECTED_COMMIT="$(node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}")"
export RELEASE_GATE_EXPECTED_COMMIT
echo ">>> 0. 校验 Git 来源与发布提交"
PROVENANCE_JSON="$(node "${ROOT_DIR}/${PROVENANCE_SCRIPT}")"
SOURCE_BUILD_ENV_LINES="$(node -e '
@@ -155,6 +158,7 @@ SOURCE_PUBLISHED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout
SOURCE_REMOTE_VERIFIED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.remoteVerified))' "$PROVENANCE_JSON")"
SOURCE_OVERRIDE_USED="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.overrideUsed))' "$PROVENANCE_JSON")"
SOURCE_OVERRIDE_REASON="$(node -e 'const p=JSON.parse(process.argv[1]); process.stdout.write(String(p.overrideReason || ""))' "$PROVENANCE_JSON")"
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$SOURCE_COMMIT" >/dev/null
echo "[deploy-source] commit=${SOURCE_COMMIT} branch=${SOURCE_BRANCH} published=${SOURCE_PUBLISHED} remoteVerified=${SOURCE_REMOTE_VERIFIED}"
if [[ "$SOURCE_OVERRIDE_USED" == "true" || "$SOURCE_REMOTE_VERIFIED" != "true" ]]; then
echo "[deploy-source][UNPUBLISHED-OVERRIDE] reason=${SOURCE_OVERRIDE_REASON} remoteVerified=${SOURCE_REMOTE_VERIFIED}" >&2
@@ -202,6 +206,8 @@ ssh "${DEPLOY_SERVER}" \
"REMOTE_DIR='${REMOTE_DIR}' REMOTE_ENV_FILE='${REMOTE_ENV_FILE}' COMPOSE_FILE='${COMPOSE_FILE}' POSTGRES_DATA_DIR='${POSTGRES_DATA_DIR}' STORAGE_HOST_DIR='${STORAGE_HOST_DIR}' DEPLOY_DATA_MODE='${DEPLOY_DATA_MODE}' SYNC_ENV='${SYNC_ENV}' bash -s" \
< "${ROOT_DIR}/${REMOTE_PREFLIGHT_SCRIPT}"
node "$ROOT_DIR/scripts/qa/release-gate-result.mjs" require "${RELEASE_GATE_RECEIPT_DIR:-}" "$SOURCE_COMMIT" >/dev/null
echo ">>> 3.1 创建远端目录"
ssh "${DEPLOY_SERVER}" "mkdir -p '${REMOTE_DIR}'"
@@ -7,6 +7,7 @@ import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
import { fileURLToPath, pathToFileURL } from "node:url";
import { deploymentReadOnlyMode, requireReleaseReceipt, ReleaseGateError } from "../../scripts/qa/release-gate-result.mjs";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(SCRIPT_DIR, "../..");
@@ -198,6 +199,9 @@ function main() {
try {
const identityOnly = process.env.PRODUCTION_IDENTITY_ONLY ?? "0";
if (!["0", "1"].includes(identityOnly)) fail("PRODUCTION_IDENTITY_ONLY_INVALID");
const { preflightOnly } = deploymentReadOnlyMode(process.env, true);
// Reject before SSH configuration discovery, target probes or database reads.
const release = identityOnly === "1" || preflightOnly ? null : requireReleaseReceipt();
const historicalTargetScript = requiredCommandOutput(
"git",
["show", `${HISTORICAL_TARGET_COMMIT}:${DEPLOY_SCRIPT_AT_COMMIT}`],
@@ -228,11 +232,13 @@ function main() {
const databaseIdentityHash = bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin);
console.log("[production-identity-bootstrap][PASS] exact_identity_group");
if (release) requireReleaseReceipt(release.directory, release.commit);
const result = spawnSync("bash", [DEPLOY_EXISTING_SCRIPT], {
cwd: ROOT_DIR,
stdio: "inherit",
env: {
...process.env,
...(release ? { RELEASE_GATE_RECEIPT_DIR: release.directory, RELEASE_GATE_EXPECTED_COMMIT: release.commit } : {}),
DEPLOY_SERVER: identity.deployServer,
DEPLOY_REFRESH_SERVER: refreshTarget,
EXPECTED_DEPLOY_SERVER: identity.deployServer,
@@ -246,7 +252,7 @@ function main() {
});
if (result.status !== 0) fail("DEPLOY_EXISTING_FAILED");
} catch (error) {
const code = error instanceof ProductionIdentityBootstrapError
const code = error instanceof ProductionIdentityBootstrapError || error instanceof ReleaseGateError
? error.code
: "PRODUCTION_IDENTITY_BOOTSTRAP_UNEXPECTED_ERROR";
console.error(`[production-identity-bootstrap][PAUSED] code=${code}`);