From c7e416d5cc99fc8bb38b3374a90bfd6e4bafc4a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Fri, 13 Mar 2026 13:31:55 +0100 Subject: [PATCH] feat: surface evidence status in github step summary --- docs/EVIDENCE_ALIGNMENT_2026-03-11.md | 6 ++ ...IFE_WORKFLOW_GITHUB_SEQUENCE_2026-03-11.md | 6 +- docs/evidence/evidence_pack.md | 2 + docs/plans/REPO_DEEP_ANALYSIS_2026-03-11.md | 3 +- test/test_auto_check_ci_cd.py | 72 +++++++++++++++++ tools/auto_check_ci_cd.py | 80 ++++++++++++++++++- tools/test_python.sh | 2 + 7 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 test/test_auto_check_ci_cd.py diff --git a/docs/EVIDENCE_ALIGNMENT_2026-03-11.md b/docs/EVIDENCE_ALIGNMENT_2026-03-11.md index 8f82d56..2c9ba09 100644 --- a/docs/EVIDENCE_ALIGNMENT_2026-03-11.md +++ b/docs/EVIDENCE_ALIGNMENT_2026-03-11.md @@ -61,6 +61,12 @@ Acceleration complementaire du 2026-03-13: - La version de `PlatformIO` est figee dans `tools/compliance/requirements-platformio.txt`. - Le bootstrap Python et la CI evidence s'appuient maintenant sur la meme source de versionnement, ce qui reduit les retelechargements et rend la lane plus stable. +Lisibilite operateur complementaire du 2026-03-13: + +- `tools/auto_check_ci_cd.py` ecrit maintenant un resume Markdown dans `GITHUB_STEP_SUMMARY` quand le job tourne sur GitHub Actions. +- Le Step Summary reprend `compliance`, `esp`, `linux`, puis le detail des sous-steps `build/test -> collect -> verify`. +- Le JSON `docs/evidence/ci_cd_audit_summary.json` reste la source canonique; le Step Summary est une projection lisible pour la review. + ## Consequence operateur - Un echec CI ne doit plus masquer l'evidence pack: les logs et resumes restent telechargeables via l'artifact `evidence-pack`. diff --git a/docs/KILL_LIFE_WORKFLOW_GITHUB_SEQUENCE_2026-03-11.md b/docs/KILL_LIFE_WORKFLOW_GITHUB_SEQUENCE_2026-03-11.md index 28024cc..76cab1d 100644 --- a/docs/KILL_LIFE_WORKFLOW_GITHUB_SEQUENCE_2026-03-11.md +++ b/docs/KILL_LIFE_WORKFLOW_GITHUB_SEQUENCE_2026-03-11.md @@ -66,7 +66,7 @@ sequenceDiagram | `tools/run_github_dispatch_mcp.sh` | launcher stdio du dispatch GitHub | | `.github/workflows/ci.yml` | gate principal `python-stable` sur la branche ou la PR | | `.github/workflows/repo_state.yml` | photographie exploitable du repo et artefacts de statut | -| `.github/workflows/evidence_pack.yml` | bootstrap Python + `platformio` repo-local + caches `pip`/`PlatformIO` + evidence lane forcee en `native-pio` | +| `.github/workflows/evidence_pack.yml` | bootstrap Python + `platformio` repo-local + caches `pip`/`PlatformIO` + evidence lane forcee en `native-pio` + Step Summary | | `.github/workflows/release_signing.yml` | chemin de release signee par tag ou `workflow_dispatch` | | `docs/evidence/evidence_pack.md` | contrat minimal et chemins canoniques d'un evidence pack | | `docs/EVIDENCE_ALIGNMENT_2026-03-11.md` | note d'audit qui ferme l'ecart entre CI, preuves locales et doc | @@ -78,6 +78,7 @@ sequenceDiagram - L'artifact `evidence-pack` est un dump de `docs/evidence/`; il reste utile meme si un target sort en `incomplete`. - La lane evidence GitHub n'a plus besoin du CAD stack Docker pour `PlatformIO`; elle force la voie `native-pio` depuis le venv repo-local. - Les caches `pip` et `PlatformIO` accelerent la lane sans changer le contrat des preuves produites. +- Le GitHub Step Summary donne une lecture humaine immediate du lane status sans remplacer le JSON d'audit. - `Kill_LIFE` garde la definition canonique des workflows et de leurs gates; le dispatch n'est qu'un mode d'execution distant. ## Next lots @@ -88,4 +89,5 @@ sequenceDiagram - `K-DA-006` est ferme par l'alignement du workflow `evidence_pack.yml` avec la chaine reelle `docs/evidence/*`. - `K-DA-007` est ferme par la voie `native-pio` repo-locale et le durcissement anti-artefacts obsoletes. - `K-DA-008` est ferme par l'ajout des caches `pip` / `PlatformIO` et du versionnement `requirements-platformio.txt`. -- `K-DA-009`: exposer un resume evidence lisible dans le GitHub Step Summary. +- `K-DA-009` est ferme par la generation du GitHub Step Summary depuis `tools/auto_check_ci_cd.py`. +- `K-DA-010`: ajouter un sidecar Markdown `docs/evidence/ci_cd_audit_summary.md` pour revue locale et artifact. diff --git a/docs/evidence/evidence_pack.md b/docs/evidence/evidence_pack.md index 345d9f9..c146253 100644 --- a/docs/evidence/evidence_pack.md +++ b/docs/evidence/evidence_pack.md @@ -73,6 +73,8 @@ Pour limiter le temps de bootstrap sur GitHub Actions, la lane evidence cache au - le cache `pip` resolu depuis `tools/compliance/requirements.txt` et `tools/compliance/requirements-platformio.txt` - le core `PlatformIO` dans `~/.platformio` +Pour la review rapide sur GitHub Actions, `tools/auto_check_ci_cd.py` ecrit aussi un resume Markdown dans le GitHub Step Summary quand la variable `GITHUB_STEP_SUMMARY` est presente. + Le job peut échouer tout en laissant un evidence pack partiel exploitable. C’est un comportement voulu : les fichiers `*.result.json`, `*.stdout.txt`, `*.stderr.txt` et `summary.json` restent la première preuve de diagnostic. En revanche, `summary.json` ne peut plus sortir `ok` si la commande build/test la plus récente a renvoyé un code non nul. ## Checklist minimum PR diff --git a/docs/plans/REPO_DEEP_ANALYSIS_2026-03-11.md b/docs/plans/REPO_DEEP_ANALYSIS_2026-03-11.md index cb4a5db..5aeaa32 100644 --- a/docs/plans/REPO_DEEP_ANALYSIS_2026-03-11.md +++ b/docs/plans/REPO_DEEP_ANALYSIS_2026-03-11.md @@ -33,7 +33,8 @@ Rendre `Kill_LIFE` plus lisible comme socle canonique runtime/spec-first: - `K-DA-006` ferme par `.github/workflows/evidence_pack.yml`, `docs/evidence/evidence_pack.md` et `docs/EVIDENCE_ALIGNMENT_2026-03-11.md` - `K-DA-007` ferme par `tools/bootstrap_python_env.sh`, `tools/ci_runtime.py`, `tools/collect_evidence.py`, `.github/workflows/evidence_pack.yml` et `test/test_firmware_evidence.py` - `K-DA-008` ferme par `.github/workflows/evidence_pack.yml`, `tools/compliance/requirements-platformio.txt` et la mise en cache `pip` / `PlatformIO` +- `K-DA-009` ferme par `tools/auto_check_ci_cd.py`, `test/test_auto_check_ci_cd.py` et la doc evidence/GitHub workflow ## Next tasks -- `K-DA-009` exposer un resume evidence lisible dans le GitHub Step Summary +- `K-DA-010` ajouter un sidecar Markdown `docs/evidence/ci_cd_audit_summary.md` pour revue locale et artifact diff --git a/test/test_auto_check_ci_cd.py b/test/test_auto_check_ci_cd.py new file mode 100644 index 0000000..4a142e8 --- /dev/null +++ b/test/test_auto_check_ci_cd.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from tools import auto_check_ci_cd + + +class AutoCheckCiCdTests(unittest.TestCase): + def sample_report(self) -> dict: + return { + "compliance": { + "command": ["python", "tools/compliance/validate.py", "--strict"], + "returncode": 0, + "stdout": "OK: compliance profile validated.", + "stderr": "", + }, + "targets": { + "esp": [ + { + "command": ["python", "tools/build_firmware.py", "esp"], + "returncode": 0, + "stdout": "Build terminé pour esp", + "stderr": "", + }, + { + "command": ["python", "tools/collect_evidence.py", "esp"], + "returncode": 0, + "stdout": "Evidence pack généré pour esp", + "stderr": "", + }, + ], + "linux": [ + { + "command": ["python", "tools/test_firmware.py", "linux"], + "returncode": 1, + "stdout": "", + "stderr": "native test failed", + } + ], + }, + } + + def test_render_markdown_summary_contains_lane_table_and_step_details(self): + summary = auto_check_ci_cd.render_markdown_summary(self.sample_report()) + self.assertIn("# Kill_LIFE Evidence Pack Summary", summary) + self.assertIn("| compliance | `0` | ok |", summary) + self.assertIn("| esp | `0` | ok |", summary) + self.assertIn("| linux | `1` | failed (1) |", summary) + self.assertIn("## esp", summary) + self.assertIn("`build_firmware`", summary) + self.assertIn("Build terminé pour esp", summary) + + def test_write_step_summary_writes_markdown_when_env_is_set(self): + report = self.sample_report() + with tempfile.TemporaryDirectory() as tmp: + summary_path = Path(tmp) / "summary.md" + with patch.dict(os.environ, {auto_check_ci_cd.STEP_SUMMARY_ENV: str(summary_path)}, clear=False): + written = auto_check_ci_cd.write_step_summary(report) + + self.assertEqual(written, summary_path) + content = summary_path.read_text(encoding="utf-8") + self.assertIn("Kill_LIFE Evidence Pack Summary", content) + self.assertIn("Artifact snapshot: `docs/evidence/`", content) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/auto_check_ci_cd.py b/tools/auto_check_ci_cd.py index 921734a..a5835bd 100644 --- a/tools/auto_check_ci_cd.py +++ b/tools/auto_check_ci_cd.py @@ -2,12 +2,14 @@ from __future__ import annotations import json +import os import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] REPORT_PATH = ROOT / "docs" / "evidence" / "ci_cd_audit_summary.json" +STEP_SUMMARY_ENV = "GITHUB_STEP_SUMMARY" def run_step(args: list[str]) -> dict: @@ -25,6 +27,79 @@ def run_step(args: list[str]) -> dict: } +def command_label(result: dict) -> str: + command = result.get("command", []) + if len(command) >= 2: + return Path(command[1]).stem + return "step" + + +def result_status_label(returncode: int) -> str: + return "ok" if returncode == 0 else f"failed ({returncode})" + + +def target_returncode(steps: list[dict]) -> int: + return max((item.get("returncode", 1) for item in steps), default=1) + + +def first_output_line(result: dict) -> str: + for key in ("stdout", "stderr"): + value = (result.get(key) or "").strip() + if value: + return value.splitlines()[0] + return "" + + +def render_markdown_summary(report: dict) -> str: + compliance_rc = report["compliance"]["returncode"] + + lines = [ + "# Kill_LIFE Evidence Pack Summary", + "", + f"- JSON report: `{REPORT_PATH.relative_to(ROOT)}`", + "- Artifact snapshot: `docs/evidence/`", + "", + "| Lane | RC | Status |", + "| --- | --- | --- |", + f"| compliance | `{compliance_rc}` | {result_status_label(compliance_rc)} |", + ] + + for target, steps in report["targets"].items(): + rc = target_returncode(steps) + lines.append(f"| {target} | `{rc}` | {result_status_label(rc)} |") + + for target, steps in report["targets"].items(): + lines.extend( + [ + "", + f"## {target}", + "", + "| Step | RC | Command | Signal |", + "| --- | --- | --- | --- |", + ] + ) + for result in steps: + label = command_label(result) + rc = result["returncode"] + command = " ".join(result.get("command", [])[1:]) + signal = first_output_line(result).replace("|", "\\|") + lines.append( + f"| `{label}` | `{rc}` | `{command}` | {signal or '-'} |" + ) + + return "\n".join(lines) + "\n" + + +def write_step_summary(report: dict) -> Path | None: + summary_path = os.environ.get(STEP_SUMMARY_ENV, "").strip() + if not summary_path: + return None + path = Path(summary_path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(render_markdown_summary(report), encoding="utf-8") + return path + + def check_all_targets() -> tuple[dict, bool]: report = { "targets": {}, @@ -65,10 +140,13 @@ def check_all_targets() -> tuple[dict, bool]: if __name__ == '__main__': report, failed = check_all_targets() + summary_path = write_step_summary(report) print("\n=== Rapport de vérification ===") print(f"compliance: rc={report['compliance']['returncode']}") for target, steps in report["targets"].items(): - rc = max(item["returncode"] for item in steps) + rc = target_returncode(steps) print(f"{target}: rc={rc}") print(f"rapport: {REPORT_PATH}") + if summary_path is not None: + print(f"step summary: {summary_path}") raise SystemExit(1 if failed else 0) diff --git a/tools/test_python.sh b/tools/test_python.sh index 8efa25a..3a9fcec 100755 --- a/tools/test_python.sh +++ b/tools/test_python.sh @@ -47,6 +47,7 @@ stable: test/test_mcp_runtime_status.py test/test_openclaw_sanitizer.py test/test_apply_safe_patch.py + test/test_auto_check_ci_cd.py test/test_firmware_evidence.py test/test_validate_specs.py tools/hw/schops/tests/test_*.py @@ -136,6 +137,7 @@ if [[ "${SUITE}" == "stable" || "${SUITE}" == "all" ]]; then run_discover test 'test_mcp_runtime_status.py' run_discover test 'test_openclaw_sanitizer.py' run_discover test 'test_apply_safe_patch.py' + run_discover test 'test_auto_check_ci_cd.py' run_discover test 'test_firmware_evidence.py' run_discover test 'test_validate_specs.py' run_discover tools/hw/schops/tests 'test_*.py'