#!/usr/bin/env python3 """GitHub Actions adapter for openapi_breaking_diff.py. Writes a Markdown step summary, emits workflow annotations, and preserves the CLI exit contract: 0=no supported breaking findings, 2=findings, 1=input error. """ from __future__ import annotations import argparse import json import os import sys from pathlib import Path from typing import Iterable from openapi_breaking_diff import compare, load, markdown def escape_command(value: str) -> str: return ( value.replace("%", "%25") .replace("\r", "%0D") .replace("\n", "%0A") .replace(":", "%3A") .replace(",", "%2C") ) def annotations(findings: Iterable[dict[str, str]]) -> list[str]: rows = [] for finding in findings: title = escape_command("OpenAPI breaking change: " + finding["change"]) message = escape_command(finding["location"] + " — " + finding["detail"] ) rows.append(f"::error title={title}::{message}") return rows def write_summary(report: str, destination: str | None) -> None: if not destination: return path = Path(destination) path.parent.mkdir(parents=True, exist_ok=True) with path.open("a", encoding="utf-8") as handle: handle.write(report) def set_output(name: str, value: str, destination: str | None) -> None: if not destination: return path = Path(destination) path.parent.mkdir(parents=True, exist_ok=True) delimiter = "OPENAPI_DIFF_OUTPUT" with path.open("a", encoding="utf-8") as handle: handle.write(f"{name}<<{delimiter}\n{value}\n{delimiter}\n") def main() -> int: parser = argparse.ArgumentParser(description="Emit GitHub Actions annotations for OpenAPI breaking findings.") parser.add_argument("old", help="Baseline OpenAPI JSON file") parser.add_argument("new", help="Proposed OpenAPI JSON file") parser.add_argument("--report", default="openapi-breaking-report.md", help="Markdown report path") parser.add_argument("--json-report", help="Optional machine-readable report path") parser.add_argument("--allow-breaking", action="store_true", help="Do not fail when findings exist") args = parser.parse_args() try: findings = compare(load(args.old), load(args.new)) except (OSError, json.JSONDecodeError, ValueError) as exc: print(f"::error title=OpenAPI diff input error::{escape_command(str(exc))}") return 1 report = markdown(findings, args.old, args.new) Path(args.report).write_text(report, encoding="utf-8") if args.json_report: Path(args.json_report).write_text( json.dumps({"breaking_count": len(findings), "findings": findings}, indent=2) + "\n", encoding="utf-8", ) for line in annotations(findings): print(line) if not findings: print("::notice title=OpenAPI compatibility check::No supported breaking changes detected") write_summary(report, os.environ.get("GITHUB_STEP_SUMMARY")) set_output("breaking_count", str(len(findings)), os.environ.get("GITHUB_OUTPUT")) set_output("report_path", args.report, os.environ.get("GITHUB_OUTPUT")) return 0 if args.allow_breaking or not findings else 2 if __name__ == "__main__": raise SystemExit(main())