feat(runner): use deploy snapshot for closure fallback
This commit is contained in:
@@ -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