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>
82 lines
3.8 KiB
Python
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())
|