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

318 lines
17 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
TARGET_PATH="${1:-}"
if [[ -z "${TARGET_PATH}" ]]; then
echo "usage: bash domain-api/deploy/multi-region/review_go_live_bundle.sh <manifest.json|report_dir>" >&2
exit 1
fi
if [[ -d "${TARGET_PATH}" ]]; then
MANIFEST_PATH="${TARGET_PATH%/}/manifest.json"
else
MANIFEST_PATH="${TARGET_PATH}"
fi
if [[ ! -f "${MANIFEST_PATH}" ]]; then
echo "manifest not found: ${MANIFEST_PATH}" >&2
exit 1
fi
python3 - "${MANIFEST_PATH}" <<'PY'
import json
import pathlib
import sys
manifest_path = pathlib.Path(sys.argv[1])
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
artifacts = payload.get("artifacts") or []
failures = payload.get("failures") or []
summary = payload.get("summary") or {}
critical_keys = {"go_live_summary", "stack_diagnosis", "driver_feed", "codex_brief", "release_launchpad"}
artifact_by_key = {str(item.get("key") or ""): item for item in artifacts if isinstance(item, dict)}
critical_failures = [key for key in critical_keys if not bool((artifact_by_key.get(key) or {}).get("ok", False))]
noncritical_failures = [key for key in failures if key not in critical_keys]
env_audit_artifact = artifact_by_key.get("env_audit") or {}
env_audit_status = ""
env_audit_headline = ""
env_audit_missing_items = []
env_audit_recommended_actions = []
env_audit_blocking = False
env_audit_attention = False
env_audit_runtime_may_need_restart = False
go_live_summary_payload = {}
stack_diagnosis_payload = {}
driver_feed_payload = {}
codex_brief_payload = {}
def load_json_artifact(key: str) -> dict:
artifact = artifact_by_key.get(key) or {}
artifact_path = pathlib.Path(str(artifact.get("path") or ""))
if not artifact_path.is_file():
return {}
try:
loaded = json.loads(artifact_path.read_text(encoding="utf-8"))
except Exception:
return {}
return loaded if isinstance(loaded, dict) else {}
env_audit_path = pathlib.Path(str(env_audit_artifact.get("path") or ""))
if env_audit_path.is_file():
raw_text = env_audit_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 candidate_text:
try:
env_payload = json.loads(candidate_text)
except Exception:
env_payload = {}
env_audit_status = str(env_payload.get("status") or "")
env_audit_headline = str(env_payload.get("headline") or "")
env_audit_missing_items = list(env_payload.get("missing_items") or [])
env_audit_recommended_actions = [str(item).strip() for item in list(env_payload.get("recommended_actions") or []) if str(item).strip()]
env_audit_runtime_may_need_restart = bool(((env_payload.get("runtime") or {}).get("runtime_may_need_restart", False)))
env_audit_blocking = env_audit_status == "blocked"
env_audit_attention = env_audit_status == "attention"
go_live_summary_payload = load_json_artifact("go_live_summary")
stack_diagnosis_payload = load_json_artifact("stack_diagnosis")
driver_feed_payload = load_json_artifact("driver_feed")
codex_brief_payload = load_json_artifact("codex_brief")
go_live_launchpad_target_node_code = str(go_live_summary_payload.get("launchpad_recommended_target_node_code") or "").strip()
go_live_launchpad_recovery_label = str(go_live_summary_payload.get("launchpad_recommended_recovery_label") or "").strip()
go_live_launchpad_recovery_summary = str(go_live_summary_payload.get("launchpad_recommended_recovery_summary") or "").strip()
go_live_launchpad_bootstrap_pending_nodes = int(go_live_summary_payload.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0)
go_live_launchpad_acceptance_ready_nodes = int(go_live_summary_payload.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0)
stack_diagnosis = stack_diagnosis_payload.get("diagnosis") or {}
stack_launchpad_target_node_code = str(stack_diagnosis.get("launchpad_recommended_target_node_code") or "").strip()
stack_launchpad_recovery_label = str(stack_diagnosis.get("launchpad_recommended_recovery_label") or "").strip()
stack_launchpad_recovery_summary = str(stack_diagnosis.get("launchpad_recommended_recovery_summary") or "").strip()
stack_launchpad_bootstrap_pending_nodes = int(stack_diagnosis.get("launchpad_onboarding_bootstrap_pending_nodes", 0) or 0)
stack_launchpad_acceptance_ready_nodes = int(stack_diagnosis.get("launchpad_onboarding_acceptance_ready_nodes", 0) or 0)
driver_feed_summary = driver_feed_payload.get("summary") or {}
codex_brief_summary = codex_brief_payload.get("summary") or {}
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": go_live_launchpad_target_node_code,
"stack_diagnosis": stack_launchpad_target_node_code,
"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": go_live_launchpad_recovery_label,
"stack_diagnosis": stack_launchpad_recovery_label,
"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": go_live_launchpad_recovery_summary,
"stack_diagnosis": stack_launchpad_recovery_summary,
"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": go_live_launchpad_bootstrap_pending_nodes,
"stack_diagnosis": stack_launchpad_bootstrap_pending_nodes,
"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": go_live_launchpad_acceptance_ready_nodes,
"stack_diagnosis": stack_launchpad_acceptance_ready_nodes,
"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_target_consistent = len(nonempty_distinct({
source: value for source, value in launchpad_target_node_codes.items() if source in available_alignment_sources
})) <= 1
launchpad_recovery_label_consistent = len(nonempty_distinct({
source: value for source, value in launchpad_recovery_labels.items() if source in available_alignment_sources
})) <= 1
launchpad_recovery_summary_consistent = len(nonempty_distinct({
source: value for source, value in launchpad_recovery_summaries.items() if source in available_alignment_sources
})) <= 1
launchpad_bootstrap_pending_consistent = len(distinct_ints(launchpad_bootstrap_pending_nodes, available_alignment_sources)) <= 1
launchpad_acceptance_ready_consistent = len(distinct_ints(launchpad_acceptance_ready_nodes, available_alignment_sources)) <= 1
launchpad_alignment_consistent = all([
launchpad_target_consistent,
launchpad_recovery_label_consistent,
launchpad_recovery_summary_consistent,
launchpad_bootstrap_pending_consistent,
launchpad_acceptance_ready_consistent,
])
node_agent_env_missing = any(
str(item).strip() == "missing_env:/etc/default/domaincheck-node-agent"
for item in env_audit_missing_items
)
if critical_failures or env_audit_blocking:
status = "blocked"
if critical_failures and env_audit_blocking:
headline = "关键报告存在缺失,且环境审计已给出阻断结论,当前不适合上线。"
elif critical_failures:
headline = "上线证据包仍缺关键报告,暂不建议直接宣称收口完成。"
else:
headline = "环境审计已给出阻断结论,需先修复现场环境后再进入发布门禁。"
elif noncritical_failures or env_audit_attention or not launchpad_alignment_consistent:
status = "attention"
if not launchpad_alignment_consistent:
headline = "核心报告已齐,但 launchpad 摘要在不同 surface 之间存在漂移,建议先复核再发版。"
elif noncritical_failures and env_audit_attention:
headline = "核心报告已齐,但补充报告和环境审计都提示仍有缺口,建议先复核再发版。"
elif noncritical_failures:
headline = "核心报告已齐,但仍有补充报告超时或失败,建议先复核再发版。"
else:
headline = "核心报告已齐,但环境审计仍提示基础缺口,建议补齐后再发版。"
else:
status = "ready"
headline = "核心上线证据已经齐备,可进入最终人工复核或正式发布门禁。"
result = {
"report_dir": str(payload.get("report_dir") or manifest_path.parent),
"status": status,
"headline": headline,
"artifact_total": int(summary.get("artifact_total", len(artifacts)) or len(artifacts)),
"failure_total": int(summary.get("failure_total", len(failures)) or len(failures)),
"critical_failures": critical_failures,
"noncritical_failures": noncritical_failures,
"env_audit": {
"ok": bool(env_audit_artifact.get("ok", False)),
"status": env_audit_status,
"headline": env_audit_headline,
"missing_items": env_audit_missing_items,
"runtime_may_need_restart": env_audit_runtime_may_need_restart,
"recommended_actions": env_audit_recommended_actions,
},
"go_live_summary": {
"go_live_status": str(go_live_summary_payload.get("go_live_status") or "").strip(),
"publish_status": str(go_live_summary_payload.get("publish_status") or "").strip(),
"operator_title": str(go_live_summary_payload.get("operator_title") or "").strip(),
"next_step_action_code": str(go_live_summary_payload.get("next_step_action_code") or "").strip(),
"launchpad_recommended_target_node_code": go_live_launchpad_target_node_code,
"launchpad_recommended_recovery_label": go_live_launchpad_recovery_label,
"launchpad_recommended_recovery_summary": go_live_launchpad_recovery_summary,
"launchpad_onboarding_bootstrap_pending_nodes": go_live_launchpad_bootstrap_pending_nodes,
"launchpad_onboarding_acceptance_ready_nodes": go_live_launchpad_acceptance_ready_nodes,
},
"stack_diagnosis": {
"stack_status": str((stack_diagnosis_payload.get("diagnosis") or {}).get("stack_status") or "").strip(),
"operator_title": str(((stack_diagnosis_payload.get("diagnosis") or {}).get("operator_decision") or {}).get("title") or "").strip(),
"next_step_action_code": str(((stack_diagnosis_payload.get("diagnosis") or {}).get("next_step") or {}).get("action_code") or "").strip(),
"launchpad_recommended_target_node_code": stack_launchpad_target_node_code,
"launchpad_recommended_recovery_label": stack_launchpad_recovery_label,
"launchpad_recommended_recovery_summary": stack_launchpad_recovery_summary,
"launchpad_onboarding_bootstrap_pending_nodes": stack_launchpad_bootstrap_pending_nodes,
"launchpad_onboarding_acceptance_ready_nodes": stack_launchpad_acceptance_ready_nodes,
},
"driver_feed": {
"headline": str(driver_feed_payload.get("headline") or "").strip(),
"launch_status": str(((driver_feed_payload.get("automation_coverage") or {}).get("launch_status") or "")).strip(),
"launch_ready": bool(((driver_feed_payload.get("automation_coverage") or {}).get("launch_ready", False))),
"launchpad_recommended_target_node_code": str(
((driver_feed_payload.get("summary") or {}).get("launchpad_recommended_target_node_code") or "")
).strip(),
"launchpad_recommended_recovery_label": str(
((driver_feed_payload.get("summary") or {}).get("launchpad_recommended_recovery_label") or "")
).strip(),
},
"codex_brief": {
"headline": str(codex_brief_payload.get("headline") or "").strip(),
"focus_entry_key": str(((codex_brief_payload.get("focus") or {}).get("entry_key") or "")).strip(),
"launch_status": str(((codex_brief_payload.get("automation_coverage") or {}).get("launch_status") or "")).strip(),
"launch_ready": bool(((codex_brief_payload.get("automation_coverage") or {}).get("launch_ready", False))),
"launchpad_recommended_target_node_code": str(
((codex_brief_payload.get("summary") or {}).get("launchpad_recommended_target_node_code") or "")
).strip(),
"launchpad_recommended_recovery_label": str(
((codex_brief_payload.get("summary") or {}).get("launchpad_recommended_recovery_label") or "")
).strip(),
},
"launchpad_alignment": {
"consistent": launchpad_alignment_consistent,
"target_node_code_consistent": launchpad_target_consistent,
"recovery_label_consistent": launchpad_recovery_label_consistent,
"recovery_summary_consistent": launchpad_recovery_summary_consistent,
"bootstrap_pending_consistent": launchpad_bootstrap_pending_consistent,
"acceptance_ready_consistent": launchpad_acceptance_ready_consistent,
"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,
},
"recommended_reading_order": list(summary.get("recommended_reading_order") or []),
"recommended_next_steps": [
"先看 00_env_audit.txt确认当前机器环境是否存在阻断或注意项",
"先看 02_go_live_summary.json 和 03_stack_diagnosis.json",
"再看 04_driver_feed.json 和 05_codex_brief.json",
"若涉及发布门禁,再看 06_release_launchpad.json",
"若 manifest 里仍有失败项,再看对应 artifact 原文件",
]
+ (
[
"当前缺口指向 Node Agent 未接管:先执行 agent-gap-export拿到首个缺口节点的接管包",
"随后执行 node-bootstrap-plan为缺口节点生成可直接落地的接管脚本",
"如果仓库代码已更新但 bootstrap-plan 输出仍偏旧,先重启控制面 API 再重新导出接管计划",
]
if node_agent_env_missing
else []
)
+ (
[
"env-audit 已识别运行中 API 可能仍是旧代码:先执行 drive_ops_center.sh runtime-refresh-recover按固定链路完成 API 重启与复检",
"若仍需人工逐条确认,再执行 recommended_actions 里的 API 重启与复检命令"
]
if env_audit_runtime_may_need_restart
else []
)
+ (
[
f"当前 launchpad 缺口已经收敛到节点 {go_live_launchpad_target_node_code},建议优先执行:{go_live_launchpad_recovery_label or '节点恢复'}",
"先对照 02_go_live_summary.json、04_driver_feed.json、05_codex_brief.json 中的 launchpad 字段,确认三处摘要是否一致",
(go_live_launchpad_recovery_summary or "如果 recovery_summary 仍为空,再回到 06_release_launchpad.json 查看 gap rows 详情"),
]
if go_live_launchpad_target_node_code
else []
)
+ (
[
"当前 bundle 内的 launchpad 摘要在 go_live_summary / stack_diagnosis / driver_feed / codex_brief 之间存在不一致,先不要直接交付。",
"优先复核 02_go_live_summary.json 与 03_stack_diagnosis.json再检查 04_driver_feed.json 与 05_codex_brief.json 是否使用了同一版控制面数据。",
"若只有局部摘要偏旧,先执行 drive_ops_center.sh runtime-refresh-recover并重新导出 go-live bundle。",
]
if not launchpad_alignment_consistent
else []
),
}
print(json.dumps(result, ensure_ascii=False, indent=2))
PY