fix(api): fail closed on stale production runtime deploy readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-30 18:55:15 +08:00
parent 12c1811e50
commit 76fee33e1b
5 changed files with 112 additions and 10 deletions

View File

@@ -135,7 +135,11 @@ def _enrich_runtime_build_readback(payload: dict[str, Any]) -> None:
readback["desired_main_api_image_tag_readback_status"] = "ok"
readback["desired_main_api_image_tag_sha"] = desired_tag
readback["desired_main_api_image_tag_short_sha"] = desired_tag[:10]
image_matches_main = build_sha == desired_tag
image_matches_main = (
build_sha == desired_tag
and source_matches_runtime
and image_matches_runtime
)
readback["production_image_tag_matches_main"] = image_matches_main
rollups["source_control_main_ready"] = True
rollups["production_image_tag_matches_main"] = image_matches_main

View File

@@ -5,7 +5,7 @@ from src.services import awoooi_production_deploy_readback_blocker as service
_COMMITTED_SNAPSHOT_SHA = "a70c6756d9e76c33143676eef82bab7a49ac1839"
def test_production_deploy_readback_verifies_runtime_build_against_gitops_desired(
def test_production_deploy_readback_blocks_stale_source_even_when_gitops_desired_matches_runtime(
monkeypatch,
):
build_sha = "0123456789abcdef0123456789abcdef01234567"
@@ -33,10 +33,13 @@ def test_production_deploy_readback_verifies_runtime_build_against_gitops_desire
assert readback["desired_main_api_image_tag_sha"] == build_sha
assert readback["desired_main_api_image_tag_source"] == "gitops_deployment_env"
assert readback["desired_main_api_image_tag_readback_status"] == "ok"
assert readback["production_image_tag_matches_main"] is True
assert payload["status"] == "closure_verified"
assert rollups["production_image_tag_matches_main"] is True
assert rollups["hard_blocker_count"] == 0
assert readback["production_image_tag_matches_main"] is False
assert payload["status"] == "blocked_production_runtime_image_tag_not_verified"
assert rollups["production_image_tag_matches_main"] is False
assert rollups["hard_blocker_count"] == 1
assert "production_runtime_image_tag_does_not_match_gitea_main_desired_tag" in (
payload["blockers"]
)
def test_production_deploy_readback_keeps_committed_snapshot_evidence(monkeypatch):

View File

@@ -1,3 +1,16 @@
## 2026-06-30 — 19:22 P0-006 production deploy closure false-positive guard
**照主線修正的問題**
- Production Delivery Workbench 仍讀到舊 runtime build `7890778b83`,但舊 `production_image_tag_matches_main=true` 只代表 runtime build 等於舊 GitOps desired tag不能證明 runtime build 已等於目前 Gitea main。
- `awoooi_production_deploy_readback_blocker.py` 現在要求 runtime build 同時等於 GitOps desired tag、committed source-control readback 與 committed production image tag才可宣稱 `production_image_tag_matches_main=true`
- `verify-awoooi-non110-cd-closure.py` 現在把 runtime/source/image 三個 readback 布林納入 closure 判定live production 目前正確回 `blocked_production_image_not_current`,下一步是完成 CD 後重跑 production route/readback。
**驗證**
- `pytest apps/api/tests/test_awoooi_production_deploy_readback_blocker.py apps/api/tests/test_delivery_closure_workbench_api.py ops/runner/test_verify_awoooi_non110_cd_closure.py ops/runner/test_cd_controlled_runtime_profile.py -q``43 passed`
- `py_compile``git diff --check`:通過。
**邊界**:未 workflow_dispatch未 SSH 寫主機,未重啟主機,未 restart Docker daemon / host Nginx / K3s / DB / Redis / firewall未 prune / restore / DB write未讀 secret / token / raw sessions / SQLite / `.env`,未使用 GitHub / `gh` / GitHub API。
## 2026-06-30 — 18:52 P0-006 StockPlatform receipt updated after upstream recovery
**照主線修正的問題**

View File

@@ -46,12 +46,35 @@ def _queue(*, no_matching: bool) -> dict:
}
def _workbench(*, image_current: bool, governance_ready: bool) -> dict:
def _workbench(
*,
image_current: bool,
governance_ready: bool,
runtime_source_current: bool | None = None,
runtime_image_current: bool | None = None,
) -> dict:
runtime_source_current = (
image_current if runtime_source_current is None else runtime_source_current
)
runtime_image_current = (
image_current if runtime_image_current is None else runtime_image_current
)
return {
"schema_version": "delivery_closure_workbench_v1",
"summary": {
"source_count": 6,
"production_deploy_image_tag_matches_main": image_current,
"production_deploy_runtime_build_matches_committed_source_control_readback": (
runtime_source_current
),
"production_deploy_runtime_build_matches_committed_production_image_tag": (
runtime_image_current
),
"production_deploy_runtime_build_readback_status": (
"matches_committed_deploy_readback"
if runtime_source_current and runtime_image_current
else "runtime_build_diverges_from_committed_deploy_readback"
),
"production_deploy_governance_fields_present": governance_ready,
},
}
@@ -191,6 +214,37 @@ def test_closure_verifier_accepts_full_closure_evidence() -> None:
assert all(step["status"] == "complete" for step in payload["ordered_steps"])
assert payload["blockers"] == []
assert payload["readback"]["production_deploy_image_tag_matches_main"] is True
assert (
payload["readback"][
"production_deploy_runtime_build_matches_committed_source_control_readback"
]
is True
)
def test_closure_verifier_blocks_stale_runtime_even_when_desired_tag_matches() -> None:
module = _load_module()
payload = module.build_closure_verifier(
readiness_text=_readiness(ready=True),
queue=_queue(no_matching=False),
production_workbench=_workbench(
image_current=True,
governance_ready=True,
runtime_source_current=False,
runtime_image_current=False,
),
)
assert payload["status"] == "blocked_production_image_not_current"
assert payload["progress"]["ordered_completed_prefix_count"] == 4
assert payload["progress"]["next_blocked_step_id"] == "production_image_tag_current"
assert "production_image_tag_not_current" in payload["blockers"]
assert payload["readback"]["production_deploy_image_tag_matches_main"] is False
assert (
payload["readback"][
"production_deploy_runtime_build_matches_committed_source_control_readback"
]
is False
)
def test_cli_uses_fixture_files_without_live_dispatch(tmp_path: Path) -> None:

