← Files RAGOpsARCHIVED FILE
skills/evaluate-ai-release/scripts/vendor/ragops/ci.py
4.24 KB · Oct 4, 2026 · 12:29 UTC
from __future__ import annotations
import json
import xml.etree.ElementTree as ET
from typing import Any
from ragops.policy_v2 import GateEvidence, ReleaseDecision
def junit_xml(decision: ReleaseDecision) -> str:
failures = sum(not gate.passed and not gate.waived and gate.severity == "block" for gate in decision.gates)
skipped = sum(not gate.passed and (gate.waived or gate.severity == "warn") for gate in decision.gates)
suite = ET.Element(
"testsuite",
{
"name": f"ragops: {decision.scenario_id}",
"tests": str(len(decision.gates)),
"failures": str(failures),
"skipped": str(skipped),
},
)
for gate in decision.gates:
case = ET.SubElement(suite, "testcase", {"name": gate.id, "classname": "ragops.release_gate"})
message = _message(gate)
if not gate.passed and gate.waived:
ET.SubElement(case, "skipped", {"message": f"waived: {message}"})
elif not gate.passed and gate.severity == "warn":
ET.SubElement(case, "skipped", {"message": f"warning: {message}"})
elif not gate.passed:
ET.SubElement(case, "failure", {"message": message, "type": gate.policy_path}).text = message
ET.indent(suite)
return ET.tostring(suite, encoding="unicode", xml_declaration=True) + "\n"
def github_summary(decision: ReleaseDecision, *, max_gates: int = 50) -> str:
max_gates = max(1, min(max_gates, 100))
lines = [
f"## RAGOps release gate: {_markdown(decision.decision)}",
"",
f"Scenario: `{_markdown(decision.scenario_id)}`",
"",
"| Gate | Scope | Observed | Threshold | Result |",
"| --- | --- | ---: | ---: | --- |",
]
for gate in decision.gates[:max_gates]:
result = "PASS" if gate.passed else "WAIVED" if gate.waived else gate.severity.upper()
lines.append(
f"| `{_markdown(gate.id)}` | `{_markdown(gate.scope)}` | {gate.observed:.6g} | "
f"{gate.comparator} {gate.threshold:.6g} | {result} |"
)
omitted = len(decision.gates) - max_gates
if omitted > 0:
lines.extend(["", f"{omitted} additional gates omitted."])
return "\n".join(lines) + "\n"
def sarif_report(decision: ReleaseDecision) -> dict[str, Any]:
failed = tuple(gate for gate in decision.gates if not gate.passed)
rules = [
{
"id": gate.id,
"name": _sarif_name(gate.id),
"shortDescription": {"text": gate.policy_path},
"properties": {"severity": gate.severity, "scope": gate.scope},
}
for gate in failed
]
results = [
{
"ruleId": gate.id,
"level": "note" if gate.waived else "error" if gate.severity == "block" else "warning",
"message": {"text": _message(gate)},
"properties": {
"policyPath": gate.policy_path,
"caseIds": list(gate.case_ids),
"waived": gate.waived,
},
}
for gate in failed
]
return {
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {"driver": {"name": "RAGOps", "informationUri": "https://github.com/thangldw/ragops", "rules": rules}},
"results": results,
}
],
}
def render_ci(decision: ReleaseDecision, output_format: str) -> str:
if output_format == "junit":
return junit_xml(decision)
if output_format == "github":
return github_summary(decision)
if output_format == "sarif":
return json.dumps(sarif_report(decision), ensure_ascii=False, indent=2) + "\n"
raise ValueError(f"Unsupported CI format: {output_format}")
def _message(gate: GateEvidence) -> str:
return (
f"{gate.policy_path}: observed {gate.observed:.6g} "
f"must be {gate.comparator} {gate.threshold:.6g}; cases={','.join(gate.case_ids) or 'none'}"
)
def _markdown(value: str) -> str:
return value.replace("\\", "\\\\").replace("|", "\\|").replace("`", "\\`").replace("\n", " ")
def _sarif_name(value: str) -> str:
result = "".join(character if character.isalnum() else "_" for character in value)
return result[:128] or "ragops_gate"
SHA-256: 1e3da0daab883400713574472d3b1bb51b209f5a5172b2e06f756ea4b6f9bab6