132 行
6.5 KiB
Bash
132 行
6.5 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
|
QUERY_SCRIPT="$ROOT_DIR/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/research/production-reimbursement-readiness-pre.cjs"
|
|
IDENTITY_MODULE="$ROOT_DIR/deploy/trialrun/production-identity-bootstrap.mjs"
|
|
IDENTITY_REMOTE="$ROOT_DIR/deploy/trialrun/production-identity-bootstrap-remote.sh"
|
|
PREFLIGHT_SCRIPT="$ROOT_DIR/deploy/trialrun/production-preflight.mjs"
|
|
CONTRACT_LIB="$ROOT_DIR/deploy/trialrun/deployment-contract-lib.sh"
|
|
CONTRACT_UNIT="hyb-oa-automatic-backup-daily.service"
|
|
CONFIRMATION="READ_ONLY_REIMBURSEMENT_READINESS_PRE"
|
|
|
|
# shellcheck source=../../../../../deploy/trialrun/deployment-contract-lib.sh
|
|
source "$CONTRACT_LIB"
|
|
|
|
fail() {
|
|
printf '[reimbursement-readiness-pre-runner][FAILED] code=%s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
shell_quote() {
|
|
printf "'%s'" "${1//\'/\'\\\'\'}"
|
|
}
|
|
|
|
[[ "$#" -eq 1 && "${1:-}" == "$CONFIRMATION" ]] || fail "EXPLICIT_CONFIRMATION_REQUIRED"
|
|
for file in "$QUERY_SCRIPT" "$IDENTITY_MODULE" "$IDENTITY_REMOTE" "$PREFLIGHT_SCRIPT"; do
|
|
[[ -f "$file" && ! -L "$file" ]] || fail "LOCAL_CONTRACT_MISSING"
|
|
done
|
|
|
|
cd "$ROOT_DIR"
|
|
identity_fields="$({
|
|
node --input-type=module - "$IDENTITY_MODULE" <<'NODE'
|
|
import fs from "node:fs";
|
|
import { execFileSync } from "node:child_process";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
const modulePath = process.argv[2];
|
|
const mod = await import(pathToFileURL(modulePath));
|
|
const targetSource = execFileSync("git", ["show", "34181e3:deploy/trialrun/deploy.sh"], { encoding: "utf8" });
|
|
const projectSource = execFileSync("git", ["show", "5e2d038938265c926e06d3cc6d0841f6dcc06148:deploy/trialrun/deploy.sh"], { encoding: "utf8" });
|
|
const identity = mod.parseHistoricalDeploymentIdentity(targetSource, projectSource);
|
|
const runbook = fs.readFileSync("docs/trialrun/OPS7运维交接Runbook.md", "utf8");
|
|
const origin = mod.parseExpectedProductionOrigin(runbook);
|
|
process.stdout.write(`${identity.deployServer}\t${identity.composeProjectName}\t${origin}`);
|
|
NODE
|
|
} 2>/dev/null)" || fail "LOCAL_IDENTITY_PARSE_FAILED"
|
|
IFS=$'\t' read -r deploy_server expected_project expected_origin <<<"$identity_fields"
|
|
deployment_contract_safe_ssh_target "$deploy_server" || fail "DEPLOY_SERVER_INVALID"
|
|
deployment_contract_safe_project "$expected_project" || fail "COMPOSE_PROJECT_INVALID"
|
|
[[ "$expected_origin" == https://* ]] || fail "PRODUCTION_ORIGIN_INVALID"
|
|
|
|
PRODUCTION_IDENTITY_ONLY=1 node "$IDENTITY_MODULE" || fail "PRODUCTION_IDENTITY_FAILED"
|
|
|
|
declare -A contract=()
|
|
deployment_contract_load "$deploy_server" "$CONTRACT_UNIT" contract || fail "SYSTEMD_CONTRACT_UNAVAILABLE"
|
|
[[ "${contract[COMPOSE_PROJECT_NAME]}" == "$expected_project" ]] || fail "COMPOSE_PROJECT_MISMATCH"
|
|
|
|
origin_hash="$(printf '%s' "$expected_origin" | sha256sum | awk '{print $1}')"
|
|
database_output="$(
|
|
ssh "$deploy_server" \
|
|
"IDENTITY_BOOTSTRAP_MODE='database' REMOTE_DIR=$(shell_quote "${contract[REMOTE_DIR]}") REMOTE_ENV_FILE=$(shell_quote "${contract[REMOTE_ENV_FILE]}") COMPOSE_FILE=$(shell_quote "${contract[COMPOSE_FILE]}") COMPOSE_PROJECT_NAME=$(shell_quote "${contract[COMPOSE_PROJECT_NAME]}") EXPECTED_PRODUCTION_ORIGIN_SHA256='$origin_hash' bash -s" \
|
|
< "$IDENTITY_REMOTE"
|
|
)" || fail "DATABASE_IDENTITY_FAILED"
|
|
database_hash="$(printf '%s' "$database_output" | sed -n 's/^\[production-identity-bootstrap\]\[DB_SHA256\] \([0-9a-f]\{64\}\)$/\1/p')"
|
|
[[ "$database_hash" =~ ^[0-9a-f]{64}$ ]] || fail "DATABASE_IDENTITY_PAYLOAD_INVALID"
|
|
|
|
candidate_commit="$(git rev-parse origin/main)"
|
|
[[ "$candidate_commit" =~ ^[0-9a-f]{40,64}$ ]] || fail "DEPLOYED_COMMIT_INVALID"
|
|
[[ -z "$(git diff --name-only "$candidate_commit"..HEAD -- client/src server/src deploy/trialrun/docker-compose.yml)" ]] \
|
|
|| fail "PRODUCT_DELTA_AFTER_DEPLOYED_COMMIT"
|
|
|
|
runtime_check_command="
|
|
set -euo pipefail
|
|
server_ids=\"\$(docker ps --filter label=com.docker.compose.project=$(shell_quote "${contract[COMPOSE_PROJECT_NAME]}") --filter label=com.docker.compose.service=server --format '{{.ID}}')\"
|
|
[[ \"\$(printf '%s\\n' \"\$server_ids\" | sed '/^\$/d' | wc -l)\" -eq 1 ]]
|
|
runtime_sha=\"\$(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' \"\$server_ids\" | sed -n 's/^BUILD_SHA=//p' | tail -n 1)\"
|
|
[[ \"\$runtime_sha\" == $(shell_quote "$candidate_commit") ]]
|
|
printf '[reimbursement-readiness-pre-runner][PASS] runtime_build_exact\\n'
|
|
"
|
|
ssh "$deploy_server" "bash -c $(shell_quote "$runtime_check_command")" || fail "RUNTIME_BUILD_MISMATCH"
|
|
|
|
report_dir="$(mktemp -d)"
|
|
trap 'rm -rf -- "$report_dir"' EXIT
|
|
EXPECTED_PRODUCTION_ORIGIN="$expected_origin" \
|
|
EXPECTED_PRODUCTION_DB_NAME_SHA256="$database_hash" \
|
|
EXPECTED_PENDING_MIGRATIONS="" \
|
|
EXPECTED_DEPLOY_SERVER="$deploy_server" \
|
|
EXPECTED_REMOTE_DIR="${contract[REMOTE_DIR]}" \
|
|
EXPECTED_COMPOSE_PROJECT_NAME="${contract[COMPOSE_PROJECT_NAME]}" \
|
|
DEPLOY_SERVER="$deploy_server" \
|
|
REMOTE_DIR="${contract[REMOTE_DIR]}" \
|
|
REMOTE_ENV_FILE="${contract[REMOTE_ENV_FILE]}" \
|
|
COMPOSE_FILE="${contract[COMPOSE_FILE]}" \
|
|
COMPOSE_PROJECT_NAME="${contract[COMPOSE_PROJECT_NAME]}" \
|
|
POSTGRES_DATA_DIR="${contract[POSTGRES_DATA_DIR]}" \
|
|
STORAGE_HOST_DIR="${contract[STORAGE_HOST_DIR]}" \
|
|
CANDIDATE_COMMIT="$candidate_commit" \
|
|
PRODUCTION_PREFLIGHT_REPORT_DIR="$report_dir/preflight" \
|
|
node "$PREFLIGHT_SCRIPT" || fail "PRODUCTION_PREFLIGHT_FAILED"
|
|
|
|
remote_query_command="
|
|
set -euo pipefail
|
|
server_ids=\"\$(docker ps --filter label=com.docker.compose.project=$(shell_quote "${contract[COMPOSE_PROJECT_NAME]}") --filter label=com.docker.compose.service=server --format '{{.ID}}')\"
|
|
[[ \"\$(printf '%s\\n' \"\$server_ids\" | sed '/^\$/d' | wc -l)\" -eq 1 ]]
|
|
docker exec -i -w /app/server \"\$server_ids\" node -
|
|
"
|
|
query_output="$(ssh "$deploy_server" "bash -c $(shell_quote "$remote_query_command")" < "$QUERY_SCRIPT")" \
|
|
|| fail "QUERY_EXECUTION_FAILED"
|
|
|
|
validated_output="$(printf '%s' "$query_output" | node -e '
|
|
const fs = require("node:fs");
|
|
const expected = process.argv[1];
|
|
const value = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
if (value.status !== "PASS" || value.transactionReadOnly !== true) process.exit(1);
|
|
if (value.sourceBuildSha !== expected.slice(0, 12)) process.exit(1);
|
|
if (!value.employees || !value.configuration || !value.lifecycle) process.exit(1);
|
|
for (const key of [
|
|
"reimbursements",
|
|
"invoices",
|
|
"workflow",
|
|
"paymentBatches",
|
|
"receipts",
|
|
"completeSubmitApprovePayReceiptLoops",
|
|
]) {
|
|
if (!(key in value.lifecycle)) process.exit(1);
|
|
}
|
|
process.stdout.write(JSON.stringify(value, null, 2));
|
|
' "$candidate_commit")" || fail "QUERY_OUTPUT_INVALID"
|
|
|
|
printf '%s\n' "$validated_output"
|
|
printf '[reimbursement-readiness-pre-runner][PASS] queryExecuted=1 zeroRetry=1\n' >&2
|