feat(runner): use deploy snapshot for closure fallback

This commit is contained in:
Your Name
2026-06-29 10:48:19 +08:00
parent 7191193c71
commit 5e3e5c7aea
6 changed files with 158 additions and 5 deletions

View File

@@ -212,7 +212,7 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary():
assert lanes["production_deploy"]["blocker_count"] == 4
assert lanes["production_deploy"]["metric"][
"observed_source_control_main_short_sha"
] == "76071f21a811"
] == "7191193c71e"
assert lanes["production_deploy"]["metric"][
"production_image_tag_short_sha"
] == "af45811e87"

View File

@@ -1,3 +1,12 @@
## 2026-06-29 — 10:44 non-110 CD closure verifier snapshot fallback
**完成內容**
- `ops/runner/verify-awoooi-non110-cd-closure.py` 在未提供 fresh readiness file 時,會退回 committed production deploy snapshot 內的 sanitized non-110 readiness 欄位,輸出 `non110_runner_readiness_source=committed_production_deploy_snapshot`
- 補測試覆蓋缺 readiness file 但有 deploy snapshot 時的 fail-closed 結果;狀態會是 `blocked_non110_runner_not_ready`,不再只停在 `blocked_missing_non110_readiness_readback`
- 更新 production deploy snapshot / README將 verifier command 明確包含 `--production-deploy-snapshot-json-file`;刷新 source readback 觀測 SHA 為 `7191193c71e`
**邊界**:只改 committed source / snapshot / tests / docs未使用 GitHub未讀 token / `.runner` 內容 / cookie / session / secret未操作 host / Docker / K8s / runner service未 workflow_dispatch。snapshot fallback 只代表 source-level fail-closed evidence不代表 live host proof。
## 2026-06-29 — 10:31 non-110 CD closure verifier contract
**完成內容**

View File

