#!/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 jinja2 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)") # Une copie fichier par fichier ne cree pas son dossier (2026-10-09 : reconstruction de # Technolibre arretee sur un runner neuf). Chaque dossier vise doit etre assure AVANT. assures: set[str] = set() for t in yaml.safe_load((RACINE / "roles/serveur_ops/tasks/main.yml").read_text()): f = t.get("ansible.builtin.file") or {} if f.get("state") == "directory": assures.add(f["path"]) c = t.get("ansible.builtin.copy") or {} if str(c.get("dest", "")).endswith("/{{ item.nom }}"): dossier = c["dest"].rsplit("/", 1)[0] verifier(dossier in assures, f"« {t['name']} » : son dossier est cree avant ({dossier})") # LE FICHIER RENDU, PAS LE MODELE (2026-10-09). `'\\1'` dans un bloc `|` restait `\1` : # `ansible-galaxy` refusait « Non integer values in LooseVersion ('\1') », et la # reconstruction de Technolibre s'est arretee. Rendu ici avec Jinja2 seul, comme le runner. tache = next(t for t in yaml.safe_load((RACINE / "roles/serveur_ops/tasks/main.yml").read_text()) if t.get("name") == "Ecrire la liste hors ligne des collections du verrou") try: rendu = yaml.safe_load(jinja2.Template(tache["ansible.builtin.copy"]["content"]).render( serveur_ops_verrou={"collections": colls}))["collections"] obtenues = sorted(f"{c['name']}={c['version']}" for c in rendu) except Exception as e: # un filtre propre a Ansible, une syntaxe cassee : c'est un echec obtenues = [f"rendu impossible : {e}"] voulues = sorted(f"{c['name'].replace('.', '-')}-{c['version']}.tar.gz={c['version']}" for c in req) verifier(obtenues == voulues, f"la liste hors ligne rendue porte les vraies versions ({obtenues})") 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())