feat(awooop): expose ansible runtime readiness
Some checks failed
CD Pipeline / tests (push) Failing after 51s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Code Review / ai-code-review (push) Successful in 12s

This commit is contained in:
Your Name
2026-05-24 15:01:51 +08:00
parent 4874f2b649
commit 1322216f73
6 changed files with 79 additions and 2 deletions

View File

@@ -70,6 +70,10 @@ COPY --chown=appuser:appuser apps/api/models.json ./models.json
COPY --chown=appuser:appuser apps/api/alert_rules.yaml ./alert_rules.yaml
# 2026-04-10 Claude Sonnet 4.6: drift_detector 需要 k8s/ YAML 做 Git state 比對
COPY --chown=appuser:appuser k8s/ ./k8s/
# 2026-05-24 Codex: truth-chain / Ansible readiness needs the repo-known
# playbook catalog in the API image. This does not install ansible-core or
# enable apply; it only lets operators see whether check-mode can be wired.
COPY --chown=appuser:appuser infra/ansible/ ./infra/ansible/
# 2026-04-10 Claude Sonnet 4.6: RAG 知識庫索引來源 (ADR-067 Phase 33)
COPY --chown=appuser:appuser docs/ ./docs/
COPY --chown=appuser:appuser .agents/skills/ ./.agents/skills/

View File

