feat: add ops center and node onboarding flow
This commit is contained in:
317
domain-api/deploy/multi-region/review_go_live_bundle.sh
Executable file
317
domain-api/deploy/multi-region/review_go_live_bundle.sh
Executable file
@@ -0,0 +1,317 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user