diff --git a/apps/api/src/services/awoooi_priority_work_order_readback.py b/apps/api/src/services/awoooi_priority_work_order_readback.py index 8bc0a1e9..7f9ce97d 100644 --- a/apps/api/src/services/awoooi_priority_work_order_readback.py +++ b/apps/api/src/services/awoooi_priority_work_order_readback.py @@ -8,14 +8,18 @@ external SCM APIs. from __future__ import annotations import json +import re +from datetime import datetime from pathlib import Path from typing import Any +from zoneinfo import ZoneInfo from src.services.snapshot_paths import default_operations_dir _DEFAULT_OPERATIONS_DIR = default_operations_dir(Path(__file__)) _SNAPSHOT_FILE = "awoooi-priority-work-order-readback.snapshot.json" _SCHEMA_VERSION = "awoooi_priority_work_order_readback_v1" +_SHA_RE = re.compile(r"^[0-9a-f]{40}$") def load_latest_awoooi_priority_work_order_readback( @@ -117,26 +121,49 @@ def _enrich_from_current_readbacks(payload: dict[str, Any]) -> None: runtime_sha = str( workbench_summary.get("production_deploy_runtime_build_commit_sha") or "" - ) + ).strip().lower() runtime_short_sha = str( workbench_summary.get("production_deploy_runtime_build_commit_short_sha") or "" - ) + ).strip().lower() desired_short_sha = str( workbench_summary.get( "production_deploy_desired_main_api_image_tag_short_sha" ) or "" - ) + ).strip().lower() current_head = _dict(payload.setdefault("current_head", {})) - current_head["latest_verified_worktree_base_sha"] = runtime_short_sha - current_head["latest_successful_deployed_source_sha"] = runtime_sha - current_head["latest_successful_deploy_marker"] = ( - f"production desired image tag {desired_short_sha}" + runtime_source_truth_available = _is_sha(runtime_sha) + if runtime_source_truth_available: + runtime_short_sha = runtime_short_sha or runtime_sha[:10] + desired_short_sha = desired_short_sha or runtime_short_sha + production_readback_id = f"production_readback:{desired_short_sha}" + production_subject = f"production desired image tag {desired_short_sha}" + + current_head["gitea_main_sha"] = runtime_sha + current_head["latest_fetched_gitea_main_subject"] = production_subject + current_head["latest_source_readiness_cd_run_id"] = production_readback_id + current_head["latest_verified_worktree_base_sha"] = runtime_short_sha + current_head["latest_successful_deployed_source_sha"] = runtime_sha + current_head["latest_successful_deploy_marker"] = production_subject + current_head["latest_source_readiness_cd_run_status"] = ( + "production_readback_verified" + ) + current_head["latest_source_readiness_commit_sha"] = runtime_sha + + state["updated_at"] = _taipei_now_iso() + state["current_main_cd_run_id"] = production_readback_id + state["current_main_cd_run_status"] = "production_readback_verified" + state["current_main_latest_source_sha"] = runtime_sha + state["latest_successful_deployed_source_sha"] = runtime_sha + state["reboot_drill_preflight_source_status"] = "production_endpoint_ready" + state["p0_004_template_copy_apply_gate_source_status"] = ( + "production_endpoint_ready" + ) + + current_head["production_source_truth_available"] = runtime_source_truth_available + current_head["stale_current_head_metadata_normalized"] = ( + runtime_source_truth_available ) - current_head["latest_source_readiness_cd_run_status"] = ( - "production_readback_verified" - ) - current_head["latest_source_readiness_commit_sha"] = runtime_sha current_head["no_matching_runner_visible"] = False current_head["source_readiness_ci_fix_required"] = False @@ -276,6 +303,74 @@ def _enrich_from_current_readbacks(payload: dict[str, Any]) -> None: "failed CD runs, and retired GitHub lanes must not reorder closed work." ), ] + _set_rollups_and_summary( + payload=payload, + current_head=current_head, + state=state, + p0_004_ready=p0_004_ready, + p0_006_event_gated=p0_006_event_gated, + runtime_source_truth_available=runtime_source_truth_available, + runtime_sha=runtime_sha, + ) + + +def _set_rollups_and_summary( + *, + payload: dict[str, Any], + current_head: dict[str, Any], + state: dict[str, Any], + p0_004_ready: bool, + p0_006_event_gated: bool, + runtime_source_truth_available: bool, + runtime_sha: str, +) -> None: + completed = _list(payload.get("completed_in_priority_order")) + in_progress = _list(payload.get("in_progress_or_blocked_in_priority_order")) + latest_source_sha = ( + runtime_sha + if runtime_source_truth_available + else str(current_head.get("latest_successful_deployed_source_sha") or "") + ) + active_blockers = _strings(state.get("active_p0_live_active_blockers")) + active_gap_count = _int(state.get("active_p0_immediate_apply_gap_count")) + + payload["rollups"] = { + "completed_p0_count": len(completed), + "in_progress_or_blocked_count": len(in_progress), + "active_p0_immediate_apply_gap_count": active_gap_count, + "active_p0_event_gated_by_fresh_reboot_window_only": p0_006_event_gated, + "active_p0_live_active_blocker_count": len(active_blockers), + "p0_004_runtime_readback_ready": p0_004_ready, + "reboot_drill_preflight_runtime_readback_ready": ( + state.get("reboot_drill_preflight_runtime_readback_state") == "ready" + ), + "production_source_truth_available": runtime_source_truth_available, + "stale_current_head_metadata_normalized": runtime_source_truth_available, + "stale_cd_runs_reopen_closed_work": False, + "break_glass_reboot_drill_required": True, + } + payload["summary"] = { + "status": str(payload.get("status") or ""), + "active_p0_workplan_id": str(state.get("active_p0_workplan_id") or ""), + "active_p0_state": str(state.get("active_p0_state") or ""), + "active_p0_readiness_percent": _int( + state.get("active_p0_readiness_percent") + ), + "active_p0_live_active_blockers": active_blockers, + "latest_successful_deployed_source_sha": latest_source_sha, + "latest_successful_deployed_source_short_sha": latest_source_sha[:10], + "latest_successful_deploy_marker": str( + current_head.get("latest_successful_deploy_marker") or "" + ), + "next_executable_mainline_workplan_id": str( + state.get("next_executable_mainline_workplan_id") or "" + ), + "next_executable_mainline_state": str( + state.get("next_executable_mainline_state") or "" + ), + "production_source_truth_available": runtime_source_truth_available, + "stale_current_head_metadata_normalized": runtime_source_truth_available, + } def _require_schema(payload: dict[str, Any], label: str) -> None: @@ -358,3 +453,11 @@ def _int(value: Any) -> int: def _strings(value: Any) -> list[str]: return [str(item) for item in _list(value)] + + +def _is_sha(value: str) -> bool: + return bool(_SHA_RE.fullmatch(value)) + + +def _taipei_now_iso() -> str: + return datetime.now(ZoneInfo("Asia/Taipei")).isoformat(timespec="seconds") diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index 1ff5a924..dd98054c 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -42,12 +42,12 @@ from src.services.gitea_workflow_runner_owner_attestation_request import ( from src.services.p0_cicd_baseline_source_readiness import ( load_latest_p0_cicd_baseline_source_readiness, ) -from src.services.reboot_auto_recovery_slo_scorecard import ( - load_latest_reboot_auto_recovery_slo_scorecard, -) from src.services.reboot_auto_recovery_drill_preflight import ( load_latest_reboot_auto_recovery_drill_preflight, ) +from src.services.reboot_auto_recovery_slo_scorecard import ( + load_latest_reboot_auto_recovery_slo_scorecard, +) from src.services.runtime_surface_inventory import ( load_latest_runtime_surface_inventory, ) diff --git a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py index edb4cafc..0b27a98a 100644 --- a/apps/api/tests/test_awoooi_priority_work_order_readback_api.py +++ b/apps/api/tests/test_awoooi_priority_work_order_readback_api.py @@ -74,6 +74,50 @@ def test_awoooi_priority_work_order_readback_endpoint_returns_snapshot(): assert "do not reboot" in data["next_execution_order"][0] +def test_awoooi_priority_work_order_readback_normalizes_runtime_source_truth( + monkeypatch: pytest.MonkeyPatch, +): + runtime_sha = "b4dc407ce05c68a3908b993437a61b869d83810f" + runtime_short_sha = runtime_sha[:10] + monkeypatch.setenv("AWOOOI_BUILD_COMMIT_SHA", runtime_sha) + monkeypatch.setenv("AWOOOI_DESIRED_API_IMAGE_TAG", runtime_sha) + + payload = load_latest_awoooi_priority_work_order_readback() + + current_head = payload["current_head"] + state = payload["mainline_execution_state"] + assert current_head["gitea_main_sha"] == runtime_sha + assert current_head["latest_source_readiness_commit_sha"] == runtime_sha + assert current_head["latest_source_readiness_cd_run_id"] == ( + f"production_readback:{runtime_short_sha}" + ) + assert current_head["latest_fetched_gitea_main_subject"] == ( + f"production desired image tag {runtime_short_sha}" + ) + assert current_head["stale_current_head_metadata_normalized"] is True + assert state["current_main_latest_source_sha"] == runtime_sha + assert state["latest_successful_deployed_source_sha"] == runtime_sha + assert state["current_main_cd_run_status"] == "production_readback_verified" + assert state["p0_004_template_copy_apply_gate_source_status"] == ( + "production_endpoint_ready" + ) + assert state["reboot_drill_preflight_source_status"] == "production_endpoint_ready" + assert payload["rollups"]["stale_current_head_metadata_normalized"] is True + assert payload["summary"]["latest_successful_deployed_source_sha"] == runtime_sha + + current_truth = json.dumps( + { + "current_head": current_head, + "mainline_execution_state": state, + }, + ensure_ascii=False, + sort_keys=True, + ) + assert "3973" not in current_truth + assert "49637ee7" not in current_truth + assert "f426522" not in current_truth + + def test_awoooi_priority_work_order_readback_rejects_reordered_active_p0(tmp_path): operations_dir = tmp_path / "docs" / "operations" operations_dir.mkdir(parents=True) diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index bf45d639..a2c73eef 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -50156,3 +50156,23 @@ production browser smoke: - 沒有讀 secret / token / `.env` / raw sessions / SQLite / auth,沒有寫 credential marker。 - 沒有重啟主機,沒有 restart Docker / Nginx / K3s / DB / firewall。 - 沒有使用 GitHub / gh / GitHub API / GitHub Actions。 + +## 2026-06-30 — 09:28 P0 priority work-order runtime source truth 正規化 + +**完成內容**: +- 修正 `apps/api/src/services/awoooi_priority_work_order_readback.py`:當 Delivery Workbench 讀到合法 production runtime deploy SHA 時,將 `current_head` 與 `mainline_execution_state` 的 current main / CD / deployed source 欄位正規化為 `production_readback:`,不再讓舊 snapshot 的 `#3973`、`49637ee7` 或 `f426522` 混入目前主線進度。 +- 新增 `rollups` / `summary`,明確輸出 `production_source_truth_available`、`stale_current_head_metadata_normalized`、`stale_cd_runs_reopen_closed_work=false`、P0-006 fresh reboot event gate 與 P0-004 / reboot preflight readiness。 +- 補 `apps/api/tests/test_awoooi_priority_work_order_readback_api.py` regression:以 `AWOOOI_BUILD_COMMIT_SHA` / `AWOOOI_DESIRED_API_IMAGE_TAG` 模擬 production env,鎖住 priority readback 不再回吐舊 CD/run/source metadata。 + +**本地驗證結果**: +- rebase 到 `a7e6de46a feat(delivery): expose p0 reboot drill readback` 後,`DATABASE_URL=postgresql+asyncpg://test:test@localhost:5432/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_awoooi_priority_work_order_readback_api.py apps/api/tests/test_delivery_closure_workbench_api.py ops/runner/test_cd_controlled_runtime_profile.py -q`:`35 passed`。 +- `PYTHONPATH=apps/api python3.11 -m py_compile apps/api/src/services/awoooi_priority_work_order_readback.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py apps/api/src/services/delivery_closure_workbench.py apps/api/tests/test_delivery_closure_workbench_api.py`:通過。 +- `PYTHONPATH=apps/api python3.11 -m ruff check apps/api/src/services/awoooi_priority_work_order_readback.py apps/api/tests/test_awoooi_priority_work_order_readback_api.py apps/api/src/services/delivery_closure_workbench.py apps/api/tests/test_delivery_closure_workbench_api.py`:通過。 +- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .`:通過,`auto_branch_events_on_110=0`、`generic_runner_labels=0`。 +- `node scripts/ci/check-gitea-step-env-secrets.js`:通過。 +- `git diff --check`:通過。 + +**仍維持**: +- 沒有讀 secret / token / `.env` / raw sessions / SQLite / auth。 +- 沒有使用 GitHub / gh / GitHub API / GitHub Actions。 +- 沒有重啟主機,沒有 workflow_dispatch,沒有 host / Docker / K8s / DB / firewall runtime 寫入。