修正(08-08-production-deployment-execution): 先验生产公网身份再冻结目录
这个提交包含在:
@@ -9,11 +9,16 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
for key in REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE COMPOSE_PROJECT_NAME \
|
||||
EXPECTED_PRODUCTION_ORIGIN_SHA256; do
|
||||
for key in IDENTITY_BOOTSTRAP_MODE REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE \
|
||||
COMPOSE_PROJECT_NAME EXPECTED_PRODUCTION_ORIGIN_SHA256; do
|
||||
[[ -n "${!key:-}" ]] || fail "${key}_MISSING"
|
||||
done
|
||||
|
||||
case "$IDENTITY_BOOTSTRAP_MODE" in
|
||||
origin|database) ;;
|
||||
*) fail "IDENTITY_BOOTSTRAP_MODE_INVALID" ;;
|
||||
esac
|
||||
|
||||
[[ "$EXPECTED_PRODUCTION_ORIGIN_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|
||||
|| fail "PROD_TARGET_ORIGIN_HASH_INVALID"
|
||||
cd "$REMOTE_DIR"
|
||||
@@ -62,11 +67,8 @@ reject_non_production_identity() {
|
||||
}
|
||||
|
||||
actual_origin="$(dotenv_value APP_ORIGIN)" || fail "PROD_TARGET_ORIGIN_MISSING"
|
||||
actual_db_name="$(dotenv_value POSTGRES_DB)" || fail "PROD_TARGET_DB_NAME_MISSING"
|
||||
[[ "$(hash_text "$actual_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|
||||
|| fail "PROD_TARGET_ORIGIN_MISMATCH"
|
||||
reject_non_production_identity "$actual_db_name" \
|
||||
|| fail "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY"
|
||||
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
compose_cmd=(docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE")
|
||||
@@ -90,6 +92,14 @@ runtime_origin="$(
|
||||
[[ "$(hash_text "$runtime_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|
||||
|| fail "PROD_TARGET_RUNTIME_ORIGIN_MISMATCH"
|
||||
|
||||
if [[ "$IDENTITY_BOOTSTRAP_MODE" == "origin" ]]; then
|
||||
printf '[production-identity-bootstrap][PASS] origin_identity\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Only the comparison baseline crosses the SSH channel. The database name
|
||||
# itself remains remote and cannot be reconstructed from task evidence.
|
||||
actual_db_name="$(dotenv_value POSTGRES_DB)" || fail "PROD_TARGET_DB_NAME_MISSING"
|
||||
reject_non_production_identity "$actual_db_name" \
|
||||
|| fail "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY"
|
||||
printf '[production-identity-bootstrap][DB_SHA256] %s\n' "$(hash_text "$actual_db_name")"
|
||||
|
||||
@@ -60,14 +60,10 @@ function parseHistoricalDefault(source, key) {
|
||||
|
||||
export function parseHistoricalDeploymentIdentity(targetSource, projectSource) {
|
||||
const deployServer = parseHistoricalDefault(targetSource, "DEPLOY_SERVER");
|
||||
const remoteDir = parseHistoricalDefault(targetSource, "REMOTE_DIR");
|
||||
const projectRemoteDir = parseHistoricalDefault(projectSource, "REMOTE_DIR");
|
||||
const composeProjectName = parseHistoricalDefault(projectSource, "COMPOSE_PROJECT_NAME");
|
||||
if (projectRemoteDir !== remoteDir) fail("HISTORICAL_REMOTE_DIR_DIVERGED");
|
||||
if (!SAFE_SSH_TARGET_PATTERN.test(deployServer)) fail("HISTORICAL_DEPLOY_SERVER_INVALID");
|
||||
if (!SAFE_ABSOLUTE_PATH_PATTERN.test(remoteDir)) fail("HISTORICAL_REMOTE_DIR_INVALID");
|
||||
if (!SAFE_PROJECT_PATTERN.test(composeProjectName)) fail("HISTORICAL_COMPOSE_PROJECT_INVALID");
|
||||
return { deployServer, remoteDir, composeProjectName };
|
||||
return { deployServer, composeProjectName };
|
||||
}
|
||||
|
||||
export function parseExpectedProductionOrigin(runbook) {
|
||||
@@ -145,9 +141,8 @@ export function parseDeploymentContract(raw) {
|
||||
return contract;
|
||||
}
|
||||
|
||||
export function assertExactHistoricalContract(identity, contract) {
|
||||
if (contract.REMOTE_DIR !== identity.remoteDir) fail("SYSTEMD_REMOTE_DIR_MISMATCH");
|
||||
if (contract.REMOTE_ENV_FILE !== `${identity.remoteDir}/deploy/trialrun/.env`) {
|
||||
export function assertContractForOriginVerification(identity, contract) {
|
||||
if (contract.REMOTE_ENV_FILE !== `${contract.REMOTE_DIR}/deploy/trialrun/.env`) {
|
||||
fail("SYSTEMD_REMOTE_ENV_FILE_MISMATCH");
|
||||
}
|
||||
if (contract.COMPOSE_FILE !== "deploy/trialrun/docker-compose.yml") {
|
||||
@@ -166,8 +161,9 @@ function shellQuote(value) {
|
||||
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
|
||||
function runRemoteIdentityBootstrap(mode, identity, contract, expectedOrigin) {
|
||||
const remoteCommand = [
|
||||
`IDENTITY_BOOTSTRAP_MODE=${shellQuote(mode)}`,
|
||||
`REMOTE_DIR=${shellQuote(contract.REMOTE_DIR)}`,
|
||||
`REMOTE_ENV_FILE=${shellQuote(contract.REMOTE_ENV_FILE)}`,
|
||||
`COMPOSE_FILE=${shellQuote(contract.COMPOSE_FILE)}`,
|
||||
@@ -181,7 +177,18 @@ function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
|
||||
const code = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/code=([A-Z0-9_]+)/u)?.[1];
|
||||
fail(code || "REMOTE_IDENTITY_BOOTSTRAP_FAILED");
|
||||
}
|
||||
const output = String(result.stdout ?? "").trim();
|
||||
return String(result.stdout ?? "").trim();
|
||||
}
|
||||
|
||||
function verifyOriginIdentity(identity, contract, expectedOrigin) {
|
||||
const output = runRemoteIdentityBootstrap("origin", identity, contract, expectedOrigin);
|
||||
if (output !== "[production-identity-bootstrap][PASS] origin_identity") {
|
||||
fail("REMOTE_ORIGIN_IDENTITY_PAYLOAD_INVALID");
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
|
||||
const output = runRemoteIdentityBootstrap("database", identity, contract, expectedOrigin);
|
||||
const match = output.match(/^\[production-identity-bootstrap\]\[DB_SHA256\] ([0-9a-f]{64})$/u);
|
||||
if (!match) fail("REMOTE_DB_IDENTITY_PAYLOAD_INVALID");
|
||||
return match[1];
|
||||
@@ -189,6 +196,8 @@ function bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin) {
|
||||
|
||||
function main() {
|
||||
try {
|
||||
const identityOnly = process.env.PRODUCTION_IDENTITY_ONLY ?? "0";
|
||||
if (!["0", "1"].includes(identityOnly)) fail("PRODUCTION_IDENTITY_ONLY_INVALID");
|
||||
const historicalTargetScript = requiredCommandOutput(
|
||||
"git",
|
||||
["show", `${HISTORICAL_TARGET_COMMIT}:${DEPLOY_SCRIPT_AT_COMMIT}`],
|
||||
@@ -208,7 +217,14 @@ function main() {
|
||||
"SYSTEMD_CONTRACT_UNAVAILABLE",
|
||||
);
|
||||
const contract = parseDeploymentContract(contractRaw);
|
||||
assertExactHistoricalContract(identity, contract);
|
||||
assertContractForOriginVerification(identity, contract);
|
||||
verifyOriginIdentity(identity, contract, expectedOrigin);
|
||||
const expectedRemoteDir = contract.REMOTE_DIR;
|
||||
|
||||
if (identityOnly === "1") {
|
||||
console.log("[production-identity-bootstrap][PASS] origin_verified_remote_dir_baseline=systemd_contract");
|
||||
return;
|
||||
}
|
||||
const databaseIdentityHash = bootstrapDatabaseIdentityHash(identity, contract, expectedOrigin);
|
||||
|
||||
console.log("[production-identity-bootstrap][PASS] exact_identity_group");
|
||||
@@ -220,7 +236,7 @@ function main() {
|
||||
DEPLOY_SERVER: identity.deployServer,
|
||||
DEPLOY_REFRESH_SERVER: refreshTarget,
|
||||
EXPECTED_DEPLOY_SERVER: identity.deployServer,
|
||||
EXPECTED_REMOTE_DIR: identity.remoteDir,
|
||||
EXPECTED_REMOTE_DIR: expectedRemoteDir,
|
||||
EXPECTED_COMPOSE_PROJECT_NAME: identity.composeProjectName,
|
||||
EXPECTED_PRODUCTION_ORIGIN: expectedOrigin,
|
||||
EXPECTED_PRODUCTION_DB_NAME_SHA256: databaseIdentityHash,
|
||||
|
||||
@@ -453,6 +453,14 @@ for (const requiredFragment of [
|
||||
errors.push(`install-warch3-ops-timer.sh 缺少活动部署环境绑定: ${requiredFragment}`);
|
||||
}
|
||||
}
|
||||
if (productionIdentityBootstrapScriptContent.indexOf('if (identityOnly === "1")')
|
||||
> productionIdentityBootstrapScriptContent.indexOf('const databaseIdentityHash = bootstrapDatabaseIdentityHash')) {
|
||||
errors.push('production-identity-bootstrap.mjs 的 identity-only 停止点必须位于数据库身份读取前');
|
||||
}
|
||||
if (productionIdentityBootstrapScriptContent.indexOf('verifyOriginIdentity(identity, contract, expectedOrigin);')
|
||||
> productionIdentityBootstrapScriptContent.indexOf('const expectedRemoteDir = contract.REMOTE_DIR')) {
|
||||
errors.push('production-identity-bootstrap.mjs 必须在 origin 双取样后冻结 contract REMOTE_DIR');
|
||||
}
|
||||
|
||||
for (const requiredFragment of [
|
||||
'MODE="${1:-install}"',
|
||||
@@ -634,8 +642,10 @@ for (const requiredFragment of [
|
||||
'HISTORICAL_TARGET_COMMIT',
|
||||
'HISTORICAL_PROJECT_COMMIT',
|
||||
'discoverRefreshTarget',
|
||||
'assertExactHistoricalContract',
|
||||
'assertContractForOriginVerification',
|
||||
'verifyOriginIdentity',
|
||||
'bootstrapDatabaseIdentityHash',
|
||||
'PRODUCTION_IDENTITY_ONLY',
|
||||
'EXPECTED_PRODUCTION_DB_NAME_SHA256',
|
||||
'DEPLOY_EXISTING_SCRIPT',
|
||||
]) {
|
||||
@@ -647,6 +657,8 @@ for (const requiredFragment of [
|
||||
'EXPECTED_PRODUCTION_ORIGIN_SHA256',
|
||||
'PROD_TARGET_RUNTIME_ORIGIN_MISMATCH',
|
||||
'PROD_TARGET_DB_NON_PRODUCTION_IDENTITY',
|
||||
'origin_identity',
|
||||
'IDENTITY_BOOTSTRAP_MODE',
|
||||
'DB_SHA256',
|
||||
]) {
|
||||
if (!productionIdentityBootstrapRemoteScriptContent.includes(requiredFragment)) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户