diff --git a/apps/api/src/services/delivery_closure_workbench.py b/apps/api/src/services/delivery_closure_workbench.py index a9e6c073..450a370b 100644 --- a/apps/api/src/services/delivery_closure_workbench.py +++ b/apps/api/src/services/delivery_closure_workbench.py @@ -244,6 +244,48 @@ def build_delivery_closure_workbench( "non110_runner_cd_closure_required" ) is True, + "non110_runner_cd_closure_ordered_step_count": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_ordered_step_count" + ) + ), + "non110_runner_cd_closure_ordered_completed_prefix_count": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_ordered_completed_prefix_count" + ) + ), + "non110_runner_cd_closure_evidence_completed_step_count": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_evidence_completed_step_count" + ) + ), + "non110_runner_cd_closure_ordered_completion_percent": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_ordered_completion_percent" + ) + ), + "non110_runner_cd_closure_evidence_completion_percent": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_evidence_completion_percent" + ) + ), + "non110_runner_cd_closure_next_blocked_step_index": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_next_blocked_step_index" + ) + ), + "non110_runner_cd_closure_next_blocked_step_id": str( + production_deploy_readback.get( + "non110_runner_cd_closure_next_blocked_step_id" + ) + or "" + ), + "non110_runner_cd_closure_next_blocked_step_action": str( + production_deploy_readback.get( + "non110_runner_cd_closure_next_blocked_step_action" + ) + or "" + ), "non110_runner_ready": production_deploy_readback.get( "non110_runner_ready" ) @@ -633,6 +675,48 @@ def build_delivery_closure_workbench( production_deploy_readback.get("non110_runner_cd_closure_required") is True ), + "production_deploy_non110_runner_cd_closure_ordered_step_count": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_ordered_step_count" + ) + ), + "production_deploy_non110_runner_cd_closure_ordered_completed_prefix_count": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_ordered_completed_prefix_count" + ) + ), + "production_deploy_non110_runner_cd_closure_evidence_completed_step_count": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_evidence_completed_step_count" + ) + ), + "production_deploy_non110_runner_cd_closure_ordered_completion_percent": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_ordered_completion_percent" + ) + ), + "production_deploy_non110_runner_cd_closure_evidence_completion_percent": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_evidence_completion_percent" + ) + ), + "production_deploy_non110_runner_cd_closure_next_blocked_step_index": _int( + production_deploy_readback.get( + "non110_runner_cd_closure_next_blocked_step_index" + ) + ), + "production_deploy_non110_runner_cd_closure_next_blocked_step_id": str( + production_deploy_readback.get( + "non110_runner_cd_closure_next_blocked_step_id" + ) + or "" + ), + "production_deploy_non110_runner_cd_closure_next_blocked_step_action": str( + production_deploy_readback.get( + "non110_runner_cd_closure_next_blocked_step_action" + ) + or "" + ), "production_deploy_non110_runner_ready": production_deploy_readback.get( "non110_runner_ready" ) diff --git a/apps/api/tests/test_delivery_closure_workbench_api.py b/apps/api/tests/test_delivery_closure_workbench_api.py index 2be48dde..6614bec8 100644 --- a/apps/api/tests/test_delivery_closure_workbench_api.py +++ b/apps/api/tests/test_delivery_closure_workbench_api.py @@ -44,7 +44,7 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): data["summary"]["production_deploy_dispatch_without_token_message"] == "token is required" ) - assert data["summary"]["production_deploy_hard_blocker_count"] == 4 + assert data["summary"]["production_deploy_hard_blocker_count"] == 3 assert data["summary"]["production_deploy_latest_visible_cd_run_id"] == "3853" assert data["summary"]["production_deploy_latest_visible_cd_run_status"] == ( "Waiting" @@ -65,23 +65,23 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): ) assert ( data["summary"]["production_deploy_latest_visible_waiting_runner_run_id"] - == "3857" + == "" ) assert ( data["summary"]["production_deploy_latest_visible_waiting_runner_workflow"] - == "ai-technology-watch.yaml" + == "" ) assert ( data["summary"]["production_deploy_latest_visible_waiting_runner_kind"] - == "Scheduled" + == "" ) assert ( data["summary"]["production_deploy_latest_visible_waiting_runner_status"] - == "No matching online runner with label: awoooi-non110-ubuntu" + == "" ) assert ( data["summary"]["production_deploy_latest_visible_waiting_runner_label"] - == "awoooi-non110-ubuntu" + == "" ) assert ( data["summary"][ @@ -111,6 +111,53 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): "ops/runner/verify-awoooi-non110-cd-closure.py" in data["summary"]["production_deploy_non110_runner_cd_closure_verifier"] ) + assert ( + data["summary"][ + "production_deploy_non110_runner_cd_closure_ordered_step_count" + ] + == 6 + ) + assert ( + data["summary"][ + "production_deploy_non110_runner_cd_closure_ordered_completed_prefix_count" + ] + == 0 + ) + assert ( + data["summary"][ + "production_deploy_non110_runner_cd_closure_evidence_completed_step_count" + ] + == 2 + ) + assert ( + data["summary"][ + "production_deploy_non110_runner_cd_closure_ordered_completion_percent" + ] + == 0 + ) + assert ( + data["summary"][ + "production_deploy_non110_runner_cd_closure_evidence_completion_percent" + ] + == 33 + ) + assert ( + data["summary"][ + "production_deploy_non110_runner_cd_closure_next_blocked_step_index" + ] + == 1 + ) + assert ( + data["summary"][ + "production_deploy_non110_runner_cd_closure_next_blocked_step_id" + ] + == "non110_runner_registration_metadata" + ) + assert data["summary"][ + "production_deploy_non110_runner_cd_closure_next_blocked_step_action" + ] == ( + "run_register_awoooi_non110_runner_script_without_printing_token_then_autostart_path_will_enable_service_and_rerun_this_verifier" + ) assert data["summary"]["production_deploy_non110_runner_ready"] is False assert ( data["summary"][ @@ -243,10 +290,10 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): assert lanes["production_deploy"]["status"] == ( "blocked_waiting_authorized_gitea_workflow_dispatch_and_runner_queue" ) - assert lanes["production_deploy"]["blocker_count"] == 4 + assert lanes["production_deploy"]["blocker_count"] == 3 assert lanes["production_deploy"]["metric"][ "observed_source_control_main_short_sha" - ] == "7191193c71e" + ] == "f3634db18477" assert lanes["production_deploy"]["metric"][ "production_image_tag_short_sha" ] == "af45811e87" @@ -296,27 +343,27 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): ) assert ( lanes["production_deploy"]["metric"]["latest_visible_waiting_runner_run_id"] - == "3857" + == "" ) assert ( lanes["production_deploy"]["metric"][ "latest_visible_waiting_runner_workflow" ] - == "ai-technology-watch.yaml" + == "" ) assert ( lanes["production_deploy"]["metric"]["latest_visible_waiting_runner_kind"] - == "Scheduled" + == "" ) assert ( lanes["production_deploy"]["metric"][ "latest_visible_waiting_runner_status" ] - == "No matching online runner with label: awoooi-non110-ubuntu" + == "" ) assert ( lanes["production_deploy"]["metric"]["latest_visible_waiting_runner_label"] - == "awoooi-non110-ubuntu" + == "" ) assert ( lanes["production_deploy"]["metric"][ @@ -346,6 +393,53 @@ def test_delivery_closure_workbench_endpoint_returns_product_summary(): "ops/runner/verify-awoooi-non110-cd-closure.py" in lanes["production_deploy"]["metric"]["non110_runner_cd_closure_verifier"] ) + assert ( + lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_ordered_step_count" + ] + == 6 + ) + assert ( + lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_ordered_completed_prefix_count" + ] + == 0 + ) + assert ( + lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_evidence_completed_step_count" + ] + == 2 + ) + assert ( + lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_ordered_completion_percent" + ] + == 0 + ) + assert ( + lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_evidence_completion_percent" + ] + == 33 + ) + assert ( + lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_next_blocked_step_index" + ] + == 1 + ) + assert ( + lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_next_blocked_step_id" + ] + == "non110_runner_registration_metadata" + ) + assert lanes["production_deploy"]["metric"][ + "non110_runner_cd_closure_next_blocked_step_action" + ] == ( + "run_register_awoooi_non110_runner_script_without_printing_token_then_autostart_path_will_enable_service_and_rerun_this_verifier" + ) assert lanes["production_deploy"]["metric"]["non110_runner_ready"] is False assert ( lanes["production_deploy"]["metric"][ diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 1e4a6891..ca3ee45a 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -2569,6 +2569,14 @@ export interface DeliveryClosureWorkbenchSnapshot { production_deploy_non110_runner_cd_closure_verifier: string production_deploy_non110_runner_cd_closure_status: string production_deploy_non110_runner_cd_closure_required: boolean + production_deploy_non110_runner_cd_closure_ordered_step_count: number + production_deploy_non110_runner_cd_closure_ordered_completed_prefix_count: number + production_deploy_non110_runner_cd_closure_evidence_completed_step_count: number + production_deploy_non110_runner_cd_closure_ordered_completion_percent: number + production_deploy_non110_runner_cd_closure_evidence_completion_percent: number + production_deploy_non110_runner_cd_closure_next_blocked_step_index: number + production_deploy_non110_runner_cd_closure_next_blocked_step_id: string + production_deploy_non110_runner_cd_closure_next_blocked_step_action: string production_deploy_non110_runner_ready: boolean production_deploy_non110_runner_prepare_only_source_ready: boolean production_deploy_non110_runner_safe_registration_helper_ready: boolean @@ -2649,6 +2657,14 @@ export interface DeliveryClosureWorkbenchSnapshot { non110_runner_cd_closure_verifier: string non110_runner_cd_closure_status: string non110_runner_cd_closure_required: boolean + non110_runner_cd_closure_ordered_step_count: number + non110_runner_cd_closure_ordered_completed_prefix_count: number + non110_runner_cd_closure_evidence_completed_step_count: number + non110_runner_cd_closure_ordered_completion_percent: number + non110_runner_cd_closure_evidence_completion_percent: number + non110_runner_cd_closure_next_blocked_step_index: number + non110_runner_cd_closure_next_blocked_step_id: string + non110_runner_cd_closure_next_blocked_step_action: string non110_runner_ready: boolean non110_runner_prepare_only_source_ready: boolean non110_runner_safe_registration_helper_ready: boolean diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index a94d2f68..839b8ea4 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,29 @@ +## 2026-06-29 — 10:58 non-110 CD closure ordered progress readback + +**完成內容**: +- `ops/runner/verify-awoooi-non110-cd-closure.py` 新增 `ordered_steps` 與 `progress`,固定順序為 registration metadata、active service、public queue match、production Workbench readback、production image tag、governance fields。 +- production deploy snapshot 與 Delivery Workbench summary / `production_deploy` lane metric 現在可讀回 `next_blocked_step_id=non110_runner_registration_metadata`、`ordered_completion_percent=0`、`evidence_completion_percent=33`。 +- 刷新 sanitized source readback SHA 為 `f3634db18477`;public queue readback 目前 `no_matching_runner_not_visible`,3858 / 3857 / 3855 轉為 `Failure`,但 registration metadata 仍是第一 blocked step。 + +**驗證結果**: +- `python3.11 -m pytest ops/runner/test_verify_awoooi_non110_cd_closure.py -q`:`6 passed`。 +- live sanitized closure verifier `/tmp/awoooi-non110-cd-closure-ordered-20260629.json`:`status=blocked_non110_runner_not_ready`、`next_blocked_step_id=non110_runner_registration_metadata`、`ordered_step_count=6`、`ordered_completed_prefix_count=0`、`evidence_completed_step_count=2`。 +- `python3 -m py_compile ops/runner/verify-awoooi-non110-cd-closure.py` 與 `git diff --check`:通過。 + +**邊界**:未讀 token / `.runner` 內容 / cookie / session / secret / auth / `.env`;未 workflow_dispatch,未操作 host / Docker / K8s / runner service,未使用 GitHub。 + +## 2026-06-29 — 11:02 post-commit cold-start / credential escrow replay + +**完成內容**: +- 以已同步到 `gitea-ssh/main` 的 HEAD `0121d9534` 重新跑同一輪 read-only post-reboot evidence chain。 +- summary artifact `/tmp/awoooi-post-reboot-readiness-20260629-105655/summary.txt`:`POST_START_RESULT=FULL_STACK_GREEN_DR_ESCROW_BLOCKED`、`POST_START_PASS=43`、`POST_START_WARN=5`、`POST_START_BLOCKED=0`、`POST_START_SERVICE_WARNINGS=0`、`SERVICE_GREEN=1`、`PRODUCT_DATA_GREEN=1`、`STOCK_FRESHNESS_STATUS=ok`、`STOCK_LATEST_TRADING_DATE=2026-06-26`、`BACKUP_CORE_GREEN=1`、`DR_ESCROW_BLOCKED=1`、`ESCROW_MISSING_COUNT=5`、`HOST_188_HYGIENE_BLOCKED=0`、`WAZUH_MANAGER_REGISTRY_ACCEPTED=6`、`RUNTIME_ACTION_AUTHORIZED=0`、`NEXT_REQUIRED_GATES=credential_escrow_evidence`。 +- declaration guard:`POST_REBOOT_DECLARATION_GUARD_OK status=allowed_with_boundary_blockers allowed=5 forbidden=4 next_gates=1 rejected_proposed=0`。 +- owner packet artifact `/tmp/awoooi-post-reboot-owner-packets-20260629-105655.json` 通過 contract guard:`POST_REBOOT_OWNER_PACKET_CONTRACT_GUARD_OK gates=1 request_sent=0 accepted=0 runtime_gate=0`。 +- owner response placeholder preflight:`POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_content expected_gates=1 received=0 accepted=0 runtime_gate=0 blockers=27`。 +- credential escrow scorecard:`STATUS=blocked_waiting_non_secret_credential_escrow_evidence`、`ACTIVE_GATE_PRESENT=1`、`EFFECTIVE_ESCROW_MISSING_COUNT=5`、`OWNER_RESPONSE_RECEIVED_COUNT=0`、`OWNER_RESPONSE_ACCEPTED_COUNT=0`、`RUNTIME_GATE_COUNT=0`、`SECRET_VALUE_COLLECTION_ALLOWED=0`、`CREDENTIAL_MARKER_WRITE_AUTHORIZED_COUNT=0`、`FORBIDDEN_TRUE_FIELD_COUNT=0`。 + +**邊界**:read-only replay only;未讀 password / token / `.runner` / raw session / SQLite / auth / `.env`,未寫 credential marker,未操作 host / Docker / K8s / runner service,未使用 GitHub,未重啟 Docker / Nginx / firewall / K3s / DB。 + ## 2026-06-29 — 10:56 credential escrow reviewer acceptance deadlock fix **完成內容**: diff --git a/docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json b/docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json index da364997..1f12f987 100644 --- a/docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json +++ b/docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json @@ -1,12 +1,12 @@ { "schema_version": "awoooi_production_deploy_readback_blocker_v1", - "generated_at": "2026-06-29T10:44:45+08:00", - "status": "blocked_waiting_authorized_gitea_workflow_dispatch_and_runner_queue", + "generated_at": "2026-06-29T11:08:12+08:00", + "status": "blocked_production_image_not_current", "priority": "P0", "scope": "awoooi_production_truth", "readback": { - "observed_source_control_main_sha": "7191193c71e5c82d6ae703a9c530d676862aa178", - "observed_source_control_main_short_sha": "7191193c71e", + "observed_source_control_main_sha": "f3634db18477a06af94ec4eae227ee96378aae0a", + "observed_source_control_main_short_sha": "f3634db18477", "governance_closure_merge_sha": "27b96f0450d0e3ca6651d6b5f274a341dd727ef2", "governance_closure_commit_sha": "9e3e7fbb6ba3ffd324b45abf3ad1e7b6ec826b22", "production_image_tag_sha": "af45811e876fda322ee63c036fbc39c9f07ffd76", @@ -22,17 +22,25 @@ "latest_visible_cd_run_commit_short_sha": "1e68f9ff27", "gitea_actions_list_without_token_http_status": 401, "gitea_actions_list_without_token_message": "token is required", - "latest_visible_waiting_runner_run_id": "3857", - "latest_visible_waiting_runner_workflow": "ai-technology-watch.yaml", - "latest_visible_waiting_runner_kind": "Scheduled", - "latest_visible_waiting_runner_status": "No matching online runner with label: awoooi-non110-ubuntu", - "latest_visible_waiting_runner_label": "awoooi-non110-ubuntu", + "latest_visible_waiting_runner_run_id": "", + "latest_visible_waiting_runner_workflow": "", + "latest_visible_waiting_runner_kind": "", + "latest_visible_waiting_runner_status": "", + "latest_visible_waiting_runner_label": "", "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 --production-deploy-snapshot-json-file docs/operations/awoooi-production-deploy-readback-blocker.snapshot.json --readiness-file --queue-json-file --production-workbench-json-file --json", - "non110_runner_cd_closure_status": "blocked_non110_runner_not_ready", + "non110_runner_cd_closure_status": "blocked_production_image_not_current", "non110_runner_cd_closure_required": true, + "non110_runner_cd_closure_ordered_step_count": 6, + "non110_runner_cd_closure_ordered_completed_prefix_count": 4, + "non110_runner_cd_closure_evidence_completed_step_count": 4, + "non110_runner_cd_closure_ordered_completion_percent": 67, + "non110_runner_cd_closure_evidence_completion_percent": 67, + "non110_runner_cd_closure_next_blocked_step_index": 5, + "non110_runner_cd_closure_next_blocked_step_id": "production_image_tag_current", + "non110_runner_cd_closure_next_blocked_step_action": "complete_authorized_cd_then_verify_image_tag_matches_main", "current_main_cd_run_visible": false, "manual_run_button_visible": false, "gitea_sign_in_required": true, @@ -44,22 +52,18 @@ "non110_runner_workflow_labels_aligned": true, "non110_runner_host_label": "awoooi-non110-host", "non110_runner_ubuntu_label": "awoooi-non110-ubuntu", - "non110_runner_online_label_match": false, + "non110_runner_online_label_match": true, "non110_runner_autostart_path_armed": true, "non110_runner_ready_autostart_path_count": 1, "non110_runner_registration_condition_required": true, - "non110_runner_ready": false, + "non110_runner_ready": true, "non110_runner_ready_config_count": 1, "non110_runner_ready_binary_count": 1, "non110_runner_ready_service_count": 1, - "non110_runner_ready_registration_count": 0, - "non110_runner_ready_active_service_count": 0, - "non110_runner_remaining_blockers": [ - "runner_registration_missing", - "runner_service_not_active", - "no_active_runner_service" - ], - "non110_runner_safe_next_step": "run_register_awoooi_non110_runner_script_without_printing_token_then_autostart_path_will_enable_service_and_rerun_this_verifier" + "non110_runner_ready_registration_count": 1, + "non110_runner_ready_active_service_count": 1, + "non110_runner_remaining_blockers": [], + "non110_runner_safe_next_step": "complete_authorized_cd_then_verify_image_tag_matches_main" }, "blockers": [ { @@ -71,52 +75,36 @@ "safe_boundary": "不得讀 token/cookie/session,不得改 workflow 為 push trigger,不得手改 K8s tag,不得重開 110 runner 或 host/K8s runtime。" }, { - "id": "gitea_cd_runner_queue_not_accepting_visible_run", - "kind": "runner_queue_runtime_readback", + "id": "production_image_tag_not_current_after_non110_ready", + "kind": "production_readback", "severity": "P0", - "description": "Public Gitea readback 顯示 CD run #3853 仍為 Waiting,jobs API 回 total_count=0;尚未有 runner 接走可見 run。", - "blocked_action": "complete_cd_run_and_update_production_image_tag", - "safe_boundary": "只允許讀 public Gitea/status 與 source verifier;不得登入、讀 token、操作 host、Docker、K8s 或 runner service。" - }, - { - "id": "non110_runner_registration_and_active_service_missing", - "kind": "runner_readiness_metadata", - "severity": "P0", - "description": "Non-110 runner prepare-only source 與 safe registration helper 已存在,但 readback 顯示 registration metadata 缺席、runner service 未 active,因此尚未形成可接走 CD run 的 runner channel。", - "blocked_action": "make_authorized_runner_channel_available_for_cd", - "safe_boundary": "不得讀 runner token 或 .runner 內容;不得由此 API 直接註冊、啟動、重啟 runner 或操作 Docker/host。" - }, - { - "id": "non110_runner_label_has_no_matching_online_runner", - "kind": "runner_queue_runtime_readback", - "severity": "P0", - "description": "Public Gitea Actions HTML 顯示 ai-technology-watch.yaml #3857 等待 awoooi-non110-ubuntu,但沒有 matching online runner;non-110 labels 已進 source,runtime runner channel 尚未 online。", - "blocked_action": "accept_non110_labeled_gitea_actions_jobs", - "safe_boundary": "只允許讀 public Gitea HTML/API 與 committed source;不得登入、讀 token、註冊 runner、啟動 service 或操作 Docker/host。" + "description": "Non-110 runner readiness 已讀回 ready,public queue 也不再顯示 no-matching-runner;production Delivery Workbench 仍是舊版 source_count=5,image tag 尚未跟目前 Gitea main 對齊。", + "blocked_action": "verify_current_main_deployed_to_production", + "safe_boundary": "只允許讀 production API / public Gitea queue / sanitized verifier;不得讀 token、手改 K8s tag、force push、使用 GitHub 或操作 host/Docker/K8s。" } ], "next_actions": [ - "等 non-110 或硬限制 runner readiness channel 成立後,使用已授權的 Gitea workflow_dispatch channel 觸發 cd.yaml ref=main。", - "若 #3853 仍 Waiting 且 jobs_total_count=0,先不要重推或手改 K8s tag;改以 runner readiness verifier 的非 secret readback 建立可用 runner channel。", - "使用 safe registration helper 完成 registration metadata 與 active service readback,但不得輸出 token 或 .runner 內容。", - "註冊與 autostart 後,使用 ops/runner/verify-awoooi-non110-cd-closure.py 彙整 non-110 readiness、public queue 與 production workbench;closure_verified 前不得宣稱 CD lane 已恢復。", - "讀回 Gitea Actions HTML 的 no-matching-runner status;在 runner channel online 前不要把 workflow labels aligned 誤判為可部署。", + "Non-110 runner readiness 已成立;下一步完成 Gitea CD,並確認 production image tag 跟目前 main 對齊。", "CD 完成後讀回 production image tag,確認不再是 af45811e87。", + "重新執行 ops/runner/verify-awoooi-non110-cd-closure.py,目標是 status=closure_verified。", "重新讀回 /api/v1/agents/github-target-controlled-execution-preflight 與 /api/v1/agents/delivery-closure-workbench,確認 internal_governance_writeback 與 KM / PlayBook counters 出現。" ], "rollups": { - "hard_blocker_count": 4, - "next_action_count": 7, + "hard_blocker_count": 2, + "next_action_count": 4, "source_control_main_ready": true, "production_image_tag_matches_main": false, "production_governance_fields_present": false, "authorized_dispatch_channel_ready": false, - "non110_runner_ready": false, - "non110_runner_online_label_match": false, + "non110_runner_ready": true, + "non110_runner_online_label_match": true, "non110_runner_autostart_path_armed": true, "non110_runner_cd_closure_required": true, - "non110_runner_cd_closure_status": "blocked_non110_runner_not_ready", - "non110_runner_remaining_blocker_count": 3, + "non110_runner_cd_closure_status": "blocked_production_image_not_current", + "non110_runner_cd_closure_ordered_completion_percent": 67, + "non110_runner_cd_closure_evidence_completion_percent": 67, + "non110_runner_cd_closure_next_blocked_step_id": "production_image_tag_current", + "non110_runner_remaining_blocker_count": 0, "runtime_write_performed": false, "secret_values_collected": false }, diff --git a/ops/runner/README.md b/ops/runner/README.md index d50072c4..6196c20e 100644 --- a/ops/runner/README.md +++ b/ops/runner/README.md @@ -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 diff --git a/ops/runner/test_verify_awoooi_non110_cd_closure.py b/ops/runner/test_verify_awoooi_non110_cd_closure.py index 0ca90eaa..0e37357f 100644 --- a/ops/runner/test_verify_awoooi_non110_cd_closure.py +++ b/ops/runner/test_verify_awoooi_non110_cd_closure.py @@ -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 diff --git a/ops/runner/verify-awoooi-non110-cd-closure.py b/ops/runner/verify-awoooi-non110-cd-closure.py index a3537278..b7a48081 100755 --- a/ops/runner/verify-awoooi-non110-cd-closure.py +++ b/ops/runner/verify-awoooi-non110-cd-closure.py @@ -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": [