feat(runner): expose ordered non110 closure progress [skip ci]
This commit is contained in:
@@ -546,6 +546,11 @@ 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`。
|
||||
verifier 也會輸出 `ordered_steps` 與 `progress`:順序固定為 registration metadata、
|
||||
active service、public queue match、production Workbench readback、production image tag、
|
||||
governance fields。若 live readback 仍缺 `.runner` metadata,`next_blocked_step_id`
|
||||
必須維持 `non110_runner_registration_metadata`,不得跳到 dispatch、image tag 或
|
||||
governance fields。
|
||||
|
||||
`--enable` 只允許在 `AWOOOI_NON110_ENABLE=1`、`act_runner` executable、
|
||||
`config.yaml` present、`.runner` present 且 service 已由 verifier 證明 target / limits
|
||||
|
||||
@@ -118,6 +118,15 @@ def test_closure_verifier_blocks_runner_not_ready_without_secret_leak() -> None:
|
||||
text = json.dumps(payload, sort_keys=True)
|
||||
assert payload["schema_version"] == module.SCHEMA_VERSION
|
||||
assert payload["status"] == "blocked_non110_runner_not_ready"
|
||||
assert payload["progress"]["ordered_step_count"] == 6
|
||||
assert payload["progress"]["ordered_completed_prefix_count"] == 0
|
||||
assert payload["progress"]["next_blocked_step_index"] == 1
|
||||
assert payload["progress"]["next_blocked_step_id"] == (
|
||||
"non110_runner_registration_metadata"
|
||||
)
|
||||
assert payload["ordered_steps"][0]["status"] == "blocked"
|
||||
assert payload["ordered_steps"][0]["evidence_ready"] is False
|
||||
assert payload["ordered_steps"][1]["status"] == "pending"
|
||||
assert "runner_registration_missing" in payload["runner_readiness_blockers"]
|
||||
assert payload["readback"]["non110_runner_ready_registration_count"] == 0
|
||||
assert payload["operation_boundaries"]["secret_or_runner_token_read"] is False
|
||||
@@ -133,6 +142,12 @@ def test_closure_verifier_blocks_queue_after_runner_ready() -> None:
|
||||
production_workbench=_workbench(image_current=False, governance_ready=False),
|
||||
)
|
||||
assert payload["status"] == "blocked_no_matching_online_runner"
|
||||
assert payload["progress"]["ordered_completed_prefix_count"] == 2
|
||||
assert payload["progress"]["next_blocked_step_index"] == 3
|
||||
assert payload["progress"]["next_blocked_step_id"] == "public_queue_runner_match"
|
||||
assert payload["ordered_steps"][0]["status"] == "complete"
|
||||
assert payload["ordered_steps"][1]["status"] == "complete"
|
||||
assert payload["ordered_steps"][2]["status"] == "blocked"
|
||||
assert "public_queue_still_has_no_matching_online_runner" in payload["blockers"]
|
||||
|
||||
|
||||
@@ -170,6 +185,10 @@ def test_closure_verifier_accepts_full_closure_evidence() -> None:
|
||||
production_workbench=_workbench(image_current=True, governance_ready=True),
|
||||
)
|
||||
assert payload["status"] == "closure_verified"
|
||||
assert payload["progress"]["ordered_completed_prefix_count"] == 6
|
||||
assert payload["progress"]["ordered_completion_percent"] == 100
|
||||
assert payload["progress"]["next_blocked_step_id"] == ""
|
||||
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
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ DEFAULT_PRODUCTION_DEPLOY_SNAPSHOT = (
|
||||
DEFAULT_PRODUCTION_WORKBENCH_URL = (
|
||||
"https://awoooi.wooo.work/api/v1/agents/delivery-closure-workbench"
|
||||
)
|
||||
ORDERED_STEP_COUNT = 6
|
||||
|
||||
|
||||
def _read_text(path: str | None) -> str:
|
||||
@@ -157,6 +158,116 @@ def _production_summary(workbench: dict[str, Any]) -> dict[str, Any]:
|
||||
return summary if isinstance(summary, dict) else {}
|
||||
|
||||
|
||||
def _build_ordered_steps(
|
||||
*,
|
||||
readiness: dict[str, Any],
|
||||
no_matching_runner_visible: bool,
|
||||
production_workbench_present: bool,
|
||||
production_image_tag_matches_main: bool,
|
||||
production_governance_fields_present: bool,
|
||||
) -> list[dict[str, Any]]:
|
||||
registration_ready = (
|
||||
readiness["provided"]
|
||||
and readiness["ready_registration_count"] > 0
|
||||
and not readiness["raw_runner_registration_read"]
|
||||
)
|
||||
active_service_ready = (
|
||||
readiness["provided"]
|
||||
and readiness["ready_active_service_count"] > 0
|
||||
and not readiness["raw_runner_registration_read"]
|
||||
)
|
||||
definitions = [
|
||||
{
|
||||
"id": "non110_runner_registration_metadata",
|
||||
"title": "non-110 runner registration metadata present",
|
||||
"evidence_ready": registration_ready,
|
||||
"next_action": readiness["safe_next_step"]
|
||||
or "rerun_non110_runner_readiness_verifier",
|
||||
},
|
||||
{
|
||||
"id": "non110_runner_active_service",
|
||||
"title": "non-110 runner service active",
|
||||
"evidence_ready": active_service_ready,
|
||||
"next_action": "wait_for_autostart_path_or_rerun_non110_runner_readiness_verifier",
|
||||
},
|
||||
{
|
||||
"id": "public_queue_runner_match",
|
||||
"title": "public Gitea queue no longer shows no-matching-runner",
|
||||
"evidence_ready": not no_matching_runner_visible,
|
||||
"next_action": "rerun_public_queue_readback_until_no_matching_runner_is_absent",
|
||||
},
|
||||
{
|
||||
"id": "production_workbench_readback",
|
||||
"title": "production Delivery Workbench readback present",
|
||||
"evidence_ready": production_workbench_present,
|
||||
"next_action": "read_production_delivery_workbench_after_deploy",
|
||||
},
|
||||
{
|
||||
"id": "production_image_tag_current",
|
||||
"title": "production image tag matches Gitea main",
|
||||
"evidence_ready": production_image_tag_matches_main,
|
||||
"next_action": "complete_authorized_cd_then_verify_image_tag_matches_main",
|
||||
},
|
||||
{
|
||||
"id": "production_governance_fields_present",
|
||||
"title": "production governance fields present after deploy",
|
||||
"evidence_ready": production_governance_fields_present,
|
||||
"next_action": "verify_internal_governance_writeback_fields_after_deploy",
|
||||
},
|
||||
]
|
||||
first_blocked_index = next(
|
||||
(
|
||||
index
|
||||
for index, step in enumerate(definitions)
|
||||
if step["evidence_ready"] is not True
|
||||
),
|
||||
None,
|
||||
)
|
||||
steps: list[dict[str, Any]] = []
|
||||
for index, step in enumerate(definitions):
|
||||
if step["evidence_ready"] is True:
|
||||
status = "complete"
|
||||
elif index == first_blocked_index:
|
||||
status = "blocked"
|
||||
else:
|
||||
status = "pending"
|
||||
steps.append(
|
||||
{
|
||||
"index": index + 1,
|
||||
"id": step["id"],
|
||||
"title": step["title"],
|
||||
"status": status,
|
||||
"evidence_ready": step["evidence_ready"] is True,
|
||||
"next_action": step["next_action"] if status == "blocked" else "",
|
||||
}
|
||||
)
|
||||
return steps
|
||||
|
||||
|
||||
def _progress_from_steps(steps: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
evidence_completed = sum(1 for step in steps if step["evidence_ready"] is True)
|
||||
prefix_completed = 0
|
||||
for step in steps:
|
||||
if step["status"] != "complete":
|
||||
break
|
||||
prefix_completed += 1
|
||||
next_blocked = next((step for step in steps if step["status"] == "blocked"), {})
|
||||
return {
|
||||
"ordered_step_count": len(steps),
|
||||
"ordered_completed_prefix_count": prefix_completed,
|
||||
"evidence_completed_step_count": evidence_completed,
|
||||
"ordered_completion_percent": round(
|
||||
(prefix_completed / max(len(steps), 1)) * 100
|
||||
),
|
||||
"evidence_completion_percent": round(
|
||||
(evidence_completed / max(len(steps), 1)) * 100
|
||||
),
|
||||
"next_blocked_step_index": _int(next_blocked.get("index")),
|
||||
"next_blocked_step_id": str(next_blocked.get("id") or ""),
|
||||
"next_blocked_step_action": str(next_blocked.get("next_action") or ""),
|
||||
}
|
||||
|
||||
|
||||
def build_closure_verifier(
|
||||
*,
|
||||
readiness_text: str,
|
||||
@@ -221,9 +332,19 @@ def build_closure_verifier(
|
||||
else:
|
||||
status = "closure_verified"
|
||||
|
||||
ordered_steps = _build_ordered_steps(
|
||||
readiness=readiness,
|
||||
no_matching_runner_visible=no_matching_runner_visible,
|
||||
production_workbench_present=production_workbench_present,
|
||||
production_image_tag_matches_main=production_image_tag_matches_main,
|
||||
production_governance_fields_present=production_governance_fields_present,
|
||||
)
|
||||
progress = _progress_from_steps(ordered_steps)
|
||||
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"readback": {
|
||||
"non110_runner_ready": readiness["ready"],
|
||||
"non110_runner_readiness_source": readiness["source"],
|
||||
@@ -259,6 +380,7 @@ def build_closure_verifier(
|
||||
),
|
||||
},
|
||||
"blockers": blockers,
|
||||
"ordered_steps": ordered_steps,
|
||||
"runner_readiness_blockers": readiness["blockers"],
|
||||
"runner_readiness_warnings": readiness["warnings"],
|
||||
"next_actions": [
|
||||
|
||||
Reference in New Issue
Block a user