View File

@@ -158,6 +158,20 @@ def _production_summary(workbench: dict[str, Any]) -> dict[str, Any]:
return summary if isinstance(summary, dict) else {}
def _production_image_tag_current(production: dict[str, Any]) -> bool:
return (
production.get("production_deploy_image_tag_matches_main") is True
and production.get(
"production_deploy_runtime_build_matches_committed_source_control_readback"
)
is True
and production.get(
"production_deploy_runtime_build_matches_committed_production_image_tag"
)
is True
)
def _build_ordered_steps(
*,
readiness: dict[str, Any],
@@ -292,9 +306,7 @@ def build_closure_verifier(
queue_readback.get("no_matching_online_runner_visible") is True
)
production_workbench_present = bool(production)
production_image_tag_matches_main = (
production.get("production_deploy_image_tag_matches_main") is True
)
production_image_tag_matches_main = _production_image_tag_current(production)
production_governance_fields_present = (
production.get("production_deploy_governance_fields_present") is True
)
@@ -375,6 +387,22 @@ def build_closure_verifier(
"production_deploy_image_tag_matches_main": (
production_image_tag_matches_main
),
"production_deploy_runtime_build_matches_committed_source_control_readback": (
production.get(
"production_deploy_runtime_build_matches_committed_source_control_readback"
)
is True
),
"production_deploy_runtime_build_matches_committed_production_image_tag": (
production.get(
"production_deploy_runtime_build_matches_committed_production_image_tag"
)
is True
),
"production_deploy_runtime_build_readback_status": str(
production.get("production_deploy_runtime_build_readback_status")
or ""
),
"production_deploy_governance_fields_present": (
production_governance_fields_present
),