feat: harden native evidence lane for esp builds

This commit is contained in:
L'électron rare
2026-03-11 21:17:01 +01:00
parent 94781d20a1
commit 3e3cb061f1
12 changed files with 166 additions and 17 deletions
+2 -2
View File
@@ -24,10 +24,10 @@ jobs:
python-version: "3.11"
- name: Bootstrap repo-local venv
run: bash tools/bootstrap_python_env.sh
run: bash tools/bootstrap_python_env.sh --with-platformio
- name: Generate evidence pack summary
run: ./.venv/bin/python tools/auto_check_ci_cd.py
run: KILL_LIFE_PIO_MODE=native ./.venv/bin/python tools/auto_check_ci_cd.py
- name: Upload evidence pack artifact
if: always()
+18
View File
@@ -13,6 +13,7 @@ Fermer `K-DA-006`: verifier que la voie locale, la voie GitHub et la documentati
## Contrat canonique retenu
- La CI `evidence_pack.yml` utilise maintenant `bash tools/bootstrap_python_env.sh` puis `./.venv/bin/python tools/auto_check_ci_cd.py`.
- La lane evidence `esp` force maintenant `KILL_LIFE_PIO_MODE=native` et installe `platformio` dans le venv repo-local.
- Le rapport global attendu est `docs/evidence/ci_cd_audit_summary.json`.
- Chaque cible ecrit ses preuves dans `docs/evidence/<target>/`:
- `build.result.json` ou `test.result.json`
@@ -37,6 +38,23 @@ Observation du 2026-03-11:
Cette execution confirme que le repo sait deja produire des preuves exploitables meme en cas d'echec partiel. La correction principale consistait donc a faire pointer la CI GitHub sur cette chaine reelle et a documenter la granularite des sorties.
Correctif complementaire du lot suivant:
- `ci_runtime.py` detecte aussi `PlatformIO` dans `.venv/bin/pio` et via `python -m platformio`.
- `collect_evidence.py` ne valide plus un evidence pack si la derniere commande build/test a echoue, meme si des artefacts plus anciens existent encore dans `.pio/`.
Verification complementaire du 2026-03-11:
```bash
KILL_LIFE_PIO_MODE=native ./.venv/bin/python tools/auto_check_ci_cd.py
```
Resultat:
- `compliance`: `rc=0`
- `esp`: `rc=0`, evidence pack complet dans `docs/evidence/esp/`
- `linux`: `rc=0`, evidence pack complet dans `docs/evidence/linux/`
## Consequence operateur
- Un echec CI ne doit plus masquer l'evidence pack: les logs et resumes restent telechargeables via l'artifact `evidence-pack`.
@@ -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 + generation du snapshot `docs/evidence/*` pour GitHub Actions |
| `.github/workflows/evidence_pack.yml` | bootstrap Python + `platformio` repo-local + evidence lane forcee en `native-pio` pour GitHub Actions |
| `.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 |
@@ -76,6 +76,7 @@ sequenceDiagram
- La machine operateur ne pousse pas elle-meme une logique arbitraire; elle demande le dispatch d'un workflow allowliste.
- Le retour utile n'est pas seulement `success/fail`, mais un ensemble de checks, artefacts et preuves consultables.
- 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.
- `Kill_LIFE` garde la definition canonique des workflows et de leurs gates; le dispatch n'est qu'un mode d'execution distant.
## Next lots
@@ -84,4 +85,5 @@ sequenceDiagram
- `K-DA-004`: resynchroniser plus largement README et docs/plans autour des deux sequences `local` et `github`.
- `K-DA-005`: synchroniser la doc operateur avec les preuves et artefacts effectivement exposes.
- `K-DA-006` est ferme par l'alignement du workflow `evidence_pack.yml` avec la chaine reelle `docs/evidence/*`.
- `K-DA-007`: stabiliser la lane ESP pour produire un evidence pack complet sur runner Ubuntu.
- `K-DA-007` est ferme par la voie `native-pio` repo-locale et le durcissement anti-artefacts obsoletes.
- `K-DA-008`: accelerer la lane evidence GitHub avec cache `pip` / `PlatformIO`.
+11 -6
View File
@@ -29,15 +29,20 @@ Le workflow CI/CD actuellement câblé build lESP (`esp32s3_arduino`) et test
Par défaut, les wrappers tentent `pio` en natif, puis basculent automatiquement sur `tools/hw/cad_stack.sh pio` si `pio` nest pas installé sur lhôte.
Tu peux forcer le runner avec `KILL_LIFE_PIO_MODE=native` ou `KILL_LIFE_PIO_MODE=container`.
Si tu veux une voie native reproductible depuis le venv repo-local :
```bash
bash tools/bootstrap_python_env.sh --with-platformio
```
Pour lancer manuellement :
```bash
python3 tools/build_firmware.py esp
python3 tools/collect_evidence.py esp
python3 tools/verify_evidence.py esp
KILL_LIFE_PIO_MODE=native ./.venv/bin/python tools/build_firmware.py esp
./.venv/bin/python tools/collect_evidence.py esp
./.venv/bin/python tools/verify_evidence.py esp
python3 tools/test_firmware.py linux
python3 tools/collect_evidence.py linux
python3 tools/verify_evidence.py linux
KILL_LIFE_PIO_MODE=native ./.venv/bin/python tools/test_firmware.py linux
./.venv/bin/python tools/collect_evidence.py linux
./.venv/bin/python tools/verify_evidence.py linux
```
`stm` reste non supporté tant quaucune cible STM nexiste dans `firmware/platformio.ini`.
+6
View File
@@ -54,6 +54,12 @@ Override possible :
- `KILL_LIFE_PIO_MODE=native`
- `KILL_LIFE_PIO_MODE=container`
Pour une voie repo-locale sans `pio` systeme mais avec venv dedie :
```bash
bash tools/bootstrap_python_env.sh --with-platformio
KILL_LIFE_PIO_MODE=native ./.venv/bin/python tools/build_firmware.py esp
```
Les evidence packs sont stockés dans `docs/evidence/`.
La couverture est générée via `coverage_badge.py`.
+5 -3
View File
@@ -52,8 +52,8 @@ Règle pratique :
Local ou CI :
```bash
bash tools/bootstrap_python_env.sh
./.venv/bin/python tools/auto_check_ci_cd.py
bash tools/bootstrap_python_env.sh --with-platformio
KILL_LIFE_PIO_MODE=native ./.venv/bin/python tools/auto_check_ci_cd.py
```
Le script exécute :
@@ -66,7 +66,9 @@ Le script exécute :
- `tools/collect_evidence.py linux`
- `tools/verify_evidence.py linux`
Le job peut échouer tout en laissant un evidence pack partiel exploitable. Cest un comportement voulu : les fichiers `*.result.json`, `*.stdout.txt`, `*.stderr.txt` et `summary.json` restent la première preuve de diagnostic.
La lane evidence CI force `KILL_LIFE_PIO_MODE=native` pour eviter de dependre du Docker CAD stack uniquement pour la collecte de preuves firmware.
Le job peut échouer tout en laissant un evidence pack partiel exploitable. Cest 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
- [ ] Le label `ai:*` est présent et cohérent avec le contenu
+2 -1
View File
@@ -31,7 +31,8 @@ Rendre `Kill_LIFE` plus lisible comme socle canonique runtime/spec-first:
- `K-DA-003` ferme par `docs/KILL_LIFE_WORKFLOW_GITHUB_SEQUENCE_2026-03-11.md`
- `K-DA-004` ferme par la synchronisation `docs/RUNBOOK.md`, `docs/index.md`, `docs/workflows/README.md`, `docs/AI_WORKFLOWS.md`, `docs/evidence/evidence_pack.md`
- `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`
## Next tasks
- `K-DA-007` stabiliser la production d'artefacts firmware `esp` dans la lane CI evidence pack
- `K-DA-008` accelerer la lane evidence GitHub avec cache `pip` / `PlatformIO`
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from tools import ci_runtime
from tools import collect_evidence as collect_evidence_module
class FirmwareEvidenceTests(unittest.TestCase):
def test_native_pio_command_detects_repo_local_venv_binary(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
repo_pio = root / ".venv" / "bin" / "pio"
repo_pio.parent.mkdir(parents=True, exist_ok=True)
repo_pio.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
repo_pio.chmod(0o755)
with patch("tools.ci_runtime.shutil.which", return_value=None):
self.assertEqual(ci_runtime.native_pio_command(root), [str(repo_pio)])
def test_collect_evidence_rejects_stale_artifacts_after_failed_step(self):
with tempfile.TemporaryDirectory() as tmp:
evidence_dir = Path(tmp) / "docs" / "evidence" / "linux"
evidence_dir.mkdir(parents=True, exist_ok=True)
(evidence_dir / "test.result.json").write_text(
json.dumps({"returncode": 1}), encoding="utf-8"
)
(evidence_dir / "test.stdout.txt").write_text("stdout", encoding="utf-8")
(evidence_dir / "test.stderr.txt").write_text("stderr", encoding="utf-8")
spec = ci_runtime.TargetSpec(requested="linux", env="native", mode="test")
captured = {}
def capture_summary(path: Path, payload: dict) -> None:
captured["path"] = path
captured["payload"] = payload
with patch.object(collect_evidence_module, "resolve_target", return_value=spec), patch.object(
collect_evidence_module, "ensure_evidence_dir", return_value=evidence_dir
), patch.object(
collect_evidence_module,
"collect_artifacts",
return_value=["firmware/.pio/build/native"],
), patch.object(
collect_evidence_module, "now_utc", return_value="2026-03-11T00:00:00Z"
), patch.object(
collect_evidence_module, "write_json", side_effect=capture_summary
):
ok = collect_evidence_module.collect_evidence("linux")
self.assertFalse(ok)
self.assertEqual(captured["path"], evidence_dir / "summary.json")
self.assertEqual(captured["payload"]["status"], "incomplete")
self.assertEqual(captured["payload"]["step_returncode"], 1)
self.assertIn("step_returncode=1", captured["payload"]["missing"])
if __name__ == "__main__":
unittest.main()
+11
View File
@@ -5,6 +5,7 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENV_DIR="${ROOT_DIR}/.venv"
PYTHON_BIN="${PYTHON_BIN:-python3}"
REINSTALL=false
WITH_PLATFORMIO=false
REQ_FILE="${ROOT_DIR}/tools/compliance/requirements.txt"
usage() {
@@ -17,6 +18,7 @@ Options:
--python BIN Python interpreter to use (default: PYTHON_BIN or python3)
--venv-dir PATH Virtualenv directory to create/reuse (default: ./.venv)
--reinstall Remove and recreate the target virtualenv before install
--with-platformio Install PlatformIO in the target virtualenv
-h, --help Show this help
Notes:
@@ -26,6 +28,7 @@ Notes:
Examples:
bash tools/bootstrap_python_env.sh
bash tools/bootstrap_python_env.sh --with-platformio
bash tools/bootstrap_python_env.sh --venv-dir /tmp/kill-life-venv --reinstall
EOF
}
@@ -45,6 +48,9 @@ while [[ $# -gt 0 ]]; do
--reinstall)
REINSTALL=true
;;
--with-platformio)
WITH_PLATFORMIO=true
;;
-h|--help)
usage
exit 0
@@ -82,5 +88,10 @@ if ! "${VENV_DIR}/bin/python" -c "import yaml, jsonschema" >/dev/null 2>&1; then
"${VENV_DIR}/bin/python" -m pip install 'jsonschema>=4.21.0'
fi
if [[ "${WITH_PLATFORMIO}" == true ]] && [[ ! -x "${VENV_DIR}/bin/pio" ]]; then
echo "[bootstrap-python] installing PlatformIO"
"${VENV_DIR}/bin/python" -m pip install platformio
fi
echo "[bootstrap-python] python: ${VENV_DIR}/bin/python"
echo "[bootstrap-python] test: bash tools/test_python.sh --venv-dir ${VENV_DIR}"
+31 -2
View File
@@ -4,6 +4,7 @@ import json
import os
import shutil
import subprocess
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
@@ -64,8 +65,32 @@ def pio_mode() -> str:
return value
def repo_local_pio_candidates(root: Path | None = None) -> list[Path]:
base = root or ROOT
return [
base / ".venv" / "bin" / "pio",
base / ".venv" / "Scripts" / "pio.exe",
]
def native_pio_command(root: Path | None = None) -> list[str] | None:
host_pio = shutil.which("pio")
if host_pio:
return [host_pio]
for candidate in repo_local_pio_candidates(root):
if candidate.exists() and os.access(candidate, os.X_OK):
return [str(candidate)]
try:
import platformio # noqa: F401
except ImportError:
return None
return [sys.executable, "-m", "platformio"]
def native_pio_available() -> bool:
return shutil.which("pio") is not None
return native_pio_command() is not None
def container_pio_available() -> bool:
@@ -102,7 +127,11 @@ def platformio_command(spec: TargetSpec, step: str) -> tuple[list[str], Path, st
if use_container:
return ["bash", str(CAD_STACK), "pio", *args], ROOT, "cad-stack-container"
return ["pio", *args], ROOT, "native-pio"
native_cmd = native_pio_command()
if native_cmd is None:
return ["bash", str(CAD_STACK), "pio", *args], ROOT, "cad-stack-container"
return [*native_cmd, *args], ROOT, "native-pio"
def resolved_pio_runner(spec: TargetSpec, step: str) -> str:
+10 -1
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import sys
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
@@ -15,13 +16,20 @@ def collect_evidence(target: str) -> bool:
spec = resolve_target(target)
evidence_dir = ensure_evidence_dir(spec.requested)
prefix = "build" if spec.mode == "build" else "test"
result_path = evidence_dir / f"{prefix}.result.json"
required = [
evidence_dir / f"{prefix}.result.json",
result_path,
evidence_dir / f"{prefix}.stdout.txt",
evidence_dir / f"{prefix}.stderr.txt",
]
artifacts = collect_artifacts(spec)
missing = [path.name for path in required if not path.exists()]
step_returncode = None
if result_path.exists():
payload = json.loads(result_path.read_text(encoding="utf-8"))
step_returncode = payload.get("returncode")
if step_returncode != 0:
missing.append(f"step_returncode={step_returncode}")
if not artifacts:
missing.append("artifacts")
@@ -31,6 +39,7 @@ def collect_evidence(target: str) -> bool:
"mode": spec.mode,
"generated_at_utc": now_utc(),
"required_files": [path.name for path in required],
"step_returncode": step_returncode,
"artifacts": artifacts,
"status": "ok" if not missing else "incomplete",
"missing": missing,
+2
View File
@@ -47,6 +47,7 @@ stable:
test/test_mcp_runtime_status.py
test/test_openclaw_sanitizer.py
test/test_apply_safe_patch.py
test/test_firmware_evidence.py
test/test_validate_specs.py
tools/hw/schops/tests/test_*.py
EOF
@@ -135,6 +136,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_firmware_evidence.py'
run_discover test 'test_validate_specs.py'
run_discover tools/hw/schops/tests 'test_*.py'
fi