docs(security): add source control reconcile plan [skip ci]

This commit is contained in:
Your Name
2026-05-13 00:33:31 +08:00
parent 13b2db6563
commit 892e68dcca
12 changed files with 746 additions and 13 deletions

View 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())