feat: harden native evidence lane for esp builds
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
@@ -29,15 +29,20 @@ Le workflow CI/CD actuellement câblé build l’ESP (`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` n’est pas installé sur l’hô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 qu’aucune cible STM n’existe dans `firmware/platformio.ini`.
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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. C’est 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. 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
|
||||
- [ ] Le label `ai:*` est présent et cohérent avec le contenu
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user