docs(security): add source control reconcile plan [skip ci]
This commit is contained in:
234
scripts/security/source-control-reconcile-plan.py
Normal file
234
scripts/security/source-control-reconcile-plan.py
Normal file
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""產生 source-control refs reconcile plan 草案。
|
||||
|
||||
此工具只讀取既有 redacted snapshot,不呼叫遠端 Git,不 push、不 fetch、不改 remote。
|
||||
輸出用途是 review / AwoooP mirror / approval candidate,不是 execution plan runner。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_STILL_FORBIDDEN = [
|
||||
"push refs",
|
||||
"force push",
|
||||
"delete refs",
|
||||
"create GitHub repo",
|
||||
"change repo visibility",
|
||||
"switch GitHub primary",
|
||||
"disable Gitea",
|
||||
"move secret values",
|
||||
]
|
||||
|
||||
DEFAULT_EXECUTION_GATES = [
|
||||
"Gitea authenticated 或 admin_export server-side repo inventory status=ok",
|
||||
"branch-by-branch SHA diff 已完成",
|
||||
"tag-by-tag SHA diff 已完成",
|
||||
"workflow / webhook / runner / secret 名稱 inventory 已完成",
|
||||
"repo owner / visibility / branch protection / CODEOWNERS 已確認",
|
||||
"rollback plan 與 GitHub primary ADR 已完成",
|
||||
"人工批准只針對單一 repo 生效,不得批次套用到所有 repo",
|
||||
]
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def risk_for_repo(gitea_repo: str) -> str:
|
||||
if gitea_repo == "wooo/awoooi":
|
||||
return "HIGH"
|
||||
return "MEDIUM"
|
||||
|
||||
|
||||
def proposed_steps(event: dict[str, Any]) -> list[str]:
|
||||
gitea_repo = str(event.get("gitea_repo", ""))
|
||||
github_repo = str(event.get("github_repo", ""))
|
||||
steps = [
|
||||
f"針對 `{gitea_repo}` 與 `{github_repo}` 產生 branch-by-branch diff 表。",
|
||||
f"針對 `{gitea_repo}` 與 `{github_repo}` 產生 tag-by-tag diff 表。",
|
||||
"標記每個 diff 的真相來源候選:Gitea、GitHub、人工指定或 deprecated。",
|
||||
"列出 workflow / webhook / runner / secret 名稱差異,只記名稱不記 value。",
|
||||
"產生 dry-run PR / ADR 草案,仍不 push refs。",
|
||||
]
|
||||
if gitea_repo == "wooo/awoooi":
|
||||
steps.insert(
|
||||
0,
|
||||
"先確認目前 production deploy 真相來源與 deploy marker 流程,避免主控切換影響發版。",
|
||||
)
|
||||
return steps
|
||||
|
||||
|
||||
def build_plan(args: argparse.Namespace) -> dict[str, Any]:
|
||||
events = [load_json(Path(path)) for path in args.source_events]
|
||||
gitea_inventory = load_json(Path(args.gitea_inventory))
|
||||
|
||||
inventory_ready = gitea_inventory.get("status") == "ok"
|
||||
inventory_gate_status = "ready" if inventory_ready else "blocked"
|
||||
inventory_reason = (
|
||||
"Gitea server-side inventory 已完成,可在人工批准後進入更細 diff。"
|
||||
if inventory_ready
|
||||
else "Gitea authenticated / admin_export server-side inventory 尚未完成;本 plan 只能作草案,不可執行 refs sync。"
|
||||
)
|
||||
|
||||
plans: list[dict[str, Any]] = []
|
||||
for event in events:
|
||||
gitea_repo = str(event.get("gitea_repo", ""))
|
||||
github_repo = str(event.get("github_repo", ""))
|
||||
plans.append(
|
||||
{
|
||||
"gitea_repo": gitea_repo,
|
||||
"github_repo": github_repo,
|
||||
"risk": risk_for_repo(gitea_repo),
|
||||
"source_status": str(event.get("status", "")),
|
||||
"divergence_summary": {
|
||||
"gitea_branch_count": int(event.get("branch_count_gitea", 0)),
|
||||
"github_branch_count": int(event.get("branch_count_github", 0)),
|
||||
"gitea_tag_count": int(event.get("tag_count_gitea", 0)),
|
||||
"github_tag_count": int(event.get("tag_count_github", 0)),
|
||||
"gitea_main_sha": str(event.get("latest_sha_gitea", "")),
|
||||
"github_main_sha": str(event.get("latest_sha_github", "")),
|
||||
"blocking_reason": str(event.get("blocking_reason", "")),
|
||||
},
|
||||
"proposed_plan_steps": proposed_steps(event),
|
||||
"execution_gates": DEFAULT_EXECUTION_GATES,
|
||||
"allowed_now": [
|
||||
"更新 read-only evidence",
|
||||
"更新 approval board",
|
||||
"產生 draft reconcile plan",
|
||||
"讓 AwoooP mirror plan 狀態",
|
||||
],
|
||||
"still_forbidden": DEFAULT_STILL_FORBIDDEN,
|
||||
"evidence_refs": [str(value) for value in event.get("evidence_refs", [])],
|
||||
"awooop_consumption": "approval_candidate",
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"schema_version": "source_control_reconcile_plan_v1",
|
||||
"status": "draft_blocked",
|
||||
"date": args.date,
|
||||
"default_mode": "plan_only",
|
||||
"inventory_gate": {
|
||||
"status": inventory_gate_status,
|
||||
"reason": inventory_reason,
|
||||
"required_before_execution": DEFAULT_EXECUTION_GATES,
|
||||
},
|
||||
"plan_count": len(plans),
|
||||
"plans": plans,
|
||||
}
|
||||
|
||||
|
||||
def short_sha(value: str) -> str:
|
||||
return value[:8] if value else ""
|
||||
|
||||
|
||||
def write_markdown(plan: dict[str, Any], path: Path) -> None:
|
||||
gate = plan["inventory_gate"]
|
||||
lines = [
|
||||
"# Source Control Draft Reconcile Plan",
|
||||
"",
|
||||
"| 項目 | 內容 |",
|
||||
"|------|------|",
|
||||
f"| 日期 | {plan['date']} |",
|
||||
f"| 狀態 | `{plan['status']}` |",
|
||||
f"| 預設模式 | `{plan['default_mode']}` |",
|
||||
f"| inventory gate | `{gate['status']}` |",
|
||||
f"| gate 原因 | {gate['reason']} |",
|
||||
f"| plan count | {plan['plan_count']} |",
|
||||
"",
|
||||
"## 0. 核心結論",
|
||||
"",
|
||||
"這份文件只是 refs reconcile 草案,不是同步腳本,也不授權任何 GitHub primary 切換。AwoooP 可以 mirror 成 approval candidate,但不得執行 board item 或呼叫任何 push / sync 工具。",
|
||||
"",
|
||||
"## 1. Repo 差異摘要",
|
||||
"",
|
||||
"| Repo | Risk | Gitea branches | GitHub branches | Gitea tags | GitHub tags | Gitea main | GitHub main |",
|
||||
"|------|------|----------------|-----------------|------------|-------------|------------|-------------|",
|
||||
]
|
||||
for item in plan["plans"]:
|
||||
diff = item["divergence_summary"]
|
||||
lines.append(
|
||||
"| "
|
||||
+ " | ".join(
|
||||
[
|
||||
f"`{item['gitea_repo']} -> {item['github_repo']}`",
|
||||
f"`{item['risk']}`",
|
||||
f"`{diff['gitea_branch_count']}`",
|
||||
f"`{diff['github_branch_count']}`",
|
||||
f"`{diff['gitea_tag_count']}`",
|
||||
f"`{diff['github_tag_count']}`",
|
||||
f"`{short_sha(diff['gitea_main_sha'])}`",
|
||||
f"`{short_sha(diff['github_main_sha'])}`",
|
||||
]
|
||||
)
|
||||
+ " |"
|
||||
)
|
||||
|
||||
lines.extend(["", "## 2. Draft Plan", ""])
|
||||
for item in plan["plans"]:
|
||||
diff = item["divergence_summary"]
|
||||
lines.extend(
|
||||
[
|
||||
f"### {item['gitea_repo']} -> {item['github_repo']}",
|
||||
"",
|
||||
f"- 狀態:`{item['source_status']}`",
|
||||
f"- 阻塞原因:{diff['blocking_reason']}",
|
||||
"- 允許現在做:",
|
||||
]
|
||||
)
|
||||
for value in item["allowed_now"]:
|
||||
lines.append(f" - {value}")
|
||||
lines.append("- 草案步驟:")
|
||||
for value in item["proposed_plan_steps"]:
|
||||
lines.append(f" - {value}")
|
||||
lines.append("- 執行前 gate:")
|
||||
for value in item["execution_gates"]:
|
||||
lines.append(f" - {value}")
|
||||
lines.append("- 仍然禁止:")
|
||||
for value in item["still_forbidden"]:
|
||||
lines.append(f" - {value}")
|
||||
lines.append("- Evidence refs:")
|
||||
for value in item["evidence_refs"]:
|
||||
lines.append(f" - `{value}`")
|
||||
lines.append("")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"## 3. AwoooP 消費方式",
|
||||
"",
|
||||
"1. 只 mirror `source_control_reconcile_plan_v1`。",
|
||||
"2. 只顯示 `draft_blocked` 與 blocking reason。",
|
||||
"3. 可產生 approval candidate,但不得自動批准。",
|
||||
"4. 不得新增 execution action button。",
|
||||
"",
|
||||
]
|
||||
)
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--date", required=True)
|
||||
parser.add_argument("--gitea-inventory", default="docs/security/gitea-repo-inventory.snapshot.json")
|
||||
parser.add_argument("--source-event", action="append", dest="source_events", required=True)
|
||||
parser.add_argument("--output-json", required=True)
|
||||
parser.add_argument("--output-md", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
plan = build_plan(args)
|
||||
Path(args.output_json).write_text(
|
||||
json.dumps(plan, ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
write_markdown(plan, Path(args.output_md))
|
||||
print(f"OK source-control reconcile plan count={plan['plan_count']}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user