docs(security): add source control approval board [skip ci]

This commit is contained in:
Your Name
2026-05-12 23:43:48 +08:00
parent 5b144f0c92
commit cd72808560
9 changed files with 905 additions and 6 deletions

View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""產生 Gitea -> GitHub 逐 repo approval board。
此工具只讀取既有 redacted snapshot不呼叫 Gitea/GitHub API不需要 token。
用途是讓 AwoooP / PR reviewer 可以看見每個 repo 的下一個低摩擦決策點。
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def infer_lane(recommended_action: str) -> str:
if recommended_action == "hold_refs_reconcile":
return "refs_reconcile"
if recommended_action == "confirm_internal_remote_purpose":
return "internal_remote_purpose"
if recommended_action == "scope_review_only":
return "scope_review"
return "target_creation_or_access"
def required_decision(lane: str) -> str:
mapping = {
"refs_reconcile": "決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。",
"target_creation_or_access": "決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。",
"internal_remote_purpose": "決定 110 internal remote 是 active source、legacy mirror 或應降級。",
"scope_review": "決定此 repo 是否屬於 AWOOOI 資安供應鏈範圍。",
}
return mapping[lane]
def low_friction_next_step(lane: str) -> str:
mapping = {
"refs_reconcile": "先產生 draft reconcile plan不 push refs、不切 primary。",
"target_creation_or_access": "先取得 owner / visibility 決策,不自動建立 repo。",
"internal_remote_purpose": "先文件化用途與風險,不刪除 remote、不同步 refs。",
"scope_review": "只標記 scope review不納入主控切換。",
}
return mapping[lane]
def awooop_consumption(lane: str, approval_required: bool) -> str:
if lane == "scope_review":
return "scope_review_only"
if approval_required:
return "approval_candidate"
return "mirror_only"
def build_board(args: argparse.Namespace) -> dict[str, Any]:
decisions = load_json(Path(args.github_target_decision))
packages = load_json(Path(args.repo_approval_package))
gitea_inventory = load_json(Path(args.gitea_inventory))
package_by_repo = {
str(item.get("github_repo", "")): item
for item in packages.get("approval_items", [])
if isinstance(item, dict)
}
board_items: list[dict[str, Any]] = []
pending_count = 0
for decision in decisions.get("decisions", []):
if not isinstance(decision, dict):
continue
github_repo = str(decision.get("github_repo", ""))
package = package_by_repo.get(github_repo, {})
approval_required = bool(decision.get("approval_required", False))
if approval_required:
pending_count += 1
lane = infer_lane(str(decision.get("recommended_action", "")))
approval_status = str(package.get("approval_status") or ("pending" if approval_required else "not_required"))
blocked_until = package.get("blocked_until") or decision.get("blocked_until") or []
evidence_refs = sorted(
{
*[str(value) for value in decision.get("evidence_refs", [])],
*[str(value) for value in package.get("evidence_refs", [])],
}
)
board_items.append(
{
"github_repo": github_repo,
"source_key": str(decision.get("source_key", "")),
"lane": lane,
"risk": str(decision.get("risk", "LOW")),
"probe_status": str(decision.get("probe_status", "")),
"target_state": str(decision.get("target_state", "")),
"approval_status": approval_status,
"required_decision": required_decision(lane),
"low_friction_next_step": low_friction_next_step(lane),
"blocked_until": [str(value) for value in blocked_until],
"allowed_after_approval": [
str(value)
for value in (package.get("allowed_after_approval") or ["mirror_decision_only"])
],
"still_forbidden": [
str(value)
for value in (
package.get("still_forbidden")
or ["auto_execute", "sync_refs", "switch_primary"]
)
],
"evidence_refs": evidence_refs,
"awooop_consumption": awooop_consumption(lane, approval_required),
}
)
inventory_status = str(gitea_inventory.get("status", "blocked"))
gate_status = "ready" if inventory_status == "ok" else "blocked"
gate_reason = (
"Gitea authenticated 或 admin_export inventory 已完成。"
if gate_status == "ready"
else "GITEA_READONLY_TOKEN 未提供,且不使用可 push 的既有 remote credential 當 read-only tokenserver-side private/internal repo list 仍未完成。"
)
return {
"schema_version": "source_control_approval_board_v1",
"status": "draft",
"date": args.date,
"default_mode": "mirror_only",
"authenticated_inventory_gate": {
"status": gate_status,
"reason": gate_reason,
"allowed_next_step": [
"提供 read-only token 後重跑 gitea-repo-inventory",
"或提供 redacted admin export JSON",
"在 gate 前仍可維護 approval board 與 decision table",
],
"still_forbidden": [
"使用 write-capable credential 當作 read-only token",
"建立 GitHub repo",
"修改 repo visibility",
"sync refs",
"switch GitHub primary",
],
},
"item_count": len(board_items),
"pending_approval_count": pending_count,
"board_items": board_items,
}
def write_markdown(board: dict[str, Any], path: Path) -> None:
gate = board["authenticated_inventory_gate"]
lines = [
"# Source Control Approval Board",
"",
"| 項目 | 內容 |",
"|------|------|",
f"| 日期 | {board['date']} |",
f"| 狀態 | `{board['status']}` |",
f"| 預設模式 | `{board['default_mode']}` |",
f"| authenticated inventory gate | `{gate['status']}` |",
f"| gate 原因 | {gate['reason']} |",
f"| repo items | {board['item_count']} |",
f"| pending approval | {board['pending_approval_count']} |",
"",
"## 0. 核心原則",
"",
"本 board 只整理決策不授權執行。AwoooP 可以 mirror 成 approval candidate但不得建立 repo、修改 visibility、同步 refs、切 GitHub primary 或保存 credential value。",
"",
"## 1. 逐 repo 決策隊列",
"",
"| GitHub repo | Lane | Risk | Probe | Approval | 下一步 |",
"|-------------|------|------|-------|----------|--------|",
]
for item in board["board_items"]:
lines.append(
"| "
+ " | ".join(
[
f"`{item['github_repo']}`",
f"`{item['lane']}`",
f"`{item['risk']}`",
f"`{item['probe_status']}`",
f"`{item['approval_status']}`",
item["low_friction_next_step"],
]
)
+ " |"
)
lines.extend(["", "## 2. 詳細阻塞點", ""])
for item in board["board_items"]:
lines.extend(
[
f"### {item['github_repo']}",
"",
f"- Source key`{item['source_key']}`",
f"- Required decision{item['required_decision']}",
f"- AwoooP consumption`{item['awooop_consumption']}`",
"- Blocked until",
]
)
for value in item["blocked_until"]:
lines.append(f" - {value}")
lines.append("- Still forbidden")
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. Gate 前允許做的事",
"",
"1. 更新 read-only evidence。",
"2. 更新 approval board / decision table。",
"3. 寫 draft reconcile plan。",
"4. 把 pending approval mirror 到 AwoooP。",
"",
"## 4. Gate 前仍禁止",
"",
]
)
for value in gate["still_forbidden"]:
lines.append(f"- {value}")
lines.append("")
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--date", required=True)
parser.add_argument("--github-target-decision", default="docs/security/github-target-decision.snapshot.json")
parser.add_argument(
"--repo-approval-package",
default="docs/security/github-target-repo-approval-package.snapshot.json",
)
parser.add_argument("--gitea-inventory", default="docs/security/gitea-repo-inventory.snapshot.json")
parser.add_argument("--output-json", required=True)
parser.add_argument("--output-md", required=True)
args = parser.parse_args()
board = build_board(args)
Path(args.output_json).write_text(
json.dumps(board, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
write_markdown(board, Path(args.output_md))
print(f"OK source-control approval board items={board['item_count']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())