文件
hyb-oa/.trellis/tasks/archive/2026-08/08-11-internal-oa-usage-census-and-simplification/research/run-production-usage-census.sh
T

155 行
7.1 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
TASK_RESEARCH_DIR="$ROOT_DIR/.trellis/tasks/08-11-internal-oa-usage-census-and-simplification/research"
CENSUS_SCRIPT="$TASK_RESEARCH_DIR/usage-census.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_USAGE_CENSUS"
# shellcheck source=../../../../../deploy/trialrun/deployment-contract-lib.sh
source "$CONTRACT_LIB"
fail() {
printf '[usage-census-runner][FAILED] code=%s\n' "$1" >&2
exit 1
}
shell_quote() {
printf "'%s'" "${1//\'/\'\\\'\'}"
}
mode="${1:---preflight}"
case "$mode" in
--preflight)
[[ "$#" -eq 1 ]] || fail "UNSUPPORTED_ARGUMENTS"
;;
--execute)
[[ "$#" -eq 2 && "${2:-}" == "$CONFIRMATION" ]] \
|| fail "EXPLICIT_CONFIRMATION_REQUIRED"
;;
*)
fail "UNSUPPORTED_ARGUMENTS"
;;
esac
for file in "$CENSUS_SCRIPT" "$IDENTITY_MODULE" "$IDENTITY_REMOTE" "$PREFLIGHT_SCRIPT"; do
[[ -f "$file" && ! -L "$file" ]] || fail "LOCAL_CONTRACT_MISSING"
done
cd "$ROOT_DIR"
# Reuse the reviewed history parsing and runbook-origin parser. Values remain
# in process memory and are never printed.
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"
# Identity-only mode exits before database and migration reads. It also proves
# the unique forced-command refresh identity and origin/runtime agreement.
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 '[usage-census-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"
printf '[usage-census-runner][PASS] preflight deployedSource=origin/main productDelta=0\n'
if [[ "$mode" == "--preflight" ]]; then
printf '[usage-census-runner][PASS] read_only_preflight_complete queryExecuted=0\n'
exit 0
fi
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")" < "$CENSUS_SCRIPT")" \
|| fail "QUERY_EXECUTION_FAILED"
validated_output="$(printf '%s' "$query_output" | node -e '
const fs = require("node:fs");
const expected = process.argv[1];
const input = fs.readFileSync(0, "utf8");
const value = JSON.parse(input);
if (value.status !== "PASS" || value.transactionReadOnly !== true) process.exit(1);
if (value.sourceBuildSha !== expected.slice(0, 12)) process.exit(1);
if (!Array.isArray(value.modules) || value.modules.length !== 20) process.exit(1);
if (value.modules.some((item) => item.queryStatus !== "PASS")) process.exit(1);
process.stdout.write(JSON.stringify(value, null, 2));
' "$candidate_commit")" || fail "QUERY_OUTPUT_INVALID"
printf '%s\n' "$validated_output"
printf '[usage-census-runner][PASS] queryExecuted=1 zeroRetry=1\n' >&2