feat(awooop): expose ansible runtime readiness
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "品質摘要未列出主要缺口",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user