@@ -9,8 +9,10 @@ from __future__ import annotations
import asyncio
import json
import shutil
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal
from pathlib import Path
from typing import Any
from uuid import UUID
@@ -705,6 +707,41 @@ def _execution_backend_summary(records: list[dict[str, Any]]) -> dict[str, Any]:
return summary
def _ansible_runtime_readiness() -> dict[str, Any]:
playbook_roots = [
Path("/app/infra/ansible"),
Path.cwd() / "infra" / "ansible",
]
playbook_root = next((path for path in playbook_roots if path.exists()), None)
playbook_paths = (
sorted((playbook_root / "playbooks").glob("*.yml"))
if playbook_root is not None and (playbook_root / "playbooks").exists()
else []
)
inventory_path = playbook_root / "inventory" / "hosts.yml" if playbook_root is not None else None
binary_path = shutil.which("ansible-playbook")
blockers: list[str] = []
if not binary_path:
blockers.append("ansible_playbook_binary_missing")
if playbook_root is None:
blockers.append("ansible_playbook_catalog_missing")
if inventory_path is None or not inventory_path.exists():
blockers.append("ansible_inventory_missing")
if not playbook_paths:
blockers.append("ansible_playbooks_missing")
return {
"ansible_playbook_binary_present": bool(binary_path),
"ansible_playbook_binary_path": binary_path,
"playbook_root_present": playbook_root is not None,
"playbook_root": str(playbook_root) if playbook_root is not None else None,
"inventory_present": bool(inventory_path and inventory_path.exists()),
"playbook_count": len(playbook_paths),
"can_run_check_mode": not blockers,
"blockers": blockers,
}
def summarize_automation_quality_records(
*,
project_id: str,
@@ -810,6 +847,7 @@ def summarize_automation_quality_records(
"by_verdict": by_verdict,
"gate_failures": failing_gates,
"execution_backend_summary": _execution_backend_summary(records),
"ansible_runtime": _ansible_runtime_readiness(),
"examples": examples[:25],
"production_claim": {
"can_claim_full_auto_repair": evaluated_total > 0 and verified_total == evaluated_total,

View File

@@ -9,6 +9,7 @@ from src.services.awooop_ansible_audit_service import (
build_ansible_truth,
)
from src.services.awooop_truth_chain_service import (
_ansible_runtime_readiness,
_automation_quality_score_bucket,
_clean_row,
_incident_fingerprints,
@@ -687,10 +688,24 @@ def test_automation_quality_summary_denies_full_claim_when_unverified() -> None:
"ansible_rollback_total": 0,
"ansible_pending_check_mode_total": 1,
}
assert summary["ansible_runtime"]["playbook_root_present"] is True
assert summary["ansible_runtime"]["inventory_present"] is True
assert summary["ansible_runtime"]["playbook_count"] >= 1
assert "ansible_playbook_binary_present" in summary["ansible_runtime"]
assert summary["examples"][1]["incident_id"] == "INC-GAP"
assert summary["examples"][1]["score_bucket"] == "yellow"
def test_ansible_runtime_readiness_reports_check_mode_blockers() -> None:
readiness = _ansible_runtime_readiness()
assert readiness["playbook_root_present"] is True
assert readiness["inventory_present"] is True
assert readiness["playbook_count"] >= 1
assert isinstance(readiness["can_run_check_mode"], bool)
assert isinstance(readiness["blockers"], list)
def test_reconciliation_marks_consistent_resolved_execution() -> None:
reconciliation = build_incident_reconciliation(
incident={"incident_id": "INC-2", "status": "RESOLVED"},

View File

@@ -252,7 +252,9 @@
"autoRepair": "Auto repair",
"qualityDetail": "Average {score}, red {red}",
"qualityPending": "Quality summary is still calculating; other evidence is already shown",
"executionBackendDetail": "Execution evidence: operations {operations} (effective {effective} / audit {auditOnly}), auto-repair {autoRepair}; Ansible audit {ansibleRecords}, candidates {ansibleCandidates}, check-mode {checkMode}, apply {apply}, pending wiring {pending}",
"executionBackendDetail": "Execution evidence: operations {operations} (effective {effective} / audit {auditOnly}), auto-repair {autoRepair}; Ansible audit {ansibleRecords}, candidates {ansibleCandidates}, check-mode {checkMode}, apply {apply}, pending wiring {pending}; runtime {runtime}",
"ansibleRuntimeReady": "check-mode ready",
"ansibleRuntimeBlocked": "not ready: {blockers}",
"humanGap": "Human gap",
"humanGapDetail": "{gate} missing {count}",
"humanGapClear": "Quality summary has no top gap",

View File

@@ -253,7 +253,9 @@
"autoRepair": "自動修復",
"qualityDetail": "平均 {score},紅燈 {red}",
"qualityPending": "品質摘要計算中,其他證據已先顯示",
"executionBackendDetail": "執行證據:操作 {operations}(有效 {effective} / 稽核 {auditOnly}),自動修復 {autoRepair}Ansible 稽核 {ansibleRecords},候選 {ansibleCandidates}check-mode {checkMode}apply {apply},待接線 {pending}",
"executionBackendDetail": "執行證據:操作 {operations}(有效 {effective} / 稽核 {auditOnly}),自動修復 {autoRepair}Ansible 稽核 {ansibleRecords},候選 {ansibleCandidates}check-mode {checkMode}apply {apply},待接線 {pending}runtime {runtime}",
"ansibleRuntimeReady": "可跑 check-mode",
"ansibleRuntimeBlocked": "未就緒:{blockers}",
"humanGap": "人工缺口",
"humanGapDetail": "{gate} 缺 {count} 筆",
"humanGapClear": "品質摘要未列出主要缺口",

View File

@@ -51,6 +51,14 @@ interface AutomationQualitySummary {
ansible_apply_total?: number
ansible_pending_check_mode_total?: number
}
ansible_runtime?: {
ansible_playbook_binary_present?: boolean
playbook_root_present?: boolean
inventory_present?: boolean
playbook_count?: number
can_run_check_mode?: boolean
blockers?: string[]
}
}
interface DossierCoverageResponse {
@@ -338,6 +346,7 @@ export function AutomationEvidenceCard() {
const topGate = quality?.gate_failures?.[0]
const executionBackend = quality?.execution_backend_summary ?? null
const ansibleRuntime = quality?.ansible_runtime ?? null
const qualityLoaded = Boolean(quality)
const claimReady = Boolean(quality?.production_claim?.can_claim_full_auto_repair)
const route = snapshot?.route ?? null
@@ -374,6 +383,7 @@ export function AutomationEvidenceCard() {
routeDetail,
routeTone: routeTone(route),
executionBackend,
ansibleRuntime,
}
}, [snapshot, t])
@@ -525,6 +535,12 @@ export function AutomationEvidenceCard() {
checkMode: derived.executionBackend.ansible_check_mode_total ?? 0,
apply: derived.executionBackend.ansible_apply_total ?? 0,
pending: derived.executionBackend.ansible_pending_check_mode_total ?? 0,
runtime: derived.ansibleRuntime?.can_run_check_mode
? t('ansibleRuntimeReady')
: t('ansibleRuntimeBlocked', {
blockers: (derived.ansibleRuntime?.blockers ?? []).slice(0, 2).join(', ') || '--',
}),
}),
})}
</span>
</div>