diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index a07739c7..a3399183 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,18 @@ +## 2026-06-30 — 21:23 Harbor 110 repair `awoooi-host` runner blocker closure readback + +**照主線修正的問題**: +- Live public queue 已由 `4ccd800c1` 的 reader 回 `status=blocked_harbor_110_repair_no_matching_runner`:最新 CD `#4075` running,Harbor repair `#4076` Waiting,`latest_visible_harbor_110_repair_no_matching_runner_label=awoooi-host`,registry public / internal `/v2/` 仍為 502。 +- 現有 `verify-awoooi-non110-cd-closure.py` 雖讀到 queue status,仍降級成 generic `blocked_no_matching_online_runner`;本輪新增專用 closure blocker `harbor_110_repair_no_matching_runner` 與 next action `restore_awoooi_host_runner_control_path_without_legacy_or_generic_labels_then_rerun_harbor_110_repair_queue_readback`。 +- Live closure verifier 現在回 `status=blocked_harbor_110_repair_no_matching_runner`、`ordered_completion_percent=33`、`evidence_completion_percent=67`;下一步明確是恢復 `awoooi-host` 110 repair control path,而不是泛化重跑 queue 或宣稱 Harbor repair 成功。 + +**驗證**: +- `py_compile ops/runner/verify-awoooi-non110-cd-closure.py ops/runner/test_verify_awoooi_non110_cd_closure.py` 通過。 +- `pytest ops/runner/test_verify_awoooi_non110_cd_closure.py -q` 通過(8 passed)。 +- `ruff check ops/runner/verify-awoooi-non110-cd-closure.py ops/runner/test_verify_awoooi_non110_cd_closure.py` 通過。 +- Live `verify-awoooi-non110-cd-closure.py --json` 回專用 `blocked_harbor_110_repair_no_matching_runner`,且 operation boundaries 全部維持 no-write / no-secret / no-dispatch。 + +**邊界**:只改 closure verifier / tests / LOGBOOK;110 SSH read-only metadata probe timeout 後已中止,未讀 `.runner`、secret、token、`.env`、raw sessions、SQLite 或 auth;未使用 GitHub / `gh` / GitHub API;未 workflow_dispatch;未 SSH 寫入、Docker / Nginx / K3s / DB / firewall runtime 寫入或服務重啟。 + ## 2026-06-30 — 21:13 Priority work-order readback 對齊 reboot SLO fail-closed 真相 **照主線修正的問題**: diff --git a/ops/runner/test_verify_awoooi_non110_cd_closure.py b/ops/runner/test_verify_awoooi_non110_cd_closure.py index 847f67a2..5a79edfd 100644 --- a/ops/runner/test_verify_awoooi_non110_cd_closure.py +++ b/ops/runner/test_verify_awoooi_non110_cd_closure.py @@ -24,18 +24,29 @@ def _load_module(): return module -def _queue(*, no_matching: bool) -> dict: +def _queue(*, no_matching: bool, harbor_110_no_matching: bool = False) -> dict: return { "schema_version": "awoooi_public_gitea_actions_queue_readback_v1", "status": ( + "blocked_harbor_110_repair_no_matching_runner" + if harbor_110_no_matching + else ( "blocked_no_matching_online_runner" if no_matching else "no_matching_runner_not_visible" + ) ), "readback": { "no_matching_online_runner_visible": no_matching, "latest_visible_no_matching_runner_label": ( - "awoooi-non110-ubuntu" if no_matching else "" + "awoooi-host" + if harbor_110_no_matching + else "awoooi-non110-ubuntu" + if no_matching + else "" + ), + "latest_visible_harbor_110_repair_no_matching_runner_label": ( + "awoooi-host" if harbor_110_no_matching else "" ), }, "operation_boundaries": { @@ -174,6 +185,31 @@ def test_closure_verifier_blocks_queue_after_runner_ready() -> None: assert "public_queue_still_has_no_matching_online_runner" in payload["blockers"] +def test_closure_verifier_prioritizes_harbor_110_runner_label_blocker() -> None: + module = _load_module() + payload = module.build_closure_verifier( + readiness_text=_readiness(ready=True), + queue=_queue(no_matching=True, harbor_110_no_matching=True), + production_workbench=_workbench(image_current=False, governance_ready=False), + ) + + assert payload["status"] == "blocked_harbor_110_repair_no_matching_runner" + assert "harbor_110_repair_no_matching_runner" in payload["blockers"] + assert "public_queue_still_has_no_matching_online_runner" not in payload[ + "blockers" + ] + assert payload["readback"]["harbor_110_repair_no_matching_runner"] is True + assert ( + payload["readback"]["harbor_110_repair_no_matching_runner_label"] + == "awoooi-host" + ) + assert payload["progress"]["next_blocked_step_id"] == "public_queue_runner_match" + assert "awoooi_host_runner_control_path" in payload["next_actions"][0] + assert "awoooi_host_runner_control_path" in payload["progress"][ + "next_blocked_step_action" + ] + + def test_closure_verifier_uses_deploy_snapshot_when_readiness_file_missing() -> None: module = _load_module() payload = module.build_closure_verifier( diff --git a/ops/runner/verify-awoooi-non110-cd-closure.py b/ops/runner/verify-awoooi-non110-cd-closure.py index 07b00917..fe3b7876 100755 --- a/ops/runner/verify-awoooi-non110-cd-closure.py +++ b/ops/runner/verify-awoooi-non110-cd-closure.py @@ -176,6 +176,7 @@ def _build_ordered_steps( *, readiness: dict[str, Any], no_matching_runner_visible: bool, + queue_runner_match_next_action: str, production_workbench_present: bool, production_image_tag_matches_main: bool, production_governance_fields_present: bool, @@ -208,7 +209,7 @@ def _build_ordered_steps( "id": "public_queue_runner_match", "title": "public Gitea queue no longer shows no-matching-runner", "evidence_ready": not no_matching_runner_visible, - "next_action": "rerun_public_queue_readback_until_no_matching_runner_is_absent", + "next_action": queue_runner_match_next_action, }, { "id": "production_workbench_readback", @@ -300,11 +301,26 @@ def build_closure_verifier( if isinstance(queue.get("operation_boundaries"), dict) else {} ) + queue_status = str(queue.get("status") or "") production = _production_summary(production_workbench) no_matching_runner_visible = ( queue_readback.get("no_matching_online_runner_visible") is True ) + harbor_110_repair_no_matching_runner_label = str( + queue_readback.get("latest_visible_harbor_110_repair_no_matching_runner_label") + or "" + ) + harbor_110_repair_no_matching_runner = ( + queue_status == "blocked_harbor_110_repair_no_matching_runner" + or bool(harbor_110_repair_no_matching_runner_label) + ) + queue_runner_match_next_action = ( + "restore_awoooi_host_runner_control_path_without_legacy_or_generic_labels_" + "then_rerun_harbor_110_repair_queue_readback" + if harbor_110_repair_no_matching_runner + else "rerun_public_queue_readback_until_no_matching_runner_is_absent" + ) production_workbench_present = bool(production) production_image_tag_matches_main = _production_image_tag_current(production) production_governance_fields_present = ( @@ -318,7 +334,9 @@ def build_closure_verifier( blockers.append("non110_runner_not_ready") if readiness["raw_runner_registration_read"]: blockers.append("raw_runner_registration_read_not_allowed") - if no_matching_runner_visible: + if harbor_110_repair_no_matching_runner: + blockers.append("harbor_110_repair_no_matching_runner") + elif no_matching_runner_visible: blockers.append("public_queue_still_has_no_matching_online_runner") if not production_workbench_present: blockers.append("production_workbench_readback_missing") @@ -333,6 +351,8 @@ def build_closure_verifier( status = "blocked_non110_runner_not_ready" elif "raw_runner_registration_read_not_allowed" in blockers: status = "blocked_secret_boundary_violation" + elif "harbor_110_repair_no_matching_runner" in blockers: + status = "blocked_harbor_110_repair_no_matching_runner" elif "public_queue_still_has_no_matching_online_runner" in blockers: status = "blocked_no_matching_online_runner" elif "production_workbench_readback_missing" in blockers: @@ -347,6 +367,7 @@ def build_closure_verifier( ordered_steps = _build_ordered_steps( readiness=readiness, no_matching_runner_visible=no_matching_runner_visible, + queue_runner_match_next_action=queue_runner_match_next_action, production_workbench_present=production_workbench_present, production_image_tag_matches_main=production_image_tag_matches_main, production_governance_fields_present=production_governance_fields_present, @@ -382,6 +403,12 @@ def build_closure_verifier( "public_queue_latest_no_matching_runner_label": str( queue_readback.get("latest_visible_no_matching_runner_label") or "" ), + "harbor_110_repair_no_matching_runner": ( + harbor_110_repair_no_matching_runner + ), + "harbor_110_repair_no_matching_runner_label": ( + harbor_110_repair_no_matching_runner_label + ), "production_workbench_present": production_workbench_present, "production_workbench_source_count": _int(production.get("source_count")), "production_deploy_image_tag_matches_main": ( @@ -417,9 +444,7 @@ def build_closure_verifier( readiness["safe_next_step"] if not readiness["ready"] else "", - "rerun_public_queue_readback_until_no_matching_runner_is_absent" - if no_matching_runner_visible - else "", + queue_runner_match_next_action if no_matching_runner_visible else "", "read_production_delivery_workbench_after_deploy" if not production_workbench_present else "",