@@ -1,12 +1,12 @@
{
"schema_version": "awoooi_production_deploy_readback_blocker_v1",
"generated_at": "2026-06-29T10:31:17+08:00",
"generated_at": "2026-06-29T10:44:45+08:00",
"status": "blocked_waiting_authorized_gitea_workflow_dispatch_and_runner_queue",
"priority": "P0",
"scope": "awoooi_production_truth",
"readback": {
"observed_source_control_main_sha": "76071f21a811fe3c28998fa384e5beacd412c2f7",
"observed_source_control_main_short_sha": "76071f21a811",
"observed_source_control_main_sha": "7191193c71e5c82d6ae703a9c530d676862aa178",
"observed_source_control_main_short_sha": "7191193c71e",
"governance_closure_merge_sha": "27b96f0450d0e3ca6651d6b5f274a341dd727ef2",
"governance_closure_commit_sha": "9e3e7fbb6ba3ffd324b45abf3ad1e7b6ec826b22",
"production_image_tag_sha": "af45811e876fda322ee63c036fbc39c9f07ffd76",
@@ -30,7 +30,7 @@
"public_actions_queue_readback_schema_version": "awoooi_public_gitea_actions_queue_readback_v1",
"public_actions_queue_readback_verifier": "ops/runner/read-public-gitea-actions-queue.py --json",
"non110_runner_cd_closure_verifier_schema_version": "awoooi_non110_cd_closure_verifier_v1",
"non110_runner_cd_closure_verifier": "ops/runner/verify-awoooi-non110-cd-closure.py --readiness-file <sanitized-non110-readiness.txt> --queue-json-file <public-actions-queue.json> --production-workbench-json-file <delivery-closure-workbench.json> --json",
"non110_runner_cd_closure_verifier": "ops/runner/verify-awoooi-non110-cd-closure.py --production-deploy-snapshot-json-file docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json --readiness-file <sanitized-non110-readiness.txt> --queue-json-file <public-actions-queue.json> --production-workbench-json-file <delivery-closure-workbench.json> --json",
"non110_runner_cd_closure_status": "blocked_non110_runner_not_ready",
"non110_runner_cd_closure_required": true,
"current_main_cd_run_visible": false,

View File

@@ -516,6 +516,7 @@ sanitized evidence 彙整 verifier
```bash
python3 ops/runner/verify-awoooi-non110-cd-closure.py \
--production-deploy-snapshot-json-file docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json \
--readiness-file /path/to/sanitized-non110-readiness.txt \
--queue-json-file /path/to/public-actions-queue.json \
--production-workbench-json-file /path/to/delivery-closure-workbench.json \
@@ -525,6 +526,10 @@ python3 ops/runner/verify-awoooi-non110-cd-closure.py \
此 verifier 只接受 `check-awoooi-non110-runner-readiness.sh` 的非 secret 輸出、
public queue JSON 與 production Delivery Workbench JSON/API它不讀 `.runner`
內容、不讀 token、不 workflow_dispatch、不操作 host / Docker / K8s / runner service。
若沒有傳入 fresh readiness fileverifier 只會退回 committed production deploy
snapshot 內的 sanitized non-110 readiness 欄位,並輸出
`non110_runner_readiness_source=committed_production_deploy_snapshot`;這只代表
source-level fail-closed evidence不是 live host proof。
只有 `AWOOOI_NON110_RUNNER_READY=1`、public queue 不再顯示
`No matching online runner`、production image tag 已跟 main 對齊且 governance fields
已出現時,才會輸出 `closure_verified`

View File

@@ -57,6 +57,26 @@ def _workbench(*, image_current: bool, governance_ready: bool) -> dict:
}
def _deploy_snapshot() -> dict:
return {
"schema_version": "awoooi_production_deploy_readback_blocker_v1",
"readback": {
"non110_runner_ready": False,
"non110_runner_ready_config_count": 1,
"non110_runner_ready_binary_count": 1,
"non110_runner_ready_registration_count": 0,
"non110_runner_ready_service_count": 1,
"non110_runner_ready_active_service_count": 0,
"non110_runner_ready_autostart_path_count": 1,
"non110_runner_remaining_blockers": [
"runner_registration_missing",
"runner_service_not_active",
],
"non110_runner_safe_next_step": "run_register_awoooi_non110_runner_script_without_printing_token_then_autostart_path_will_enable_service_and_rerun_this_verifier",
},
}
def _readiness(*, ready: bool) -> str:
if ready:
return "\n".join(
@@ -116,6 +136,32 @@ 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_uses_deploy_snapshot_when_readiness_file_missing() -> None:
module = _load_module()
payload = module.build_closure_verifier(
readiness_text="",
queue=_queue(no_matching=True),
production_workbench=_workbench(image_current=False, governance_ready=False),
production_deploy_snapshot=_deploy_snapshot(),
)
assert payload["status"] == "blocked_non110_runner_not_ready"
assert payload["readback"]["non110_runner_readiness_source"] == (
"committed_production_deploy_snapshot"
)
assert payload["readback"]["non110_runner_ready_config_count"] == 1
assert payload["readback"]["non110_runner_ready_registration_count"] == 0
assert payload["runner_readiness_blockers"] == [
"runner_registration_missing",
"runner_service_not_active",
]
assert (
payload["operation_boundaries"][
"committed_production_deploy_snapshot_read"
]
is True
)
def test_closure_verifier_accepts_full_closure_evidence() -> None:
module = _load_module()
payload = module.build_closure_verifier(
@@ -162,3 +208,41 @@ def test_cli_uses_fixture_files_without_live_dispatch(tmp_path: Path) -> None:
assert payload["status"] == "blocked_non110_runner_not_ready"
assert payload["operation_boundaries"]["workflow_dispatch_performed"] is False
assert payload["operation_boundaries"]["github_api_used"] is False
def test_cli_uses_deploy_snapshot_without_readiness_file(tmp_path: Path) -> None:
queue_path = tmp_path / "queue.json"
snapshot_path = tmp_path / "deploy-snapshot.json"
workbench_path = tmp_path / "workbench.json"
queue_path.write_text(json.dumps(_queue(no_matching=True)), encoding="utf-8")
snapshot_path.write_text(json.dumps(_deploy_snapshot()), encoding="utf-8")
workbench_path.write_text(
json.dumps(_workbench(image_current=False, governance_ready=False)),
encoding="utf-8",
)
result = subprocess.run(
[
sys.executable,
str(SCRIPT),
"--production-deploy-snapshot-json-file",
str(snapshot_path),
"--queue-json-file",
str(queue_path),
"--production-workbench-json-file",
str(workbench_path),
"--json",
],
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert result.returncode == 1
payload = json.loads(result.stdout)
assert payload["status"] == "blocked_non110_runner_not_ready"
assert payload["readback"]["non110_runner_readiness_source"] == (
"committed_production_deploy_snapshot"
)
assert payload["operation_boundaries"]["raw_runner_registration_read"] is False

View File

@@ -13,6 +13,9 @@ from typing import Any
SCHEMA_VERSION = "awoooi_non110_cd_closure_verifier_v1"
ROOT = Path(__file__).resolve().parents[2]
PUBLIC_QUEUE_READER = ROOT / "ops/runner/read-public-gitea-actions-queue.py"
DEFAULT_PRODUCTION_DEPLOY_SNAPSHOT = (
ROOT / "docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json"
)
DEFAULT_PRODUCTION_WORKBENCH_URL = (
"https://awoooi.wooo.work/api/v1/agents/delivery-closure-workbench"
)
@@ -70,6 +73,12 @@ def _int(value: Any) -> int:
return 0
def _strings(value: Any) -> list[str]:
if not isinstance(value, list):
return []
return [str(item) for item in value if str(item)]
def parse_readiness_text(text: str) -> dict[str, Any]:
key_values: dict[str, str] = {}
blockers: list[str] = []
@@ -91,6 +100,7 @@ def parse_readiness_text(text: str) -> dict[str, Any]:
return {
"provided": bool(text.strip()),
"source": "sanitized_readiness_text" if text.strip() else "",
"ready": key_values.get("AWOOOI_NON110_RUNNER_READY") == "1",
"blockers": blockers,
"warnings": warnings,
@@ -114,6 +124,34 @@ def parse_readiness_text(text: str) -> dict[str, Any]:
}
def readiness_from_production_deploy_snapshot(snapshot: dict[str, Any]) -> dict[str, Any]:
readback = snapshot.get("readback") if isinstance(snapshot.get("readback"), dict) else {}
if "non110_runner_ready" not in readback:
return parse_readiness_text("")
return {
"provided": True,
"source": "committed_production_deploy_snapshot",
"ready": readback.get("non110_runner_ready") is True,
"blockers": _strings(readback.get("non110_runner_remaining_blockers")),
"warnings": [],
"ready_config_count": _int(readback.get("non110_runner_ready_config_count")),
"ready_binary_count": _int(readback.get("non110_runner_ready_binary_count")),
"ready_registration_count": _int(
readback.get("non110_runner_ready_registration_count")
),
"ready_service_count": _int(readback.get("non110_runner_ready_service_count")),
"ready_active_service_count": _int(
readback.get("non110_runner_ready_active_service_count")
),
"ready_autostart_path_count": _int(
readback.get("non110_runner_ready_autostart_path_count")
),
"raw_runner_registration_read": False,
"safe_next_step": str(readback.get("non110_runner_safe_next_step") or ""),
}
def _production_summary(workbench: dict[str, Any]) -> dict[str, Any]:
summary = workbench.get("summary")
return summary if isinstance(summary, dict) else {}
@@ -124,8 +162,13 @@ def build_closure_verifier(
readiness_text: str,
queue: dict[str, Any],
production_workbench: dict[str, Any],
production_deploy_snapshot: dict[str, Any] | None = None,
) -> dict[str, Any]:
readiness = parse_readiness_text(readiness_text)
if not readiness["provided"] and production_deploy_snapshot:
readiness = readiness_from_production_deploy_snapshot(
production_deploy_snapshot
)
queue_readback = queue.get("readback") if isinstance(queue.get("readback"), dict) else {}
queue_boundaries = (
queue.get("operation_boundaries")
@@ -183,6 +226,7 @@ def build_closure_verifier(
"status": status,
"readback": {
"non110_runner_ready": readiness["ready"],
"non110_runner_readiness_source": readiness["source"],
"non110_runner_ready_config_count": readiness["ready_config_count"],
"non110_runner_ready_binary_count": readiness["ready_binary_count"],
"non110_runner_ready_registration_count": readiness[
@@ -243,6 +287,9 @@ def build_closure_verifier(
"operation_boundaries": {
"secret_or_runner_token_read": False,
"raw_runner_registration_read": False,
"committed_production_deploy_snapshot_read": (
readiness["source"] == "committed_production_deploy_snapshot"
),
"workflow_dispatch_performed": False,
"host_write_performed": False,
"gitea_api_write_performed": False,
@@ -262,6 +309,10 @@ def main() -> int:
description="Verify sanitized AWOOOI non-110 CD closure evidence."
)
parser.add_argument("--readiness-file", default="")
parser.add_argument(
"--production-deploy-snapshot-json-file",
default=str(DEFAULT_PRODUCTION_DEPLOY_SNAPSHOT),
)
parser.add_argument("--queue-json-file", default="")
parser.add_argument("--production-workbench-json-file", default="")
parser.add_argument(
@@ -273,6 +324,9 @@ def main() -> int:
args = parser.parse_args()
readiness_text = _read_text(args.readiness_file)
production_deploy_snapshot = _load_json_file(
args.production_deploy_snapshot_json_file
)
queue = (
_load_json_file(args.queue_json_file)
if args.queue_json_file
@@ -289,6 +343,7 @@ def main() -> int:
readiness_text=readiness_text,
queue=queue,
production_workbench=production_workbench,
production_deploy_snapshot=production_deploy_snapshot,
)
if args.json: