Le requirements.yml genere portait version: \1 (regex_replace dans un bloc YAML) ; ansible-galaxy refusait, et la reprise de la reconstruction de Technolibre s'est arretee apres avoir pris roues et collections au site. Version tiree du nom par decoupage, sans expression reguliere ; rejoue sur le poste, les trois collections s'installent hors ligne. test_verrou_runner.py rend desormais le fichier avec Jinja2 et compare. 94 preuves. CHANGELOG (121). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
5.6 KiB
Python
107 lines
5.6 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 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())
|