diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 0923e6e7..5cf9a91c 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,32 @@ +## 2026-05-13 | Security Supply Chain branch/tag 明細 diff + +**背景**:統帥批准繼續推進後,本輪先把最新 `gitea/main` 合入 Security Supply Chain PR #117,保留 AwoooP T3/T4 已推版紀錄,再進行 refs-blocked repo 的 branch/tag 明細盤點。此階段仍只做 read-only evidence,不執行同步。 + +**本次交付**: +- 新增 `scripts/security/source-control-ref-detail-diff.py`,只執行 read-only `git ls-remote --heads/--tags`,不 fetch、不 push、不改 remote。 +- 新增 `docs/schemas/source_control_ref_detail_diff_v1.schema.json`。 +- 產出 `docs/security/source-control-ref-detail-diff.snapshot.json` 與 `docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md`。 +- 明細 diff 涵蓋 `awoooi`、`clawbot-v5`、`wooo-aiops` 三個 refs-blocked mapped repos。 +- 本輪忽略 PR 分支 `codex/security-supply-chain-contracts-20260512`,避免 evidence 因本 PR 自己推版而自我污染。 +- 更新 `SOURCE-CONTROL-RECONCILE-PLAN.md`,以 ref detail diff 更新 `awoooi` 最新 Gitea main SHA 與分支數。 +- 更新 `SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST`,contract count 從 14 增至 15,新增 `source_control_ref_detail_diff_v1`。 + +**盤點結果**: +- `wooo/awoooi`:Gitea-only branches 115、GitHub-only branches 0、branch SHA diff 1、Gitea-only tags 2、GitHub-only tags 0。 +- `wooo/clawbot-v5`:branch SHA diff 1、Gitea-only tags 1。 +- `wooo/wooo-aiops`:GitHub-only branch 1、branch SHA diff 1、GitHub-only tags 19。 + +**邊界**: +- 未 fetch、未 push refs、未 force push、未刪 refs、未建立 GitHub repo、未改 visibility、未切 GitHub primary、未部署。 +- AwoooP 只可 mirror `source_control_ref_detail_diff_v1`,不得依此新增 execution action button。 + +**驗證**: +- `source-control-ref-detail-diff.py` 產生 3 repo detail diff。 +- JSON / schema / snapshot parse 通過。 +- `scripts/security/*.py` 可編譯。 +- `git diff --check` 通過。 +- PR diff added lines 未命中本輪敏感 token / credential pattern。 + ## 2026-05-13 | T4 Config Drift fingerprint repeat-state 已推版 **背景**:Config Drift Telegram 卡片只顯示單次 `report_id` 與 HIGH/MEDIUM/INFO 計數,Operator 無法判斷是否同一漂移一直重複、已跑到哪個流程階段、是否需要人工。舊 truth-chain repeat 只用 namespace/status/counts 分組,會把「剛好同計數但 items 不同」誤認為同一漂移。 diff --git a/docs/schemas/source_control_ref_detail_diff_v1.schema.json b/docs/schemas/source_control_ref_detail_diff_v1.schema.json new file mode 100644 index 00000000..639529fb --- /dev/null +++ b/docs/schemas/source_control_ref_detail_diff_v1.schema.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:awoooi:source-control-ref-detail-diff-v1", + "title": "AWOOOI Source Control Ref Detail Diff (v1)", + "description": "針對 Gitea / GitHub refs-blocked repo 保存 branch/tag 層級 read-only diff 明細。", + "type": "object", + "required": [ + "schema_version", + "status", + "date", + "default_mode", + "ignored_branches", + "repo_count", + "repos" + ], + "properties": { + "schema_version": { + "const": "source_control_ref_detail_diff_v1" + }, + "status": { + "type": "string", + "enum": ["draft_blocked"] + }, + "date": { + "type": "string" + }, + "default_mode": { + "type": "string", + "enum": ["read_only_diff"] + }, + "ignored_branches": { + "type": "array", + "items": {"type": "string"} + }, + "repo_count": { + "type": "integer", + "minimum": 0 + }, + "repos": { + "type": "array", + "items": { + "type": "object", + "required": [ + "repo_key", + "repo_path", + "gitea_remote", + "github_remote", + "gitea_url_redacted", + "github_url_redacted", + "gitea_repo", + "github_repo", + "status", + "blocking_reason", + "branch_diff", + "tag_diff", + "still_forbidden" + ], + "properties": { + "repo_key": {"type": "string"}, + "repo_path": {"type": "string"}, + "gitea_remote": {"type": "string"}, + "github_remote": {"type": "string"}, + "gitea_url_redacted": {"type": "string"}, + "github_url_redacted": {"type": "string"}, + "gitea_repo": {"type": "string"}, + "github_repo": {"type": "string"}, + "status": { + "type": "string", + "enum": ["blocked", "verified", "probe_failed"] + }, + "blocking_reason": {"type": "string"}, + "branch_diff": {"$ref": "#/$defs/refDiff"}, + "tag_diff": {"$ref": "#/$defs/refDiff"}, + "still_forbidden": { + "type": "array", + "items": {"type": "string"} + } + }, + "additionalProperties": false + } + } + }, + "$defs": { + "refItem": { + "type": "object", + "required": ["name", "sha"], + "properties": { + "name": {"type": "string"}, + "sha": {"type": "string"} + }, + "additionalProperties": false + }, + "refMismatch": { + "type": "object", + "required": ["name", "gitea_sha", "github_sha"], + "properties": { + "name": {"type": "string"}, + "gitea_sha": {"type": "string"}, + "github_sha": {"type": "string"} + }, + "additionalProperties": false + }, + "refDiff": { + "type": "object", + "required": [ + "gitea_count", + "github_count", + "only_gitea_count", + "only_github_count", + "sha_mismatch_count", + "matching_count", + "only_gitea", + "only_github", + "sha_mismatch", + "matching" + ], + "properties": { + "gitea_count": {"type": "integer", "minimum": 0}, + "github_count": {"type": "integer", "minimum": 0}, + "only_gitea_count": {"type": "integer", "minimum": 0}, + "only_github_count": {"type": "integer", "minimum": 0}, + "sha_mismatch_count": {"type": "integer", "minimum": 0}, + "matching_count": {"type": "integer", "minimum": 0}, + "only_gitea": { + "type": "array", + "items": {"$ref": "#/$defs/refItem"} + }, + "only_github": { + "type": "array", + "items": {"$ref": "#/$defs/refItem"} + }, + "sha_mismatch": { + "type": "array", + "items": {"$ref": "#/$defs/refMismatch"} + }, + "matching": { + "type": "array", + "items": {"$ref": "#/$defs/refItem"} + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md index 3dd8d76f..fe67bd87 100644 --- a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md +++ b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md @@ -35,6 +35,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `github_target_repo_approval_package_v1` | GitHub target 逐 repo approval package | Approval queue、Migration target evidence | mirror-only | 低摩擦,只 gate 高風險執行 | | `source_control_approval_board_v1` | 逐 repo owner / visibility / canonical / refs 決策 board | Approval queue、PR reviewer handoff | approval-only | 只顯示決策隊列,不執行 board item | | `source_control_reconcile_plan_v1` | refs-blocked repo draft reconcile plan | Approval candidate、migration reviewer handoff | approval-only | 只顯示草案,不 push refs、不切 primary | +| `source_control_ref_detail_diff_v1` | refs-blocked repo branch/tag 明細 diff | Migration reviewer evidence | mirror-only | 只顯示 diff,不 fetch、不 push、不刪 refs | | `local_repo_canonical_probe_v1` | 本機 working tree lineage 比對 | Canonical decision evidence | mirror-only | 不自動合併、不自動建 repo、不刪除 | | `git_remote_refs_probe_v1` | 指定 repo remote refs read-only probe | Source readiness evidence | mirror-only | 不 fetch、不 push、不自動 mirror | | `approval_required_event_v1` | 上述事件的高風險 gate | Approval queue、Audit | approval-only | `blocked_until_approved=true` | @@ -85,6 +86,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `github_target_repo_approval_package_v1.status=draft` | `observe` | 建立 approval queue draft,不阻擋 read-only evidence | | `source_control_approval_board_v1.pending_approval_count>0` | `approve_required` | 顯示逐 repo 決策隊列,不執行 repo 建立、visibility 修改、refs sync | | `source_control_reconcile_plan_v1.status=draft_blocked` | `approve_required` | 只顯示 refs reconcile 草案與 gate,不執行 sync | +| `source_control_ref_detail_diff_v1.status=draft_blocked` | `observe` | 顯示 branch/tag 明細 diff,支援人工 review | | `local_repo_canonical_probe_v1.status=unrelated` | `approve_required` | 禁止自動合併,需人工 canonical 判定 | | `git_remote_refs_probe_v1.status=ok` | `observe` | 可作 source evidence,但仍需 GitHub target 與 approval | | `security_rollout_policy_v1.enforcement_level=mirror_only` | `observe` | 只顯示 policy,不阻擋既有流程 | @@ -127,6 +129,7 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | GitHub target repo approval package | `docs/security/github-target-repo-approval-package.snapshot.json` / `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | | Source Control approval board | `docs/security/source-control-approval-board.snapshot.json` / `docs/security/SOURCE-CONTROL-APPROVAL-BOARD.md` | | Source Control draft reconcile plan | `docs/security/source-control-reconcile-plan.snapshot.json` / `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` | +| Source Control branch/tag detail diff | `docs/security/source-control-ref-detail-diff.snapshot.json` / `docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md` | | 本機 repo canonical lineage snapshot | `docs/security/local-repo-canonical-ewoooc-momo.snapshot.json` / `docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md` | | Internal 110 refs snapshot | `docs/security/git-remote-refs-bitan-tsenyang.snapshot.json` / `docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md` | | wooo-infra-config refs snapshot | `docs/security/git-remote-refs-wooo-infra-config.snapshot.json` / `docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md` | diff --git a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md index d6d3a5b6..d2173959 100644 --- a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md +++ b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md @@ -539,6 +539,8 @@ Console 初期不提供高風險 action button。 2026-05-12 draft reconcile plan 追加:已新增 `scripts/security/source-control-reconcile-plan.py`、`docs/schemas/source_control_reconcile_plan_v1.schema.json`,並產出 `docs/security/source-control-reconcile-plan.snapshot.json` 與 `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md`。此 plan 只涵蓋 `awoooi`、`clawbot-v5`、`wooo-aiops` 三個 refs-blocked mapped repos,狀態為 `draft_blocked`;AwoooP 可 mirror 成 approval candidate,但不得 push refs、force push、刪 refs、切 GitHub primary。 +2026-05-13 branch/tag detail diff 追加:已新增 `scripts/security/source-control-ref-detail-diff.py`、`docs/schemas/source_control_ref_detail_diff_v1.schema.json`,並產出 `docs/security/source-control-ref-detail-diff.snapshot.json` 與 `docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md`。最新 read-only diff 忽略本 PR 分支後,`awoooi` 仍有 Gitea-only branches 115、Gitea-only tags 2、main SHA 不一致;`clawbot-v5` main SHA 不一致且 GitHub 缺 Gitea tag;`wooo-aiops` GitHub 有 1 條額外 branch 與 19 個 GitHub-only tags。AwoooP 可 mirror 此 evidence,但不得 fetch、push、delete refs 或切 primary。 + 2026-05-12 public search / canonical 追加:Gitea public search 在未提供 token 時可見 `wooo/awoooi`、`wooo/ewoooc`。已新增 `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md`,其中 `wooo/ewoooc`、`root/momo-pro-system`、`momo-pro-system`、`momo_pro_system` 仍需人工判定 canonical 關係,不得自動合併。 2026-05-12 GitHub target probe 追加:已新增 `scripts/security/github-target-probe.py`、`docs/schemas/github_target_probe_v1.schema.json` 與 `docs/security/github-target-probe.snapshot.json`。8 個候選中 5 個可讀,`owenhytsai/ewoooc`、`owenhytsai/bitan-pharmacy`、`owenhytsai/tsenyang-website` 為 `not_found_or_private`。 diff --git a/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md b/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md index 4e2e5d92..c7dba33f 100644 --- a/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md +++ b/docs/security/GITEA-GITHUB-MIGRATION-INVENTORY.md @@ -24,6 +24,7 @@ | GitHub target 決策表 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` / `docs/security/github-target-decision.snapshot.json` | | GitHub target repo-by-repo approval package | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` / `docs/security/github-target-repo-approval-package.snapshot.json` | | Source Control draft reconcile plan | `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` / `docs/security/source-control-reconcile-plan.snapshot.json` | +| Source Control branch/tag detail diff | `docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md` / `docs/security/source-control-ref-detail-diff.snapshot.json` | | Source Control 遷移矩陣 | `docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md` | | Canonical repo 判定表 | `docs/security/SOURCE-CONTROL-CANONICAL-DECISION-TABLE.md` | @@ -34,7 +35,7 @@ 第二輪只讀盤點顯示,至少目前工作中的 `awoooi` repo 存在以下差異: - GitHub `origin` 與 Gitea `gitea` 的 `main` SHA 不一致。 -- Gitea 有大量 `drift/adopt-*` 分支,GitHub 沒有;截至 2026-05-12 最新 snapshot,Gitea heads 為 117 條,GitHub heads 為 2 條。 +- Gitea 有大量 `drift/adopt-*` 分支,GitHub 沒有;截至 2026-05-13 最新 ref detail diff,忽略本 PR 分支後 Gitea heads 為 117 條,GitHub heads 為 2 條。 - Gitea 有 release tags,GitHub 目前查不到 tags。 - 本機 `gitea` remote URL 內嵌憑證,這是 credential hygiene 風險;不得寫入文件、不得複製到 GitHub,後續需移除並輪替。 - Gitea `wooo` user endpoint 在未提供 token 時可見 `wooo/awoooi` 與 `wooo/ewoooc`,目前 `gitea_repo_inventory_v1.status=partial`。 @@ -56,7 +57,7 @@ |------|------| | `git remote -v` | 已確認 `origin` 指向 GitHub,`gitea` 指向本地 Gitea;未在文件中保存憑證 | | GitHub heads | 2 條 | -| Gitea heads | 117 條 | +| Gitea heads | 117 條,已忽略本 PR 分支 | | GitHub tags | 0 條 | | Gitea tag refs | 4 條 raw refs,實際 tag 為 `v7.2.0`、`v7.3.0` | | Gitea org API | 未認證查詢 `http://192.168.0.110:3001/api/v1/orgs/wooo/repos` 回 404,保留為 endpoint 判定 evidence | @@ -74,6 +75,7 @@ | GitHub target 決策 snapshot | `docs/security/github-target-decision.snapshot.json`,依前述 read-only evidence 人工彙整,非執行工具,不授權 repo 建立或 visibility 修改 | | GitHub target repo-by-repo approval snapshot | `docs/security/github-target-repo-approval-package.snapshot.json`,逐 repo 拆分 approval path,不授權執行 | | Source Control draft reconcile plan | `docs/security/source-control-reconcile-plan.snapshot.json`,只產生 `draft_blocked` 草案,不授權 refs sync | +| Source Control branch/tag detail diff | `docs/security/source-control-ref-detail-diff.snapshot.json`,保存 3 個 refs-blocked mapped repos 的 branch/tag 明細,不授權 fetch/push | ## 1.1 Gitea repo list snapshot @@ -144,7 +146,7 @@ GitHub target repo-by-repo approval package 已建立於 `docs/security/GITHUB-T | 欄位 | Gitea | GitHub | 狀態 | |------|-------|--------|------| | Repo | `wooo/awoooi` | `owenhytsai/awoooi` | 已有對應 | -| `main` | `0bc187877884f6fa6fe87a03dab99e9c6622fd42` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | 不一致,阻塞主控切換 | +| `main` | `631fc220c3f50a2b977b9e07cbd4f99c0fccf8d8` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | 不一致,阻塞主控切換 | | `release/v1.0` | `d15fb7d9f4bac86873d5c16b9c17c527b8f38bef` | `d15fb7d9f4bac86873d5c16b9c17c527b8f38bef` | 一致 | | `dev` | `25889d4b8edcb83b6ec707c5eef3c21ae5d432b0` | 無 | GitHub 缺分支 | | `drift/adopt-*` | 多條 | 無 | GitHub 缺分支 | @@ -199,7 +201,7 @@ GitHub target repo-by-repo approval package 已建立於 `docs/security/GITHUB-T "branch_count_github": 2, "tag_count_gitea": 2, "tag_count_github": 0, - "latest_sha_gitea": "0bc187877884f6fa6fe87a03dab99e9c6622fd42", + "latest_sha_gitea": "631fc220c3f50a2b977b9e07cbd4f99c0fccf8d8", "latest_sha_github": "202071f7a8724d5e8c29de441c3f380575a0ea94", "workflows_mapped": false, "webhooks_mapped": false, diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md b/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md index a48d0c26..f8e069f0 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md @@ -11,7 +11,7 @@ ## 0. 核心結論 -目前 Security Supply Chain 已有 14 個主要契約可交給 AwoooP 消費。Manifest 的用途是把分散的 schema、snapshot、人讀文件、允許動作與禁止動作收成一份入口,避免不同 Session 各自解讀。 +目前 Security Supply Chain 已有 15 個主要契約可交給 AwoooP 消費。Manifest 的用途是把分散的 schema、snapshot、人讀文件、允許動作與禁止動作收成一份入口,避免不同 Session 各自解讀。 初期預設仍是 `mirror_only`。Manifest 不授權 runtime enforcement、不授權 GitHub/Gitea 主控切換、不授權 repo 建立或 refs sync。 @@ -30,6 +30,7 @@ | `github_target_repo_approval_package_v1` | approval-only | 逐 repo approval queue draft | `github-target-repo-approval-package.snapshot.json` | | `source_control_approval_board_v1` | approval-only | 逐 repo owner / visibility / canonical / refs 決策 board | `source-control-approval-board.snapshot.json` | | `source_control_reconcile_plan_v1` | approval-only | refs-blocked repo 的 draft reconcile plan | `source-control-reconcile-plan.snapshot.json` | +| `source_control_ref_detail_diff_v1` | mirror-only | refs-blocked repo 的 branch/tag 明細 diff | `source-control-ref-detail-diff.snapshot.json` | | `local_repo_canonical_probe_v1` | mirror-only | momo/ewoooc lineage evidence | `local-repo-canonical-ewoooc-momo.snapshot.json` | | `git_remote_refs_probe_v1` | mirror-only | 110 / GitHub remote refs readiness | `bitan-tsenyang`、`wooo-infra-config` | | `approval_required_event_v1` | approval-only | 高風險 / 敏感邊界 approval | `gitea-readonly-inventory-approval.snapshot.json` | diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index dbb3ae07..86811a25 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -4,7 +4,7 @@ |------|------| | 日期 | 2026-05-12 | | 狀態 | S0/S1 read-only evidence 建置中 | -| 本階段完成 | Security Supply Chain contract manifest + Source Control Approval Board + Draft Reconcile Plan | +| 本階段完成 | Security Supply Chain contract manifest + Source Control Approval Board + Draft Reconcile Plan + Ref Detail Diff | | 原則 | 低摩擦分階段;文件、schema、read-only evidence 優先;不做 runtime enforcement、不切 primary | ## 0. 本階段完成後整體進度 @@ -17,8 +17,9 @@ | S1.1 GitHub target 決策 | 完成草案 | 8 個 target 候選,7 個需人工批准;3 個 `not_found_or_private` 不得自動建立 | owner / visibility / canonical approval | | S1.2 GitHub target 逐 repo approval | 完成草案 | 7 個 approval-required targets 已拆成逐 repo pending package,並彙整成 8-item approval board | 低摩擦逐項批准 | | S1.2a refs reconcile plan | 完成草案 | `awoooi`、`clawbot-v5`、`wooo-aiops` 已產生 draft plan;狀態仍為 `draft_blocked` | authenticated inventory + branch/tag diff + single-repo approval | +| S1.2b branch/tag detail diff | 完成草案 | 3 個 refs-blocked mapped repos 已完成 branch/tag 明細 diff;已忽略本 PR 分支避免 evidence 自我污染 | 人工判定真相來源與 deprecated refs | | S1.3 低摩擦 rollout policy | 完成草案 | observe-first / mirror-only matrix 已建立 | AwoooP read-only policy 消費 | -| S1.4 Contract manifest | 完成草案 | 14 個主要 contract 已集中成 manifest | AwoooP mirror-only contract registry | +| S1.4 Contract manifest | 完成草案 | 15 個主要 contract 已集中成 manifest | AwoooP mirror-only contract registry | | S2 AwoooP mirror-only | 可交接 | `AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md` 已列出可消費事件與禁止動作 | AwoooP 主線建立只讀入口 | | S3 approval gate | 未開始 | 已定義哪些動作要進 approval | 不得繞過人工批准 | | S4 migration execution | 未開始 | GitHub primary 長期方向已確認,但 refs / tags / workflow / secret 名稱尚未全量驗證 | SHA/tag/workflow parity 與 rollback ADR | @@ -45,6 +46,8 @@ | Source Control approval board JSON | `docs/security/source-control-approval-board.snapshot.json` | | Source Control draft reconcile plan | `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` | | Source Control draft reconcile plan JSON | `docs/security/source-control-reconcile-plan.snapshot.json` | +| Source Control branch/tag detail diff | `docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md` | +| Source Control branch/tag detail diff JSON | `docs/security/source-control-ref-detail-diff.snapshot.json` | | 低摩擦 rollout policy | `docs/security/SECURITY-LOW-FRICTION-ROLLOUT-POLICY.md` | | 低摩擦 rollout policy JSON | `docs/security/security-rollout-policy.snapshot.json` | | Security Supply Chain contract manifest | `docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md` | @@ -71,7 +74,7 @@ 1. 等待 Gitea read-only inventory approval 被批准後,再用只讀 token 或管理匯出補 private/internal server-side 全量 repo list。 2. 依 `SOURCE-CONTROL-APPROVAL-BOARD.md` 對 7 個 `approval_required=true` 的 GitHub target 做 owner / visibility / canonical 決策。 -3. 依 `SOURCE-CONTROL-RECONCILE-PLAN.md` 對 `awoooi`、`clawbot-v5`、`wooo-aiops` 補 branch-by-branch / tag-by-tag diff;仍不得 push refs。 +3. 依 `SOURCE-CONTROL-REF-DETAIL-DIFF.md` 對 `awoooi`、`clawbot-v5`、`wooo-aiops` 標記真相來源候選與 deprecated refs;仍不得 push refs。 4. 對 `ewoooc` / `momo-pro-system` 完成 server-side canonical 判定。 5. AwoooP 主線只建立 mirror-only / read-only policy 入口,不新增執行按鈕。 6. AwoooP 主線消費 `security_rollout_policy_v1` 時,只做 read-only policy,不做 runtime blocking。 diff --git a/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md b/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md index 0014ca0a..a6a31d5d 100644 --- a/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md +++ b/docs/security/SOURCE-CONTROL-MIGRATION-MATRIX.md @@ -11,6 +11,7 @@ | GitHub target 決策 | `docs/security/GITHUB-TARGET-VISIBILITY-DECISION-TABLE.md` | | GitHub target repo approval | `docs/security/GITHUB-TARGET-REPO-APPROVAL-PACKAGE.md` | | Source Control draft reconcile plan | `docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md` | +| Source Control branch/tag detail diff | `docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md` | | 原則 | 只做盤點與分類,不做同步、不刪除、不切 primary | ## 0. 核心結論 @@ -135,7 +136,7 @@ Repo-by-repo approval package 已建立,7 個 approval-required targets 皆為 | Repo | Status | Gitea branches | GitHub branches | Gitea tags | GitHub tags | Gitea main | GitHub main | Evidence | |------|--------|----------------|-----------------|------------|-------------|------------|-------------|----------| -| `wooo/awoooi` -> `owenhytsai/awoooi` | `blocked` | `117` | `2` | `2` | `0` | `0bc187877884f6fa6fe87a03dab99e9c6622fd42` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | `docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md` | +| `wooo/awoooi` -> `owenhytsai/awoooi` | `blocked` | `117` | `2` | `2` | `0` | `631fc220c3f50a2b977b9e07cbd4f99c0fccf8d8` | `202071f7a8724d5e8c29de441c3f380575a0ea94` | `docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md` | | `wooo/clawbot-v5` -> `owenhytsai/clawbot-v5` | `blocked` | `1` | `1` | `1` | `0` | `22074fbe4d6ec6c11c86f76139eea55756d1d160` | `7a769de46450087f9d6a8ef0d2ac23ed15565d2c` | `docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md` | | `wooo/wooo-aiops` -> `owenhytsai/wooo-aiops` | `blocked` | `2` | `3` | `0` | `19` | `507384a2e1943f4183942bf17d7b52e223067853` | `7c7aa109d93da6d75d687d6ee5131151afee37e8` | `docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md` | diff --git a/docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md b/docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md index 5d4c645e..a213461a 100644 --- a/docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md +++ b/docs/security/SOURCE-CONTROL-RECONCILE-PLAN.md @@ -2,7 +2,7 @@ | 項目 | 內容 | |------|------| -| 日期 | 2026-05-12 | +| 日期 | 2026-05-13 | | 狀態 | `draft_blocked` | | 預設模式 | `plan_only` | | inventory gate | `blocked` | @@ -17,7 +17,7 @@ | Repo | Risk | Gitea branches | GitHub branches | Gitea tags | GitHub tags | Gitea main | GitHub main | |------|------|----------------|-----------------|------------|-------------|------------|-------------| -| `wooo/awoooi -> owenhytsai/awoooi` | `HIGH` | `117` | `2` | `2` | `0` | `0bc18787` | `202071f7` | +| `wooo/awoooi -> owenhytsai/awoooi` | `HIGH` | `117` | `2` | `2` | `0` | `631fc220` | `202071f7` | | `wooo/clawbot-v5 -> owenhytsai/clawbot-v5` | `MEDIUM` | `1` | `1` | `1` | `0` | `22074fbe` | `7a769de4` | | `wooo/wooo-aiops -> owenhytsai/wooo-aiops` | `MEDIUM` | `2` | `3` | `0` | `19` | `507384a2` | `7c7aa109` | @@ -26,7 +26,7 @@ ### wooo/awoooi -> owenhytsai/awoooi - 狀態:`blocked` -- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 +- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊 - 允許現在做: - 更新 read-only evidence - 更新 approval board @@ -62,7 +62,7 @@ ### wooo/clawbot-v5 -> owenhytsai/clawbot-v5 - 狀態:`blocked` -- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 +- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊 - 允許現在做: - 更新 read-only evidence - 更新 approval board @@ -97,7 +97,7 @@ ### wooo/wooo-aiops -> owenhytsai/wooo-aiops - 狀態:`blocked` -- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致 +- 阻塞原因:branches 尚未完全對齊;tags 尚未完全對齊 - 允許現在做: - 更新 read-only evidence - 更新 approval board diff --git a/docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md b/docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md new file mode 100644 index 00000000..ea087229 --- /dev/null +++ b/docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md @@ -0,0 +1,219 @@ +# Source Control Branch / Tag Detail Diff + +| 項目 | 內容 | +|------|------| +| 日期 | 2026-05-13 | +| 狀態 | `draft_blocked` | +| 預設模式 | `read_only_diff` | +| ignored branches | `codex/security-supply-chain-contracts-20260512` | +| repo count | 3 | + +## 0. 核心結論 + +本檔是 read-only refs 明細 diff,不是同步腳本。任何 refs sync、GitHub primary 切換、repo 建立或 visibility 修改都仍需單一 repo 人工批准。 + +## 1. 摘要 + +| Repo | Status | Branch only Gitea | Branch only GitHub | Branch SHA diff | Tag only Gitea | Tag only GitHub | Tag SHA diff | +|------|--------|-------------------|--------------------|-----------------|----------------|-----------------|--------------| +| `wooo/awoooi -> owenhytsai/awoooi` | `blocked` | `115` | `0` | `1` | `2` | `0` | `0` | +| `wooo/clawbot-v5 -> owenhytsai/clawbot-v5` | `blocked` | `0` | `0` | `1` | `1` | `0` | `0` | +| `wooo/wooo-aiops -> owenhytsai/wooo-aiops` | `blocked` | `0` | `1` | `1` | `0` | `19` | `0` | + +## 2. Repo 明細 + +### wooo/awoooi -> owenhytsai/awoooi + +- Status:`blocked` +- Blocking reason:branches 尚未完全對齊;tags 尚未完全對齊 +- Gitea URL:`http://192.168.0.110:3001/wooo/awoooi.git` +- GitHub URL:`https://github.com/owenhytsai/awoooi.git` + +#### Branch SHA 不一致 + +- `main`:Gitea `5294f071` / GitHub `202071f7` + +#### Branch 只在 Gitea + +- `dev` @ `25889d4b` +- `drift/adopt-00e93ec6-20260507202835` @ `d356cd32` +- `drift/adopt-018dfeb6-20260510201057` @ `d356cd32` +- `drift/adopt-036399ae-20260512101936` @ `d356cd32` +- `drift/adopt-036399ae-20260512113011` @ `d356cd32` +- `drift/adopt-03ab23eb-20260511190611` @ `d356cd32` +- `drift/adopt-04532fd3-20260509104706` @ `d356cd32` +- `drift/adopt-094107c8-20260511171658` @ `d356cd32` +- `drift/adopt-0b67aa5a-20260508132246` @ `d356cd32` +- `drift/adopt-0d55cf70-20260512152046` @ `abdab853` +- `drift/adopt-0d7e2731-20260422111000` @ `75b7d338` +- `drift/adopt-11d02e8c-20260505000302` @ `40badc42` +- `drift/adopt-18d2841e-20260506203612` @ `b2f0db07` +- `drift/adopt-1941d569-20260509001722` @ `d356cd32` +- `drift/adopt-1b510a59-20260508071355` @ `d356cd32` +- `drift/adopt-1d45b689-20260512101956` @ `d356cd32` +- `drift/adopt-1ffaa1de-20260509191538` @ `d356cd32` +- `drift/adopt-22d3c494-20260504010134` @ `035fe20e` +- `drift/adopt-256b7b16-20260507082410` @ `c52ebfc0` +- `drift/adopt-2582b60d-20260509160733` @ `d356cd32` +- `drift/adopt-2b26cfe4-20260509104618` @ `d356cd32` +- `drift/adopt-3254209e-20260505150522` @ `d08d1e49` +- `drift/adopt-3336b941-20260511111108` @ `d356cd32` +- `drift/adopt-34bf86fd-20260421010616` @ `3323a905` +- `drift/adopt-35e8371b-20260508222338` @ `d356cd32` +- `drift/adopt-378290ab-20260510201203` @ `d356cd32` +- `drift/adopt-381202c7-20260507171310` @ `afb5f955` +- `drift/adopt-38e32fb9-20260506171242` @ `76aaaf48` +- `drift/adopt-39383806-20260502230445` @ `68e18238` +- `drift/adopt-3da139d9-20260503161425` @ `e45b055e` +- `drift/adopt-44860c3d-20260510171817` @ `d356cd32` +- `drift/adopt-46e77bf6-20260506235709` @ `012cd27b` +- `drift/adopt-49920526-20260511111126` @ `d356cd32` +- `drift/adopt-4b40fc0d-20260429080302` @ `20009cdd` +- `drift/adopt-4d8f3bef-20260508212335` @ `d356cd32` +- `drift/adopt-4e188293-20260504100431` @ `035fe20e` +- `drift/adopt-4e5775dc-20260510233800` @ `d356cd32` +- `drift/adopt-50d442fb-20260510201215` @ `d356cd32` +- `drift/adopt-52fb18e5-20260512000119` @ `d356cd32` +- `drift/adopt-55aaa7de-20260512151737` @ `abdab853` +- `drift/adopt-567cd7c0-20260511132950` @ `d356cd32` +- `drift/adopt-5bc6628e-20260512113026` @ `d356cd32` +- `drift/adopt-60b714c6-20260504131233` @ `b4055c59` +- `drift/adopt-60c4401c-20260510201228` @ `d356cd32` +- `drift/adopt-629f415a-20260504110028` @ `f7e5fc77` +- `drift/adopt-636e7a0a-20260508150402` @ `d356cd32` +- `drift/adopt-6384c896-20260510201239` @ `d356cd32` +- `drift/adopt-67e03b24-20260510194653` @ `d356cd32` +- `drift/adopt-6857ca25-20260508105853` @ `d356cd32` +- `drift/adopt-699b4785-20260506160255` @ `927c2a75` +- `drift/adopt-6beac0f6-20260425192742` @ `4a8c3ca5` +- `drift/adopt-6c17ef6d-20260509191548` @ `d356cd32` +- `drift/adopt-6d4dcbb6-20260421230348` @ `49e46595` +- `drift/adopt-6e7ec67f-20260504120519` @ `b4055c59` +- `drift/adopt-6fae380e-20260422102753` @ `75b7d338` +- `drift/adopt-6fb9a504-20260507202914` @ `d356cd32` +- `drift/adopt-7018530f-20260512113057` @ `d356cd32` +- `drift/adopt-732ed7e5-20260511150635` @ `d356cd32` +- `drift/adopt-7517bc64-20260509125450` @ `d356cd32` +- `drift/adopt-7551fb42-20260424144550` @ `9793f7f5` +- 另有 `55` 筆,完整清單見 JSON snapshot。 + +#### Branch 只在 GitHub + +無。 + +#### Tag 只在 Gitea + +- `v7.2.0` @ `898145d6` +- `v7.3.0` @ `4b8be326` + +#### Tag 只在 GitHub + +無。 + +#### 仍然禁止 + +- fetch +- push refs +- force push +- delete refs +- create GitHub repo +- change repo visibility +- switch GitHub primary +- disable Gitea +- move secret values + +### wooo/clawbot-v5 -> owenhytsai/clawbot-v5 + +- Status:`blocked` +- Blocking reason:branches 尚未完全對齊;tags 尚未完全對齊 +- Gitea URL:`http://192.168.0.110:3001/wooo/clawbot-v5.git` +- GitHub URL:`https://github.com/owenhytsai/clawbot-v5.git` + +#### Branch SHA 不一致 + +- `main`:Gitea `22074fbe` / GitHub `7a769de4` + +#### Branch 只在 Gitea + +無。 + +#### Branch 只在 GitHub + +無。 + +#### Tag 只在 Gitea + +- `v5.5-sprint1` @ `2b126871` + +#### Tag 只在 GitHub + +無。 + +#### 仍然禁止 + +- fetch +- push refs +- force push +- delete refs +- create GitHub repo +- change repo visibility +- switch GitHub primary +- disable Gitea +- move secret values + +### wooo/wooo-aiops -> owenhytsai/wooo-aiops + +- Status:`blocked` +- Blocking reason:branches 尚未完全對齊;tags 尚未完全對齊 +- Gitea URL:`http://192.168.0.110:3001/wooo/wooo-aiops.git` +- GitHub URL:`https://github.com/owenhytsai/wooo-aiops.git` + +#### Branch SHA 不一致 + +- `main`:Gitea `507384a2` / GitHub `7c7aa109` + +#### Branch 只在 Gitea + +無。 + +#### Branch 只在 GitHub + +- `refactor/phase-9.3` @ `7261f94b` + +#### Tag 只在 Gitea + +無。 + +#### Tag 只在 GitHub + +- `uat-20260316-1ec245f` @ `1ec245fd` +- `uat-20260316-2d54a43` @ `2d54a43b` +- `uat-20260316-3e8df86` @ `3e8df861` +- `uat-20260316-95002f5` @ `95002f50` +- `uat-20260316-9ebea63` @ `9ebea633` +- `uat-20260316-a851c80` @ `a851c809` +- `uat-20260316-e2ee674` @ `e2ee6746` +- `uat-20260316-fa48be2` @ `fa48be21` +- `uat-20260317-f768051` @ `f7680510` +- `uat-20260318-20dde91` @ `20dde91a` +- `uat-20260318-4025dde` @ `4025dde5` +- `uat-20260318-5bfa151` @ `5bfa1514` +- `uat-20260318-663b590` @ `663b590d` +- `uat-20260318-78a3757` @ `78a3757c` +- `uat-20260318-7c6f666` @ `7c6f6665` +- `uat-20260318-c4d5669` @ `c4d56690` +- `uat-20260318-cf42fd4` @ `cf42fd4e` +- `uat-20260319-7c7aa10` @ `7c7aa109` +- `uat-20260319-ce5c72b` @ `ce5c72b5` + +#### 仍然禁止 + +- fetch +- push refs +- force push +- delete refs +- create GitHub repo +- change repo visibility +- switch GitHub primary +- disable Gitea +- move secret values diff --git a/docs/security/security-supply-chain-contract-manifest.snapshot.json b/docs/security/security-supply-chain-contract-manifest.snapshot.json index 214f1eb2..bf82f0aa 100644 --- a/docs/security/security-supply-chain-contract-manifest.snapshot.json +++ b/docs/security/security-supply-chain-contract-manifest.snapshot.json @@ -2,7 +2,7 @@ "schema_version": "security_supply_chain_contract_manifest_v1", "status": "draft", "default_enforcement_level": "mirror_only", - "contract_count": 14, + "contract_count": 15, "contracts": [ { "contract": "security_rollout_policy_v1", @@ -157,6 +157,26 @@ ], "notes": "只針對 3 個 refs-blocked mapped repos 產生 draft plan;inventory gate 仍 blocked,不可執行。" }, + { + "contract": "source_control_ref_detail_diff_v1", + "schema_path": "docs/schemas/source_control_ref_detail_diff_v1.schema.json", + "snapshot_paths": ["docs/security/source-control-ref-detail-diff.snapshot.json"], + "human_docs": ["docs/security/SOURCE-CONTROL-REF-DETAIL-DIFF.md"], + "consumer": "AwoooP migration reviewer / PR reviewer", + "consumption_mode": "mirror_only", + "allowed_actions": [ + "mirror_branch_tag_diff", + "display_ref_counts", + "support_single_repo_reconcile_review" + ], + "forbidden_actions": [ + "fetch_refs", + "push_refs", + "delete_refs", + "switch_github_primary" + ], + "notes": "只保存 branch/tag 明細 diff;忽略本 PR 分支避免 evidence 自我污染。" + }, { "contract": "local_repo_canonical_probe_v1", "schema_path": "docs/schemas/local_repo_canonical_probe_v1.schema.json", diff --git a/docs/security/source-control-reconcile-plan.snapshot.json b/docs/security/source-control-reconcile-plan.snapshot.json index ebb5b136..0391ef4c 100644 --- a/docs/security/source-control-reconcile-plan.snapshot.json +++ b/docs/security/source-control-reconcile-plan.snapshot.json @@ -1,7 +1,7 @@ { "schema_version": "source_control_reconcile_plan_v1", "status": "draft_blocked", - "date": "2026-05-12", + "date": "2026-05-13", "default_mode": "plan_only", "inventory_gate": { "status": "blocked", @@ -28,9 +28,9 @@ "github_branch_count": 2, "gitea_tag_count": 2, "github_tag_count": 0, - "gitea_main_sha": "0bc187877884f6fa6fe87a03dab99e9c6622fd42", + "gitea_main_sha": "631fc22090ff6b6a26582890b2de0498d118895f", "github_main_sha": "202071f7a8724d5e8c29de441c3f380575a0ea94", - "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致" + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊" }, "proposed_plan_steps": [ "先確認目前 production deploy 真相來源與 deploy marker 流程,避免主控切換影響發版。", @@ -82,7 +82,7 @@ "github_tag_count": 0, "gitea_main_sha": "22074fbe4d6ec6c11c86f76139eea55756d1d160", "github_main_sha": "7a769de46450087f9d6a8ef0d2ac23ed15565d2c", - "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致" + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊" }, "proposed_plan_steps": [ "針對 `wooo/clawbot-v5` 與 `owenhytsai/clawbot-v5` 產生 branch-by-branch diff 表。", @@ -133,7 +133,7 @@ "github_tag_count": 19, "gitea_main_sha": "507384a2e1943f4183942bf17d7b52e223067853", "github_main_sha": "7c7aa109d93da6d75d687d6ee5131151afee37e8", - "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊;main SHA 不一致" + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊" }, "proposed_plan_steps": [ "針對 `wooo/wooo-aiops` 與 `owenhytsai/wooo-aiops` 產生 branch-by-branch diff 表。", diff --git a/docs/security/source-control-ref-detail-diff.snapshot.json b/docs/security/source-control-ref-detail-diff.snapshot.json new file mode 100644 index 00000000..02f71bfc --- /dev/null +++ b/docs/security/source-control-ref-detail-diff.snapshot.json @@ -0,0 +1,738 @@ +{ + "schema_version": "source_control_ref_detail_diff_v1", + "status": "draft_blocked", + "date": "2026-05-13", + "default_mode": "read_only_diff", + "ignored_branches": [ + "codex/security-supply-chain-contracts-20260512" + ], + "repo_count": 3, + "repos": [ + { + "repo_key": "awoooi", + "repo_path": "/private/tmp/awoooi-security-supplychain-push", + "gitea_remote": "gitea", + "github_remote": "origin", + "gitea_url_redacted": "http://192.168.0.110:3001/wooo/awoooi.git", + "github_url_redacted": "https://github.com/owenhytsai/awoooi.git", + "gitea_repo": "wooo/awoooi", + "github_repo": "owenhytsai/awoooi", + "status": "blocked", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊", + "branch_diff": { + "gitea_count": 117, + "github_count": 2, + "only_gitea_count": 115, + "only_github_count": 0, + "sha_mismatch_count": 1, + "matching_count": 1, + "only_gitea": [ + { + "name": "dev", + "sha": "25889d4b8edcb83b6ec707c5eef3c21ae5d432b0" + }, + { + "name": "drift/adopt-00e93ec6-20260507202835", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-018dfeb6-20260510201057", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-036399ae-20260512101936", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-036399ae-20260512113011", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-03ab23eb-20260511190611", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-04532fd3-20260509104706", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-094107c8-20260511171658", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-0b67aa5a-20260508132246", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-0d55cf70-20260512152046", + "sha": "abdab85362c577bd734ab1c7796bb6f33f070eca" + }, + { + "name": "drift/adopt-0d7e2731-20260422111000", + "sha": "75b7d338e0003311c63f5fd81f9c81e2ebe41f7b" + }, + { + "name": "drift/adopt-11d02e8c-20260505000302", + "sha": "40badc42cf81b1f8193bee2221d46ec5b52de6dd" + }, + { + "name": "drift/adopt-18d2841e-20260506203612", + "sha": "b2f0db07178c7469ccaa1d1644fcdadaf044bfcb" + }, + { + "name": "drift/adopt-1941d569-20260509001722", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-1b510a59-20260508071355", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-1d45b689-20260512101956", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-1ffaa1de-20260509191538", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-22d3c494-20260504010134", + "sha": "035fe20e4d5b7564a68c22d82c56dca266009bd7" + }, + { + "name": "drift/adopt-256b7b16-20260507082410", + "sha": "c52ebfc0423bea993fdc21dd205f78d2262a660e" + }, + { + "name": "drift/adopt-2582b60d-20260509160733", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-2b26cfe4-20260509104618", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-3254209e-20260505150522", + "sha": "d08d1e49518fcf57abce7bda10554adbcba395cc" + }, + { + "name": "drift/adopt-3336b941-20260511111108", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-34bf86fd-20260421010616", + "sha": "3323a9052c95a81e645fbf3c78c87ee9e2b16d6b" + }, + { + "name": "drift/adopt-35e8371b-20260508222338", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-378290ab-20260510201203", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-381202c7-20260507171310", + "sha": "afb5f9556e74eeced130cf0e67029d9c85b48c1a" + }, + { + "name": "drift/adopt-38e32fb9-20260506171242", + "sha": "76aaaf480c08beb9d5d4c880a8fe850fd84c5ce0" + }, + { + "name": "drift/adopt-39383806-20260502230445", + "sha": "68e182381f37dc45d99744c24a518d47c859540f" + }, + { + "name": "drift/adopt-3da139d9-20260503161425", + "sha": "e45b055e0e3ea25afbbb45c4f477a632a8945c54" + }, + { + "name": "drift/adopt-44860c3d-20260510171817", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-46e77bf6-20260506235709", + "sha": "012cd27b4a27065e40a33728f8095e933114c27b" + }, + { + "name": "drift/adopt-49920526-20260511111126", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-4b40fc0d-20260429080302", + "sha": "20009cddcf392fde3423680069b2322604a31cf5" + }, + { + "name": "drift/adopt-4d8f3bef-20260508212335", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-4e188293-20260504100431", + "sha": "035fe20e4d5b7564a68c22d82c56dca266009bd7" + }, + { + "name": "drift/adopt-4e5775dc-20260510233800", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-50d442fb-20260510201215", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-52fb18e5-20260512000119", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-55aaa7de-20260512151737", + "sha": "abdab85362c577bd734ab1c7796bb6f33f070eca" + }, + { + "name": "drift/adopt-567cd7c0-20260511132950", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-5bc6628e-20260512113026", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-60b714c6-20260504131233", + "sha": "b4055c5915bfcf8f6a37f9656be291ebe7fc526c" + }, + { + "name": "drift/adopt-60c4401c-20260510201228", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-629f415a-20260504110028", + "sha": "f7e5fc772e6f25d7f9a69374a2bdd9febccccf87" + }, + { + "name": "drift/adopt-636e7a0a-20260508150402", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-6384c896-20260510201239", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-67e03b24-20260510194653", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-6857ca25-20260508105853", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-699b4785-20260506160255", + "sha": "927c2a758dd748d4cac084dc7850059952b02353" + }, + { + "name": "drift/adopt-6beac0f6-20260425192742", + "sha": "4a8c3ca5c4a5c80f4d75dac4acb54e74dbb71bfe" + }, + { + "name": "drift/adopt-6c17ef6d-20260509191548", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-6d4dcbb6-20260421230348", + "sha": "49e465954cbe8f552b440b78c76b235d5f8ff14c" + }, + { + "name": "drift/adopt-6e7ec67f-20260504120519", + "sha": "b4055c5915bfcf8f6a37f9656be291ebe7fc526c" + }, + { + "name": "drift/adopt-6fae380e-20260422102753", + "sha": "75b7d338e0003311c63f5fd81f9c81e2ebe41f7b" + }, + { + "name": "drift/adopt-6fb9a504-20260507202914", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7018530f-20260512113057", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-732ed7e5-20260511150635", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7517bc64-20260509125450", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7551fb42-20260424144550", + "sha": "9793f7f5edaff9c9ac199ab34053831534544635" + }, + { + "name": "drift/adopt-75f11639-20260508212322", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-767d3213-20260512000035", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-777008e2-20260510233821", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7912a251-20260508181656", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7dddec5a-20260508230651", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7e701601-20260508140947", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7f3a03db-20260512112944", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-7fbb45c2-20260506081629", + "sha": "df5e6c66263884eb9d6f510853e1f5144de3b9ec" + }, + { + "name": "drift/adopt-8293df04-20260510201332", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-8360b6c9-20260507082544", + "sha": "c52ebfc0423bea993fdc21dd205f78d2262a660e" + }, + { + "name": "drift/adopt-84e9fc38-20260507082618", + "sha": "c52ebfc0423bea993fdc21dd205f78d2262a660e" + }, + { + "name": "drift/adopt-8eb798d0-20260503020341", + "sha": "0f009d94590d0545bc681682b2ca5b1980539ce7" + }, + { + "name": "drift/adopt-8f0c2384-20260509132348", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-8f29634d-20260510223341", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-8f8e1f11-20260507082638", + "sha": "c52ebfc0423bea993fdc21dd205f78d2262a660e" + }, + { + "name": "drift/adopt-92789f1e-20260512181538", + "sha": "a18e2f9c3f403050d0fb7476bf6fdb860225731a" + }, + { + "name": "drift/adopt-94ed08b7-20260510201336", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-95e3318c-20260510194637", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-96153b22-20260509171850", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-97554a3d-20260507130220", + "sha": "c88d82f2acec52032de658d732ef314d9dd3331d" + }, + { + "name": "drift/adopt-978af647-20260507021159", + "sha": "3f69e03fcb915514aabf25263b5004b7de5912dc" + }, + { + "name": "drift/adopt-9959a942-20260423232150", + "sha": "9793f7f5edaff9c9ac199ab34053831534544635" + }, + { + "name": "drift/adopt-99d4bd24-20260512000050", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-9d6a34ed-20260511111155", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-9e72bfe1-20260512160042", + "sha": "6b02f49fc64f2ba6d55166d0517894ab2f6253a3" + }, + { + "name": "drift/adopt-a72c3cd5-20260502225149", + "sha": "68e182381f37dc45d99744c24a518d47c859540f" + }, + { + "name": "drift/adopt-a979c86e-20260506140947", + "sha": "578bf3bc7ce495a80752e7124c005b64c6943ead" + }, + { + "name": "drift/adopt-aadf7da2-20260512101903", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-ac03f7a1-20260509104658", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-af72d85c-20260425140126", + "sha": "4a8c3ca5c4a5c80f4d75dac4acb54e74dbb71bfe" + }, + { + "name": "drift/adopt-b3dbf399-20260512113118", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-b75fe78c-20260508105822", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-b8cf88fc-20260512113132", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-b9320b66-20260511001336", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-bd499c90-20260506191626", + "sha": "d90414ddfa863443927d663a13140bf7e089d0c4" + }, + { + "name": "drift/adopt-c6231f70-20260512181458", + "sha": "a18e2f9c3f403050d0fb7476bf6fdb860225731a" + }, + { + "name": "drift/adopt-c68af767-20260512101909", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-c9eebf3d-20260502210402", + "sha": "68e182381f37dc45d99744c24a518d47c859540f" + }, + { + "name": "drift/adopt-cd357cc7-20260507130241", + "sha": "c88d82f2acec52032de658d732ef314d9dd3331d" + }, + { + "name": "drift/adopt-cdcb477f-20260508212328", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-d01dab15-20260508105747", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-d55fd9d6-20260508132237", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-de6bc318-20260510201129", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-e31f120f-20260511120033", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-e46ff8f0-20260511111140", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-e56ef1bf-20260507113021", + "sha": "c88d82f2acec52032de658d732ef314d9dd3331d" + }, + { + "name": "drift/adopt-e75a0bc0-20260510201400", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-ed7621f5-20260504001046", + "sha": "035fe20e4d5b7564a68c22d82c56dca266009bd7" + }, + { + "name": "drift/adopt-ee276030-20260422124713", + "sha": "9793f7f5edaff9c9ac199ab34053831534544635" + }, + { + "name": "drift/adopt-ef9d03b0-20260505130426", + "sha": "0ebd0d8a920e2217785e841b92c66b9fc8ad1ccc" + }, + { + "name": "drift/adopt-efca2ad0-20260510201505", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-f0c35f7a-20260508110913", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-f40987d5-20260509125434", + "sha": "d356cd32fcfff2f9c9dbcc489402997153f8d54b" + }, + { + "name": "drift/adopt-f6372f90-20260507082708", + "sha": "c52ebfc0423bea993fdc21dd205f78d2262a660e" + }, + { + "name": "drift/adopt-ffb724f1-20260503190044", + "sha": "e45b055e0e3ea25afbbb45c4f477a632a8945c54" + } + ], + "only_github": [], + "sha_mismatch": [ + { + "name": "main", + "gitea_sha": "5294f0712f1a3370d0155c0d88e5d10c6ec0250e", + "github_sha": "202071f7a8724d5e8c29de441c3f380575a0ea94" + } + ], + "matching": [ + { + "name": "release/v1.0", + "sha": "d15fb7d9f4bac86873d5c16b9c17c527b8f38bef" + } + ] + }, + "tag_diff": { + "gitea_count": 2, + "github_count": 0, + "only_gitea_count": 2, + "only_github_count": 0, + "sha_mismatch_count": 0, + "matching_count": 0, + "only_gitea": [ + { + "name": "v7.2.0", + "sha": "898145d68e4e78db4876f8b8e5ae1892986525ff" + }, + { + "name": "v7.3.0", + "sha": "4b8be32610eaeaa5cff9b94d6d0312d90714b6c7" + } + ], + "only_github": [], + "sha_mismatch": [], + "matching": [] + }, + "still_forbidden": [ + "fetch", + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values" + ] + }, + { + "repo_key": "clawbot-v5", + "repo_path": "/Users/ogt/clawbot-v5", + "gitea_remote": "gitea", + "github_remote": "origin", + "gitea_url_redacted": "http://192.168.0.110:3001/wooo/clawbot-v5.git", + "github_url_redacted": "https://github.com/owenhytsai/clawbot-v5.git", + "gitea_repo": "wooo/clawbot-v5", + "github_repo": "owenhytsai/clawbot-v5", + "status": "blocked", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊", + "branch_diff": { + "gitea_count": 1, + "github_count": 1, + "only_gitea_count": 0, + "only_github_count": 0, + "sha_mismatch_count": 1, + "matching_count": 0, + "only_gitea": [], + "only_github": [], + "sha_mismatch": [ + { + "name": "main", + "gitea_sha": "22074fbe4d6ec6c11c86f76139eea55756d1d160", + "github_sha": "7a769de46450087f9d6a8ef0d2ac23ed15565d2c" + } + ], + "matching": [] + }, + "tag_diff": { + "gitea_count": 1, + "github_count": 0, + "only_gitea_count": 1, + "only_github_count": 0, + "sha_mismatch_count": 0, + "matching_count": 0, + "only_gitea": [ + { + "name": "v5.5-sprint1", + "sha": "2b12687133db6253503cf0d2c12aff67aa92b899" + } + ], + "only_github": [], + "sha_mismatch": [], + "matching": [] + }, + "still_forbidden": [ + "fetch", + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values" + ] + }, + { + "repo_key": "wooo-aiops", + "repo_path": "/Users/ogt/wooo-aiops", + "gitea_remote": "gitea", + "github_remote": "origin", + "gitea_url_redacted": "http://192.168.0.110:3001/wooo/wooo-aiops.git", + "github_url_redacted": "https://github.com/owenhytsai/wooo-aiops.git", + "gitea_repo": "wooo/wooo-aiops", + "github_repo": "owenhytsai/wooo-aiops", + "status": "blocked", + "blocking_reason": "branches 尚未完全對齊;tags 尚未完全對齊", + "branch_diff": { + "gitea_count": 2, + "github_count": 3, + "only_gitea_count": 0, + "only_github_count": 1, + "sha_mismatch_count": 1, + "matching_count": 1, + "only_gitea": [], + "only_github": [ + { + "name": "refactor/phase-9.3", + "sha": "7261f94b86346fb706eca729c5db844e65bd902c" + } + ], + "sha_mismatch": [ + { + "name": "main", + "gitea_sha": "507384a2e1943f4183942bf17d7b52e223067853", + "github_sha": "7c7aa109d93da6d75d687d6ee5131151afee37e8" + } + ], + "matching": [ + { + "name": "uat", + "sha": "1ec245fd4d90ed6db479b3fb0487545dfd632411" + } + ] + }, + "tag_diff": { + "gitea_count": 0, + "github_count": 19, + "only_gitea_count": 0, + "only_github_count": 19, + "sha_mismatch_count": 0, + "matching_count": 0, + "only_gitea": [], + "only_github": [ + { + "name": "uat-20260316-1ec245f", + "sha": "1ec245fd4d90ed6db479b3fb0487545dfd632411" + }, + { + "name": "uat-20260316-2d54a43", + "sha": "2d54a43b451ce39ab2df6080588f5361864b3a8d" + }, + { + "name": "uat-20260316-3e8df86", + "sha": "3e8df8615c483702cb4f60f184c2410f12306f1c" + }, + { + "name": "uat-20260316-95002f5", + "sha": "95002f50a16bab91fd9ccc2ebf578ce7d3cbed77" + }, + { + "name": "uat-20260316-9ebea63", + "sha": "9ebea633d2d5370634f90130a258e736ca329279" + }, + { + "name": "uat-20260316-a851c80", + "sha": "a851c80906c9bd933b2160bc6215609367dae57f" + }, + { + "name": "uat-20260316-e2ee674", + "sha": "e2ee6746da08f9071db8fa320c4c77249c8a31d3" + }, + { + "name": "uat-20260316-fa48be2", + "sha": "fa48be21dfab84842b5c4f59831f17187d4643ea" + }, + { + "name": "uat-20260317-f768051", + "sha": "f768051007085517d3e13dcd7cadedfb84bfbd50" + }, + { + "name": "uat-20260318-20dde91", + "sha": "20dde91a9fab4385d1b9327f8a07f518f6c5bdc6" + }, + { + "name": "uat-20260318-4025dde", + "sha": "4025dde5927ae22e5cae289191e2422ac02a1f62" + }, + { + "name": "uat-20260318-5bfa151", + "sha": "5bfa15148417ef89b135b0bd25c5bf8e4493a337" + }, + { + "name": "uat-20260318-663b590", + "sha": "663b590df4c3c6924abc9b9274042afe3a38a968" + }, + { + "name": "uat-20260318-78a3757", + "sha": "78a3757cf7edf454d5721185344c476c7842b0a1" + }, + { + "name": "uat-20260318-7c6f666", + "sha": "7c6f66655e1faa6024f8549a741bdb964f76fdb1" + }, + { + "name": "uat-20260318-c4d5669", + "sha": "c4d56690f7c480e6ad1ef96dfa5bf90e9b7cc84f" + }, + { + "name": "uat-20260318-cf42fd4", + "sha": "cf42fd4eee5b10fac7248dc70de26cfd619191ca" + }, + { + "name": "uat-20260319-7c7aa10", + "sha": "7c7aa109d93da6d75d687d6ee5131151afee37e8" + }, + { + "name": "uat-20260319-ce5c72b", + "sha": "ce5c72b54d6e6d8e96e983d9f80ebd834683a288" + } + ], + "sha_mismatch": [], + "matching": [] + }, + "still_forbidden": [ + "fetch", + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values" + ] + } + ] +} diff --git a/scripts/security/source-control-reconcile-plan.py b/scripts/security/source-control-reconcile-plan.py index 816119e7..fb598f7a 100644 --- a/scripts/security/source-control-reconcile-plan.py +++ b/scripts/security/source-control-reconcile-plan.py @@ -63,9 +63,40 @@ def proposed_steps(event: dict[str, Any]) -> list[str]: return steps +def detail_by_repo(path: str | None) -> dict[tuple[str, str], dict[str, Any]]: + if not path: + return {} + payload = load_json(Path(path)) + return { + (str(item.get("gitea_repo", "")), str(item.get("github_repo", ""))): item + for item in payload.get("repos", []) + if isinstance(item, dict) + } + + +def main_shas_from_detail(detail: dict[str, Any]) -> tuple[str, str]: + branch = detail.get("branch_diff", {}) + if not isinstance(branch, dict): + return "", "" + for item in branch.get("sha_mismatch", []): + if isinstance(item, dict) and item.get("name") == "main": + return str(item.get("gitea_sha", "")), str(item.get("github_sha", "")) + for key in ("matching", "only_gitea", "only_github"): + for item in branch.get(key, []): + if isinstance(item, dict) and item.get("name") == "main": + sha = str(item.get("sha", "")) + if key == "only_gitea": + return sha, "" + if key == "only_github": + return "", sha + return sha, sha + return "", "" + + 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)) + details = detail_by_repo(args.ref_detail_diff) inventory_ready = gitea_inventory.get("status") == "ok" inventory_gate_status = "ready" if inventory_ready else "blocked" @@ -79,6 +110,10 @@ def build_plan(args: argparse.Namespace) -> dict[str, Any]: for event in events: gitea_repo = str(event.get("gitea_repo", "")) github_repo = str(event.get("github_repo", "")) + detail = details.get((gitea_repo, github_repo), {}) + branch_detail = detail.get("branch_diff", {}) if isinstance(detail, dict) else {} + tag_detail = detail.get("tag_diff", {}) if isinstance(detail, dict) else {} + detail_gitea_main, detail_github_main = main_shas_from_detail(detail) plans.append( { "gitea_repo": gitea_repo, @@ -86,13 +121,13 @@ def build_plan(args: argparse.Namespace) -> dict[str, Any]: "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", "")), + "gitea_branch_count": int(branch_detail.get("gitea_count", event.get("branch_count_gitea", 0))), + "github_branch_count": int(branch_detail.get("github_count", event.get("branch_count_github", 0))), + "gitea_tag_count": int(tag_detail.get("gitea_count", event.get("tag_count_gitea", 0))), + "github_tag_count": int(tag_detail.get("github_count", event.get("tag_count_github", 0))), + "gitea_main_sha": detail_gitea_main or str(event.get("latest_sha_gitea", "")), + "github_main_sha": detail_github_main or str(event.get("latest_sha_github", "")), + "blocking_reason": str(detail.get("blocking_reason") or event.get("blocking_reason", "")), }, "proposed_plan_steps": proposed_steps(event), "execution_gates": DEFAULT_EXECUTION_GATES, @@ -215,6 +250,7 @@ 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("--ref-detail-diff") 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) diff --git a/scripts/security/source-control-ref-detail-diff.py b/scripts/security/source-control-ref-detail-diff.py new file mode 100644 index 00000000..dc10ff52 --- /dev/null +++ b/scripts/security/source-control-ref-detail-diff.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +"""產生 Gitea / GitHub branch/tag 明細 diff。 + +此工具只執行 read-only `git ls-remote --heads/--tags`,不 fetch、不 push、 +不寫入任一 remote。輸出前會遮蔽 remote URL 內的帳密。 +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +from pathlib import Path +from urllib.parse import urlsplit, urlunsplit + +SHA_RE = re.compile(r"^[0-9a-fA-F]{7,40}$") + +STILL_FORBIDDEN = [ + "fetch", + "push refs", + "force push", + "delete refs", + "create GitHub repo", + "change repo visibility", + "switch GitHub primary", + "disable Gitea", + "move secret values", +] + + +def run_git(repo: Path, args: list[str]) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["git", *args], + cwd=repo, + check=False, + capture_output=True, + text=True, + ) + + +def require_git(repo: Path, args: list[str]) -> str: + result = run_git(repo, args) + if result.returncode != 0: + stderr = result.stderr.strip() or result.stdout.strip() + raise RuntimeError(f"{repo}: git {' '.join(args)} failed: {stderr}") + return result.stdout + + +def redact_url(value: str) -> str: + if "://" in value: + parts = urlsplit(value) + netloc = parts.netloc.split("@", 1)[-1] + return urlunsplit((parts.scheme, netloc, parts.path, parts.query, parts.fragment)) + if "@" in value and ":" in value.split("@", 1)[1]: + return value.split("@", 1)[1] + return value + + +def repo_slug_from_url(value: str) -> str: + redacted = redact_url(value).removesuffix("/") + if "://" in redacted: + path = urlsplit(redacted).path.strip("/") + elif ":" in redacted: + path = redacted.split(":", 1)[1].strip("/") + else: + path = redacted.strip("/") + return path.removesuffix(".git") or redacted + + +def remote_url(repo: Path, remote: str) -> str: + return redact_url(require_git(repo, ["remote", "get-url", remote]).strip()) + + +def parse_ls_remote(output: str, prefix: str) -> dict[str, str]: + refs: dict[str, str] = {} + for line in output.splitlines(): + if not line.strip(): + continue + try: + sha, ref = line.split(None, 1) + except ValueError: + continue + if not SHA_RE.match(sha) or not ref.startswith(prefix): + continue + refs[ref.removeprefix(prefix)] = sha + return refs + + +def refs_for_remote( + repo: Path, + remote: str, + ignored_branches: set[str], +) -> tuple[dict[str, str], dict[str, str]]: + heads = parse_ls_remote(require_git(repo, ["ls-remote", "--heads", remote]), "refs/heads/") + for branch in ignored_branches: + heads.pop(branch, None) + raw_tags = parse_ls_remote(require_git(repo, ["ls-remote", "--tags", remote]), "refs/tags/") + tags = { + name.removesuffix("^{}"): sha + for name, sha in raw_tags.items() + if not name.endswith("^{}") + } + peeled = { + name.removesuffix("^{}"): sha + for name, sha in raw_tags.items() + if name.endswith("^{}") + } + tags.update({name: sha for name, sha in peeled.items()}) + return heads, tags + + +def ref_items(refs: dict[str, str], names: list[str]) -> list[dict[str, str]]: + return [{"name": name, "sha": refs[name]} for name in names] + + +def compare_refs(gitea_refs: dict[str, str], github_refs: dict[str, str]) -> dict[str, object]: + gitea_names = set(gitea_refs) + github_names = set(github_refs) + common = sorted(gitea_names & github_names) + only_gitea = sorted(gitea_names - github_names) + only_github = sorted(github_names - gitea_names) + mismatches = [ + { + "name": name, + "gitea_sha": gitea_refs[name], + "github_sha": github_refs[name], + } + for name in common + if gitea_refs[name] != github_refs[name] + ] + matching = [name for name in common if gitea_refs[name] == github_refs[name]] + return { + "gitea_count": len(gitea_refs), + "github_count": len(github_refs), + "only_gitea_count": len(only_gitea), + "only_github_count": len(only_github), + "sha_mismatch_count": len(mismatches), + "matching_count": len(matching), + "only_gitea": ref_items(gitea_refs, only_gitea), + "only_github": ref_items(github_refs, only_github), + "sha_mismatch": mismatches, + "matching": ref_items(gitea_refs, matching), + } + + +def repo_status(branch_diff: dict[str, object], tag_diff: dict[str, object]) -> tuple[str, str]: + reasons: list[str] = [] + if branch_diff["only_gitea_count"] or branch_diff["only_github_count"] or branch_diff["sha_mismatch_count"]: + reasons.append("branches 尚未完全對齊") + if tag_diff["only_gitea_count"] or tag_diff["only_github_count"] or tag_diff["sha_mismatch_count"]: + reasons.append("tags 尚未完全對齊") + return ("blocked", ";".join(reasons)) if reasons else ("verified", "") + + +def parse_repo_spec(value: str) -> tuple[str, Path, str, str]: + parts = value.split("=", 3) + if len(parts) != 4: + raise ValueError("--repo 格式必須是 key=/path=gitea_remote=github_remote") + key, path, gitea_remote, github_remote = parts + return key, Path(path).expanduser().resolve(), gitea_remote, github_remote + + +def build_repo_item(spec: str, ignored_branches: set[str]) -> dict[str, object]: + key, repo, gitea_remote, github_remote = parse_repo_spec(spec) + gitea_url = remote_url(repo, gitea_remote) + github_url = remote_url(repo, github_remote) + gitea_heads, gitea_tags = refs_for_remote(repo, gitea_remote, ignored_branches) + github_heads, github_tags = refs_for_remote(repo, github_remote, ignored_branches) + branch_diff = compare_refs(gitea_heads, github_heads) + tag_diff = compare_refs(gitea_tags, github_tags) + status, reason = repo_status(branch_diff, tag_diff) + return { + "repo_key": key, + "repo_path": str(repo), + "gitea_remote": gitea_remote, + "github_remote": github_remote, + "gitea_url_redacted": gitea_url, + "github_url_redacted": github_url, + "gitea_repo": repo_slug_from_url(gitea_url), + "github_repo": repo_slug_from_url(github_url), + "status": status, + "blocking_reason": reason, + "branch_diff": branch_diff, + "tag_diff": tag_diff, + "still_forbidden": STILL_FORBIDDEN, + } + + +def build_snapshot(args: argparse.Namespace) -> dict[str, object]: + ignored_branches = set(args.ignore_branch or []) + repos = [build_repo_item(spec, ignored_branches) for spec in args.repo] + return { + "schema_version": "source_control_ref_detail_diff_v1", + "status": "draft_blocked", + "date": args.date, + "default_mode": "read_only_diff", + "ignored_branches": sorted(ignored_branches), + "repo_count": len(repos), + "repos": repos, + } + + +def short_sha(value: str) -> str: + return value[:8] if value else "" + + +def write_ref_list(lines: list[str], title: str, refs: list[dict[str, str]], *, limit: int) -> None: + lines.extend([f"#### {title}", ""]) + if not refs: + lines.extend(["無。", ""]) + return + visible = refs if limit <= 0 else refs[:limit] + for item in visible: + lines.append(f"- `{item['name']}` @ `{short_sha(item['sha'])}`") + if limit > 0 and len(refs) > limit: + lines.append(f"- 另有 `{len(refs) - limit}` 筆,完整清單見 JSON snapshot。") + lines.append("") + + +def write_markdown(snapshot: dict[str, object], path: Path, *, list_limit: int) -> None: + lines = [ + "# Source Control Branch / Tag Detail Diff", + "", + "| 項目 | 內容 |", + "|------|------|", + f"| 日期 | {snapshot['date']} |", + f"| 狀態 | `{snapshot['status']}` |", + f"| 預設模式 | `{snapshot['default_mode']}` |", + f"| ignored branches | `{', '.join(snapshot['ignored_branches']) or '無'}` |", + f"| repo count | {snapshot['repo_count']} |", + "", + "## 0. 核心結論", + "", + "本檔是 read-only refs 明細 diff,不是同步腳本。任何 refs sync、GitHub primary 切換、repo 建立或 visibility 修改都仍需單一 repo 人工批准。", + "", + "## 1. 摘要", + "", + "| Repo | Status | Branch only Gitea | Branch only GitHub | Branch SHA diff | Tag only Gitea | Tag only GitHub | Tag SHA diff |", + "|------|--------|-------------------|--------------------|-----------------|----------------|-----------------|--------------|", + ] + for repo in snapshot["repos"]: + branch = repo["branch_diff"] + tag = repo["tag_diff"] + lines.append( + "| " + + " | ".join( + [ + f"`{repo['gitea_repo']} -> {repo['github_repo']}`", + f"`{repo['status']}`", + f"`{branch['only_gitea_count']}`", + f"`{branch['only_github_count']}`", + f"`{branch['sha_mismatch_count']}`", + f"`{tag['only_gitea_count']}`", + f"`{tag['only_github_count']}`", + f"`{tag['sha_mismatch_count']}`", + ] + ) + + " |" + ) + + lines.extend(["", "## 2. Repo 明細", ""]) + for repo in snapshot["repos"]: + branch = repo["branch_diff"] + tag = repo["tag_diff"] + lines.extend( + [ + f"### {repo['gitea_repo']} -> {repo['github_repo']}", + "", + f"- Status:`{repo['status']}`", + f"- Blocking reason:{repo['blocking_reason'] or '無'}", + f"- Gitea URL:`{repo['gitea_url_redacted']}`", + f"- GitHub URL:`{repo['github_url_redacted']}`", + "", + "#### Branch SHA 不一致", + "", + ] + ) + if branch["sha_mismatch"]: + for item in branch["sha_mismatch"]: + lines.append( + f"- `{item['name']}`:Gitea `{short_sha(item['gitea_sha'])}` / GitHub `{short_sha(item['github_sha'])}`" + ) + lines.append("") + else: + lines.extend(["無。", ""]) + write_ref_list(lines, "Branch 只在 Gitea", branch["only_gitea"], limit=list_limit) + write_ref_list(lines, "Branch 只在 GitHub", branch["only_github"], limit=list_limit) + write_ref_list(lines, "Tag 只在 Gitea", tag["only_gitea"], limit=list_limit) + write_ref_list(lines, "Tag 只在 GitHub", tag["only_github"], limit=list_limit) + lines.extend(["#### 仍然禁止", ""]) + for value in repo["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("--repo", action="append", required=True) + parser.add_argument("--ignore-branch", action="append", default=[]) + parser.add_argument("--output-json", required=True) + parser.add_argument("--output-md", required=True) + parser.add_argument("--md-list-limit", type=int, default=50) + args = parser.parse_args() + + snapshot = build_snapshot(args) + Path(args.output_json).write_text( + json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + write_markdown(snapshot, Path(args.output_md), list_limit=args.md_list_limit) + print(f"OK source-control ref detail diff repos={snapshot['repo_count']}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())