接入发布候选回执强制校验(09-06-release-gate-integrity)
这个提交包含在:
@@ -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]}" \
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
在新工单中引用
屏蔽一个用户