Set-OPS-Public/scripts/tests/test_verrou_runner.py
Daniel Allaire 4ebca13058 runner : roues et collections prises au site, plus a PyPI ni a Galaxy
verrou-runner.yml fige les 15 roues et les 3 collections qui tournent sur le
runner de Technolibre, avec URL amont et somme SHA-256. Le cache du runner
tenait deux versions de six bibliotheques selon le jour du montage.

- serveur_artefacts depose ces 18 fichiers au depot de binaires du site.
- serveur_ops les prend au site d'abord, somme verifiee ; repli sur Internet
  dit en clair ; plus aucun pip download ni collection download ; seuls les
  fichiers du verrou sont deposes sur le runner.
- ansible-core epingle a 2.18.19 (la version deployee).
- P70 exige le verrou au site ; test_verrou_runner.py dans make test.

AGENTS.md et CLAUDE.md : version allegee par l'operateur.

94 preuves. CHANGELOG (121).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:24:38 -04:00

82 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""`verrou-runner.yml` : ce que le runner installe, une version par bibliotheque, coherent avec
les epingles — et plus aucune resolution contre PyPI ou Galaxy dans les taches.
POURQUOI (2026-10-09). Le runner de Technolibre portait deux versions de six bibliotheques :
`pip download` resolvait contre PyPI a chaque passage, et PyPI avait bouge entre
l'insemination et le montage. Le verrou nomme chaque fichier ; ce test garde qu'il reste un
verrou (une version par bibliotheque), que ses roues vont au Python declare, et que les
epingles ecrites ailleurs disent la meme chose que lui.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
import yaml
RACINE = Path(__file__).resolve().parents[2]
ECHECS: list[str] = []
def verifier(cond: bool, msg: str) -> None:
print(("OK " if cond else "ECHEC ") + msg)
if not cond:
ECHECS.append(msg)
def norme(nom: str) -> str:
return re.sub(r"[-_.]+", "_", nom).lower()
def main() -> int:
v = yaml.safe_load((RACINE / "verrou-runner.yml").read_text())
roues, colls = v["python"], v["collections"]
for e in roues + colls:
verifier(e["url"].endswith("/" + e["nom"]) and re.fullmatch(r"[0-9a-f]{64}", str(e["sha256"])) is not None,
f"{e['nom']} : url qui le nomme, sha256 bien formee")
versions: dict[str, set[str]] = {}
for e in roues:
dist, ver = e["nom"].split("-")[0], e["nom"].split("-")[1]
versions.setdefault(norme(dist), set()).add(ver)
doubles = {d: sorted(vs) for d, vs in versions.items() if len(vs) > 1}
verifier(not doubles, f"une seule version par bibliotheque ({len(versions)} bibliotheques){' : ' + str(doubles) if doubles else ''}")
cible = "cp" + str(v["python_cible"]).replace(".", "")
hors = [e["nom"] for e in roues if e["nom"].split("-")[2].startswith("cp") and "abi3" not in e["nom"]
and e["nom"].split("-")[2] != cible]
verifier(not hors, f"les roues compilees vont a Python {v['python_cible']} ({cible}){' : ' + str(hors) if hors else ''}")
epingles = {}
for l in (RACINE / "requirements-python.txt").read_text().splitlines():
m = re.match(r"^\s*([A-Za-z0-9_.-]+)==([^\s#]+)", l)
if m:
epingles[norme(m.group(1))] = m.group(2)
defauts = yaml.safe_load((RACINE / "roles/serveur_ops/defaults/main.yml").read_text())
m = re.fullmatch(r"\s*([A-Za-z0-9_.-]+)==(\S+)\s*", defauts["serveur_ops_ansible"])
verifier(m is not None, f"serveur_ops_ansible est une epingle exacte ({defauts['serveur_ops_ansible']})")
if m:
epingles[norme(m.group(1))] = m.group(2)
for dist, ver in sorted(epingles.items()):
verifier(versions.get(dist) == {ver}, f"{dist}=={ver} epingle, et c'est la version du verrou ({sorted(versions.get(dist, []))})")
req = yaml.safe_load((RACINE / "requirements.yml").read_text())["collections"]
attendus = sorted(f"{c['name'].replace('.', '-')}-{c['version']}.tar.gz" for c in req)
verifier(sorted(e["nom"] for e in colls) == attendus, f"les collections du verrou sont celles de requirements.yml ({attendus})")
taches = [l for l in (RACINE / "roles/serveur_ops/tasks/main.yml").read_text().splitlines()
if not l.lstrip().startswith("#")]
texte = "\n".join(taches)
verifier('"download"' not in texte and "collection download" not in texte,
"plus aucune resolution contre PyPI ou Galaxy dans serveur_ops (pip / collection download)")
verifier("verrou-runner.yml" in (RACINE / "roles/serveur_artefacts/tasks/main.yml").read_text(),
"le site tient le verrou du runner")
if ECHECS:
print(f"\n{len(ECHECS)} echec(s).")
return 1
print("\nLe verrou nomme chaque fichier du runner, une version par bibliotheque, et le site le tient.")
return 0
if __name__ == "__main__":
sys.exit(main())