diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 447af1972..53d6d2590 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -3,6 +3,7 @@ **照主線修正的問題**: - `ops/runner/read-public-gitea-actions-queue.py` 新增 Harbor CD self-heal readback 欄位:`latest_visible_cd_harbor_controlled_repair_attempted`、`latest_visible_cd_harbor_controlled_repair_skip_reason`、`latest_visible_cd_harbor_controlled_repair_public_registry_v2_status`。 - Rollups 同步暴露 `current_main_cd_harbor_controlled_repair_attempted` 與 skip reason,讓 mainline dashboard 能分辨「registry `/v2/` 壞」與「CD self-heal 因非 110 runner / watchdog missing / disabled 而無法執行」。 +- 新增 visible `Blocked` classifier:當 Gitea HTML 最新 CD run 顯示 Blocked,即使 jobs API 回 stale success,也回 `blocked_latest_visible_cd_run`,避免 stale jobs API 蓋掉 source truth。 - Regression fixture 新增 `harbor_controlled_repair_skipped=not_110_host`,鎖住目前 `#4053` 類型的失敗不會被泛化成單純 Harbor 502。 **驗證**: diff --git a/ops/runner/read-public-gitea-actions-queue.py b/ops/runner/read-public-gitea-actions-queue.py index 7592b0af8..4e5bc32c0 100644 --- a/ops/runner/read-public-gitea-actions-queue.py +++ b/ops/runner/read-public-gitea-actions-queue.py @@ -227,6 +227,7 @@ def build_readback( ) build_log_classifier = classify_cd_build_log(latest_cd_build_log_text) tests_log_classifier = classify_cd_tests_log(latest_cd_tests_log_text) + latest_cd_visible_blocked = latest_cd_run.get("status", "") == "Blocked" readback = { "actions_page_visible_run_count": len(visible_runs), @@ -251,6 +252,7 @@ def build_readback( ), "latest_visible_cd_run_id": latest_cd_run.get("run_id", ""), "latest_visible_cd_run_status": latest_cd_run.get("status", ""), + "latest_visible_cd_run_blocked": latest_cd_visible_blocked, "latest_visible_cd_run_kind": latest_cd_run.get("kind", ""), "latest_visible_cd_run_title": latest_cd_run.get("title", ""), "latest_visible_cd_run_commit_sha": latest_cd_run.get("commit_sha", ""), @@ -308,6 +310,8 @@ def build_readback( "status": ( "blocked_no_matching_online_runner" if no_matching + else "blocked_latest_visible_cd_run" + if latest_cd_visible_blocked else "blocked_harbor_public_route_unavailable" if build_log_classifier["harbor_public_route_blocked"] else "blocked_host_web_build_pressure" @@ -324,6 +328,7 @@ def build_readback( "cd_run_jobs_stale_or_mismatched": cd_jobs_stale_or_mismatched, "current_main_cd_run_visible": bool(latest_cd_run), "current_main_cd_run_status": latest_cd_run.get("status", ""), + "current_main_cd_run_blocked": latest_cd_visible_blocked, "current_main_cd_failure_classifier": build_log_classifier[ "failure_classifier" ], diff --git a/ops/runner/test_read_public_gitea_actions_queue.py b/ops/runner/test_read_public_gitea_actions_queue.py index 9c219a006..ab109746e 100644 --- a/ops/runner/test_read_public_gitea_actions_queue.py +++ b/ops/runner/test_read_public_gitea_actions_queue.py @@ -112,6 +112,13 @@ def _actions_html_failed_cd_run() -> str: """ +def _actions_html_blocked_cd_run() -> str: + return _actions_html_failed_cd_run().replace( + 'data-tooltip-content="Failure"', + 'data-tooltip-content="Blocked"', + ) + + def _harbor_blocked_log() -> str: return """ 2026-06-30T11:33:05.5822531Z harbor_login_attempt=1 registry_v2_status=000 @@ -224,6 +231,32 @@ def test_build_readback_classifies_harbor_public_route_blocker() -> None: ) +def test_build_readback_prefers_visible_blocked_over_stale_jobs() -> None: + module = _load_module() + payload = module.build_readback( + actions_html=_actions_html_blocked_cd_run(), + actions_list_http_status=401, + actions_list_payload={"message": "token is required"}, + cd_jobs_http_status=200, + cd_jobs_payload={ + "jobs": [ + { + "run_id": 4043, + "head_sha": "528d2c54e266b2362f7cc004aaa3378406e889c4", + "conclusion": "success", + } + ], + "total_count": 1, + }, + ) + + assert payload["status"] == "blocked_latest_visible_cd_run" + assert payload["readback"]["latest_visible_cd_run_status"] == "Blocked" + assert payload["readback"]["latest_visible_cd_run_blocked"] is True + assert payload["readback"]["cd_run_jobs_stale_or_mismatched"] is True + assert payload["rollups"]["current_main_cd_run_blocked"] is True + + def test_build_readback_classifies_host_pressure_waiting() -> None: module = _load_module() payload = module.build_readback(