feat(runner): use deploy snapshot for closure fallback
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
**完成內容**:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 file,verifier 只會退回 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`。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user