Files
getDomain/domain-api/deploy/multi-region/export_go_live_bundle.sh
2026-04-18 23:52:51 +08:00

557 lines
20 KiB
Bash
Executable File

#!/usr/bin/env bash
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/lib_ops_center.sh"
ops_center_load_config "${SCRIPT_DIR}"
MAINLAND_BASE_URL_DEFAULT="$(ops_center_resolve_mainland_api_base_url)"
OVERSEAS_BASE_URL_DEFAULT="$(ops_center_resolve_overseas_api_base_url)"
REPORT_ROOT_DEFAULT="${OPS_CENTER_REPORT_ROOT}/go-live-bundles"
REPORT_DIR_INPUT="${1:-}"
MAINLAND_BASE_URL_INPUT="${2:-}"
OVERSEAS_BASE_URL_INPUT="${3:-}"
timestamp_now() {
date '+%Y%m%d_%H%M%S'
}
normalize_optional_base_url() {
local raw_value="${1:-}"
local fallback_value="${2:-}"
if [[ -z "${raw_value}" ]]; then
printf '%s' "${fallback_value}"
return
fi
case "${raw_value}" in
-|none|null|disabled)
printf ''
;;
*)
printf '%s' "${raw_value}"
;;
esac
}
if [[ -n "${REPORT_DIR_INPUT}" && "${REPORT_DIR_INPUT}" != http://* && "${REPORT_DIR_INPUT}" != https://* ]]; then
REPORT_DIR="${REPORT_DIR_INPUT}"
MAINLAND_BASE_URL="${MAINLAND_BASE_URL_INPUT:-${MAINLAND_BASE_URL_DEFAULT}}"
OVERSEAS_BASE_URL="$(normalize_optional_base_url "${OVERSEAS_BASE_URL_INPUT:-}" "${OVERSEAS_BASE_URL_DEFAULT}")"
else
REPORT_DIR="${REPORT_ROOT_DEFAULT}/go-live-bundle-$(timestamp_now)"
MAINLAND_BASE_URL="${REPORT_DIR_INPUT:-${MAINLAND_BASE_URL_DEFAULT}}"
OVERSEAS_BASE_URL="$(normalize_optional_base_url "${MAINLAND_BASE_URL_INPUT:-}" "${OVERSEAS_BASE_URL_DEFAULT}")"
fi
mkdir -p "${REPORT_DIR}"
CURL_CONNECT_TIMEOUT="${OPS_CENTER_CURL_CONNECT_TIMEOUT:-3}"
CURL_MAX_TIME="${OPS_CENTER_CURL_MAX_TIME:-15}"
STEP_COMMAND_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_COMMAND_TIMEOUT_SECONDS:-45}"
DOCTOR_DECISION_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_DOCTOR_DECISION_TIMEOUT_SECONDS:-120}"
DOCTOR_EXPORT_TIMEOUT_SECONDS="${OPS_CENTER_REPORT_DOCTOR_EXPORT_TIMEOUT_SECONDS:-180}"
DOCTOR_INNER_REPORT_TIMEOUT_SECONDS="${OPS_CENTER_DOCTOR_INNER_REPORT_TIMEOUT_SECONDS:-60}"
ENV_AUDIT_TXT_PATH="${REPORT_DIR}/00_env_audit.txt"
SUMMARY_TXT_PATH="${REPORT_DIR}/01_go_live_check_summary.txt"
GO_LIVE_JSON_PATH="${REPORT_DIR}/02_go_live_summary.json"
STACK_JSON_PATH="${REPORT_DIR}/03_stack_diagnosis.json"
DRIVER_FEED_JSON_PATH="${REPORT_DIR}/04_driver_feed.json"
CODEX_BRIEF_JSON_PATH="${REPORT_DIR}/05_codex_brief.json"
RELEASE_LAUNCHPAD_JSON_PATH="${REPORT_DIR}/06_release_launchpad.json"
DOCTOR_DECISION_JSON_PATH="${REPORT_DIR}/07_doctor_decision.json"
DOCTOR_EXPORT_STDOUT_PATH="${REPORT_DIR}/08_doctor_export_stdout.txt"
MANIFEST_PATH="${REPORT_DIR}/manifest.json"
declare -a STEP_RESULTS=()
pretty_print_json_file() {
local source_path="${1:-}"
python3 - "${source_path}" <<'PY'
import json
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
raw_text = path.read_text(encoding="utf-8") if path.exists() else ""
try:
payload = json.loads(raw_text)
except Exception:
print(raw_text)
else:
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY
}
record_step_result() {
local key="${1:-}"
local path="${2:-}"
local ok="${3:-false}"
local http_status="${4:-}"
local exit_code="${5:-0}"
STEP_RESULTS+=("${key}|${path}|${ok}|${http_status}|${exit_code}")
}
fetch_json_to_file() {
local key="${1:-}"
local url="${2:-}"
local output_path="${3:-}"
local tmp_path
tmp_path="$(mktemp)"
local raw_text http_status
raw_text="$(curl -sS \
--connect-timeout "${CURL_CONNECT_TIMEOUT}" \
--max-time "${CURL_MAX_TIME}" \
"${url}" \
-w $'\nHTTP_STATUS=%{http_code}' || true)"
http_status="${raw_text##*$'\n'HTTP_STATUS=}"
printf '%s' "${raw_text%$'\n'HTTP_STATUS=*}" > "${tmp_path}"
pretty_print_json_file "${tmp_path}" > "${output_path}"
rm -f "${tmp_path}"
if [[ "${http_status}" == 2* ]]; then
record_step_result "${key}" "${output_path}" "true" "${http_status}" "0"
return 0
fi
record_step_result "${key}" "${output_path}" "false" "${http_status}" "1"
return 1
}
capture_command_to_file() {
local key="${1:-}"
local output_path="${2:-}"
shift 2 || true
capture_command_to_file_with_timeout "${STEP_COMMAND_TIMEOUT_SECONDS}" "${key}" "${output_path}" "$@"
}
capture_command_to_file_with_timeout() {
local timeout_seconds="${1:-45}"
local key="${2:-}"
local output_path="${3:-}"
shift 3 || true
local -a wrapped_command=("$@")
local had_errexit="false"
case "$-" in
*e*) had_errexit="true" ;;
esac
if command -v timeout >/dev/null 2>&1; then
wrapped_command=(timeout "${timeout_seconds}" "$@")
fi
set +e
"${wrapped_command[@]}" > "${output_path}" 2>&1
local exit_code=$?
if [[ "${had_errexit}" == "true" ]]; then
set -e
else
set +e
fi
if [[ "${exit_code}" -eq 0 ]]; then
record_step_result "${key}" "${output_path}" "true" "" "${exit_code}"
return 0
fi
record_step_result "${key}" "${output_path}" "false" "" "${exit_code}"
return "${exit_code}"
}
materialize_go_live_summary_fallback() {
local summary_path="${1:-}"
local output_path="${2:-}"
python3 - "${summary_path}" "${output_path}" <<'PY'
import json
import pathlib
import sys
summary_path = pathlib.Path(sys.argv[1])
output_path = pathlib.Path(sys.argv[2])
raw_text = summary_path.read_text(encoding="utf-8") if summary_path.exists() else ""
start = raw_text.find("{")
end = raw_text.rfind("}")
if start < 0 or end < start:
sys.exit(1)
json_text = raw_text[start:end + 1]
payload = json.loads(json_text)
if not isinstance(payload, dict):
sys.exit(1)
output_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
PY
}
override_step_result() {
local key="${1:-}"
local path="${2:-}"
local ok="${3:-false}"
local http_status="${4:-}"
local exit_code="${5:-0}"
local -a updated_results=()
local raw_item existing_key
for raw_item in "${STEP_RESULTS[@]}"; do
existing_key="${raw_item%%|*}"
if [[ "${existing_key}" == "${key}" ]]; then
continue
fi
updated_results+=("${raw_item}")
done
STEP_RESULTS=("${updated_results[@]}")
record_step_result "${key}" "${path}" "${ok}" "${http_status}" "${exit_code}"
}
set +e
capture_command_to_file \
"env_audit" \
"${ENV_AUDIT_TXT_PATH}" \
bash "${SCRIPT_DIR}/drive_ops_center.sh" env-audit "${MAINLAND_BASE_URL}"
capture_command_to_file \
"go_live_check_summary" \
"${SUMMARY_TXT_PATH}" \
bash "${SCRIPT_DIR}/drive_ops_center.sh" go-live-check "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}" summary
fetch_json_to_file \
"go_live_summary" \
"${MAINLAND_BASE_URL}/api/v1/ops/go-live-summary?base_url=${MAINLAND_BASE_URL}" \
"${GO_LIVE_JSON_PATH}"
if [[ ! -s "${GO_LIVE_JSON_PATH}" || "$(sed -n '1p' "${GO_LIVE_JSON_PATH}" 2>/dev/null)" != "{"* ]]; then
if materialize_go_live_summary_fallback "${SUMMARY_TXT_PATH}" "${GO_LIVE_JSON_PATH}" 2>/dev/null; then
override_step_result "go_live_summary" "${GO_LIVE_JSON_PATH}" "true" "fallback" "0"
fi
fi
if [[ "$(python3 - "${GO_LIVE_JSON_PATH}" <<'PY'
import json
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except Exception:
print("invalid")
raise SystemExit(0)
if isinstance(payload, dict) and payload.get("detail") == "Not Found":
print("not_found")
else:
print("ok")
PY
)" == "not_found" ]]; then
if materialize_go_live_summary_fallback "${SUMMARY_TXT_PATH}" "${GO_LIVE_JSON_PATH}" 2>/dev/null; then
override_step_result "go_live_summary" "${GO_LIVE_JSON_PATH}" "true" "fallback" "0"
fi
fi
fetch_json_to_file \
"stack_diagnosis" \
"${MAINLAND_BASE_URL}/api/v1/ops/stack-diagnosis?base_url=${MAINLAND_BASE_URL}" \
"${STACK_JSON_PATH}"
fetch_json_to_file \
"driver_feed" \
"${MAINLAND_BASE_URL}/api/v1/ops/driver-feed" \
"${DRIVER_FEED_JSON_PATH}"
fetch_json_to_file \
"codex_brief" \
"${MAINLAND_BASE_URL}/api/v1/ops/codex-brief" \
"${CODEX_BRIEF_JSON_PATH}"
fetch_json_to_file \
"release_launchpad" \
"${MAINLAND_BASE_URL}/api/v1/ops/releases/launchpad" \
"${RELEASE_LAUNCHPAD_JSON_PATH}"
capture_command_to_file_with_timeout \
"${DOCTOR_EXPORT_TIMEOUT_SECONDS}" \
"doctor_export" \
"${DOCTOR_EXPORT_STDOUT_PATH}" \
env "OPS_CENTER_DOCTOR_REPORT_TIMEOUT_SECONDS=${DOCTOR_INNER_REPORT_TIMEOUT_SECONDS}" \
bash "${SCRIPT_DIR}/drive_ops_center.sh" doctor-export "${REPORT_DIR}/doctor-export" "${MAINLAND_BASE_URL}" "${OVERSEAS_BASE_URL}"
capture_command_to_file_with_timeout \
"${DOCTOR_DECISION_TIMEOUT_SECONDS}" \
"doctor_decision" \
"${DOCTOR_DECISION_JSON_PATH}" \
bash "${SCRIPT_DIR}/drive_ops_center.sh" doctor-decision "${REPORT_DIR}/doctor-export"
set -e
python3 - <<'PY' \
"${MANIFEST_PATH}" \
"${REPORT_DIR}" \
"${MAINLAND_BASE_URL}" \
"${OVERSEAS_BASE_URL}" \
"${ENV_AUDIT_TXT_PATH}" \
"${SUMMARY_TXT_PATH}" \
"${GO_LIVE_JSON_PATH}" \
"${STACK_JSON_PATH}" \
"${DRIVER_FEED_JSON_PATH}" \
"${CODEX_BRIEF_JSON_PATH}" \
"${RELEASE_LAUNCHPAD_JSON_PATH}" \
"${DOCTOR_DECISION_JSON_PATH}" \
"${DOCTOR_EXPORT_STDOUT_PATH}" \
"${STEP_RESULTS[@]}"
import json
import pathlib
import sys
manifest_path = pathlib.Path(sys.argv[1])
report_dir = pathlib.Path(sys.argv[2])
mainland_base_url = sys.argv[3]
overseas_base_url = sys.argv[4]
file_keys = [
"env_audit",
"go_live_check_summary",
"go_live_summary",
"stack_diagnosis",
"driver_feed",
"codex_brief",
"release_launchpad",
"doctor_decision",
"doctor_export",
]
file_paths = {
"env_audit": pathlib.Path(sys.argv[5]),
"go_live_check_summary": pathlib.Path(sys.argv[6]),
"go_live_summary": pathlib.Path(sys.argv[7]),
"stack_diagnosis": pathlib.Path(sys.argv[8]),
"driver_feed": pathlib.Path(sys.argv[9]),
"codex_brief": pathlib.Path(sys.argv[10]),
"release_launchpad": pathlib.Path(sys.argv[11]),
"doctor_decision": pathlib.Path(sys.argv[12]),
"doctor_export": pathlib.Path(sys.argv[13]),
}
step_results = {}
for raw_item in sys.argv[14:]:
parts = raw_item.split("|", 4)
if len(parts) != 5:
continue
key, path_value, ok_value, http_status, exit_code = parts
step_results[key] = {
"path": path_value,
"ok": ok_value == "true",
"http_status": http_status,
"exit_code": int(exit_code or 0),
}
def load_json_file(path: pathlib.Path) -> dict:
if not path.is_file():
return {}
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except Exception:
return {}
return payload if isinstance(payload, dict) else {}
def load_env_audit_summary(path: pathlib.Path) -> dict:
if not path.is_file():
return {}
raw_text = path.read_text(encoding="utf-8", errors="replace")
marker = "[8/8] condensed env audit summary"
candidate_text = raw_text.split(marker, 1)[1].strip() if marker in raw_text else raw_text.strip()
if not candidate_text:
return {}
try:
payload = json.loads(candidate_text)
except Exception:
return {}
return payload if isinstance(payload, dict) else {}
failures = []
artifacts = []
for key in file_keys:
artifact_path = file_paths[key]
result = dict(step_results.get(key) or {})
if not result:
result = {
"path": str(artifact_path),
"ok": artifact_path.exists(),
"http_status": "",
"exit_code": 0 if artifact_path.exists() else 1,
}
entry = {
"key": key,
"path": str(artifact_path),
"exists": artifact_path.exists(),
"ok": bool(result.get("ok", False)),
"http_status": str(result.get("http_status") or ""),
"exit_code": int(result.get("exit_code", 0) or 0),
}
artifacts.append(entry)
if not entry["ok"]:
failures.append(key)
go_live_summary_payload = load_json_file(file_paths["go_live_summary"])
stack_diagnosis_payload = load_json_file(file_paths["stack_diagnosis"])
driver_feed_payload = load_json_file(file_paths["driver_feed"])
codex_brief_payload = load_json_file(file_paths["codex_brief"])
env_audit_summary = load_env_audit_summary(file_paths["env_audit"])
stack_diagnosis = dict(stack_diagnosis_payload.get("diagnosis") or {})
driver_feed_summary = dict(driver_feed_payload.get("summary") or {})
codex_brief_summary = dict(codex_brief_payload.get("summary") or {})
def first_nonempty(*values: object) -> str:
for value in values:
normalized = str(value or "").strip()
if normalized:
return normalized
return ""
def nonempty_distinct(values: dict[str, str]) -> list[str]:
return sorted({str(value or "").strip() for value in values.values() if str(value or "").strip()})
def distinct_ints(values: dict[str, int], available_sources: set[str]) -> list[int]:
return sorted({int(values[source]) for source in values if source in available_sources})
launchpad_target_node_codes = {
"go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_target_node_code") or "").strip(),
"stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_target_node_code") or "").strip(),
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_target_node_code") or "").strip(),
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_target_node_code") or "").strip(),
}
launchpad_recovery_labels = {
"go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_recovery_label") or "").strip(),
"stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_recovery_label") or "").strip(),
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_label") or "").strip(),
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_label") or "").strip(),
}
launchpad_recovery_summaries = {
"go_live_summary": str(go_live_summary_payload.get("launchpad_recommended_recovery_summary") or "").strip(),
"stack_diagnosis": str(stack_diagnosis.get("launchpad_recommended_recovery_summary") or "").strip(),
"driver_feed": str(driver_feed_summary.get("launchpad_recommended_recovery_summary") or "").strip(),
"codex_brief": str(codex_brief_summary.get("launchpad_recommended_recovery_summary") or "").strip(),
}
launchpad_bootstrap_pending_nodes = {
"go_live_summary": int(go_live_summary_payload.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
"stack_diagnosis": int(stack_diagnosis.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
"driver_feed": int(driver_feed_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
"codex_brief": int(codex_brief_summary.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0),
}
launchpad_acceptance_ready_nodes = {
"go_live_summary": int(go_live_summary_payload.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
"stack_diagnosis": int(stack_diagnosis.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
"driver_feed": int(driver_feed_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
"codex_brief": int(codex_brief_summary.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0),
}
available_alignment_sources = {
source
for source, payload_value in {
"go_live_summary": go_live_summary_payload,
"stack_diagnosis": stack_diagnosis_payload,
"driver_feed": driver_feed_payload,
"codex_brief": codex_brief_payload,
}.items()
if isinstance(payload_value, dict) and payload_value
}
launchpad_alignment = {
"consistent": True,
"target_node_code_consistent": True,
"recovery_label_consistent": True,
"recovery_summary_consistent": True,
"bootstrap_pending_consistent": True,
"acceptance_ready_consistent": True,
"available_sources": sorted(available_alignment_sources),
"target_node_codes": launchpad_target_node_codes,
"recovery_labels": launchpad_recovery_labels,
"recovery_summaries": launchpad_recovery_summaries,
"bootstrap_pending_nodes": launchpad_bootstrap_pending_nodes,
"acceptance_ready_nodes": launchpad_acceptance_ready_nodes,
}
launchpad_alignment["target_node_code_consistent"] = len(nonempty_distinct({
source: value for source, value in launchpad_target_node_codes.items() if source in available_alignment_sources
})) <= 1
launchpad_alignment["recovery_label_consistent"] = len(nonempty_distinct({
source: value for source, value in launchpad_recovery_labels.items() if source in available_alignment_sources
})) <= 1
launchpad_alignment["recovery_summary_consistent"] = len(nonempty_distinct({
source: value for source, value in launchpad_recovery_summaries.items() if source in available_alignment_sources
})) <= 1
launchpad_alignment["bootstrap_pending_consistent"] = len(
distinct_ints(launchpad_bootstrap_pending_nodes, available_alignment_sources)
) <= 1
launchpad_alignment["acceptance_ready_consistent"] = len(
distinct_ints(launchpad_acceptance_ready_nodes, available_alignment_sources)
) <= 1
launchpad_alignment["consistent"] = all([
bool(launchpad_alignment["target_node_code_consistent"]),
bool(launchpad_alignment["recovery_label_consistent"]),
bool(launchpad_alignment["recovery_summary_consistent"]),
bool(launchpad_alignment["bootstrap_pending_consistent"]),
bool(launchpad_alignment["acceptance_ready_consistent"]),
])
env_audit_status = str(env_audit_summary.get("status") or "").strip()
env_audit_runtime_may_need_restart = bool(((env_audit_summary.get("runtime") or {}).get("runtime_may_need_restart", False)))
review_status_hint = "ready"
review_headline = "核心上线证据已经齐备,可进入最终人工复核或正式发布门禁。"
if failures or env_audit_status == "blocked":
review_status_hint = "blocked"
review_headline = "当前导出包仍有关键阻断项,不能直接作为正式上线终稿。"
elif env_audit_status == "attention" or not launchpad_alignment["consistent"]:
review_status_hint = "attention"
if not launchpad_alignment["consistent"]:
review_headline = "核心报告虽已导出,但 launchpad 摘要在不同 surface 之间存在漂移,建议先重新复核。"
else:
review_headline = "核心报告已齐,但环境审计仍提示存在待收口事项。"
payload = {
"report_dir": str(report_dir),
"mainland_base_url": mainland_base_url,
"overseas_base_url": overseas_base_url,
"artifacts": artifacts,
"failures": failures,
"summary": {
"ok": len(failures) == 0,
"artifact_total": len(artifacts),
"failure_total": len(failures),
"review_status_hint": review_status_hint,
"review_headline": review_headline,
"env_audit_status": env_audit_status,
"env_audit_runtime_may_need_restart": env_audit_runtime_may_need_restart,
"launchpad_recommended_target_node_code": first_nonempty(
go_live_summary_payload.get("launchpad_recommended_target_node_code"),
stack_diagnosis.get("launchpad_recommended_target_node_code"),
driver_feed_summary.get("launchpad_recommended_target_node_code"),
codex_brief_summary.get("launchpad_recommended_target_node_code"),
),
"launchpad_recommended_recovery_label": first_nonempty(
go_live_summary_payload.get("launchpad_recommended_recovery_label"),
stack_diagnosis.get("launchpad_recommended_recovery_label"),
driver_feed_summary.get("launchpad_recommended_recovery_label"),
codex_brief_summary.get("launchpad_recommended_recovery_label"),
),
"launchpad_recommended_recovery_summary": first_nonempty(
go_live_summary_payload.get("launchpad_recommended_recovery_summary"),
stack_diagnosis.get("launchpad_recommended_recovery_summary"),
driver_feed_summary.get("launchpad_recommended_recovery_summary"),
codex_brief_summary.get("launchpad_recommended_recovery_summary"),
),
"launchpad_onboarding_bootstrap_pending_nodes": max(launchpad_bootstrap_pending_nodes.values() or [0]),
"launchpad_onboarding_acceptance_ready_nodes": max(launchpad_acceptance_ready_nodes.values() or [0]),
"launchpad_alignment": launchpad_alignment,
"recommended_reading_order": [
"00_env_audit.txt",
"01_go_live_check_summary.txt",
"02_go_live_summary.json",
"03_stack_diagnosis.json",
"04_driver_feed.json",
"05_codex_brief.json",
"06_release_launchpad.json",
"07_doctor_decision.json",
"08_doctor_export_stdout.txt",
],
},
}
manifest_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(payload, ensure_ascii=False, indent=2))
PY