publier : les releases voyagent jusqu'aux deux forges ; wiki publie sur la forge du site
Le colis du genome ne portait que la branche : les etiquettes de release n'arrivaient jamais sur la forge du site. Il les porte desormais, le runner les pousse sans forcer, et la forge est relue par son API. publier.py pousse aussi les etiquettes vers eregion. Le wiki, jamais publie sur la forge du site, l'est. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e9215a2141
commit
e8907285d5
7 changed files with 227 additions and 10 deletions
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -1,5 +1,50 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-07 (105) — Les releases et le wiki n'allaient que là où on les poussait à la main
|
||||
|
||||
**94 preuves.** La forge du site fait autorité (D-81), mais deux choses n'y arrivaient
|
||||
jamais d'elles-mêmes : les étiquettes de release, et le wiki.
|
||||
|
||||
### Les releases manquaient à la forge du site
|
||||
|
||||
`v2026.10.07` poussée sur eregion, la question suivante a été : et la forge du site ? Mesure
|
||||
(`git ls-remote --tags`) : elle portait `v2026.10.04`, pas `v2026.08.21` ni `v2026.10.07`. Le
|
||||
colis de `make publier` (`genome_colis.py`) ne transporte que `refs/heads/<branche>`, et
|
||||
`publier.py` ne poussait que la branche vers eregion. `v2026.10.04` y avait été portée par un
|
||||
geste à part, que rien ne gardait. Les deux étiquettes manquantes ont été poussées par
|
||||
`origin` (rebond SSH du poste), et relues identiques des deux côtés.
|
||||
|
||||
**Fait** :
|
||||
- `genome_colis.py` : le colis emporte toutes les étiquettes (`bundle create … --tags`) ; le
|
||||
runner les récupère et les pousse avec la branche, **sans forcer** : une étiquette déjà
|
||||
présente sous un autre objet est refusée, des deux côtés. Une release ne bouge pas.
|
||||
- `genome_pousser.yml` : la forge est relue par son API (`git/refs/tags`), et doit porter
|
||||
toutes les étiquettes du poste.
|
||||
- `publier.py` pousse aussi les étiquettes vers eregion.
|
||||
|
||||
**Option écartée** : faire d'eregion un miroir poussé par la forge du site. Mesuré : eregion
|
||||
porte deux branches de contribution non fusionnées (`essai_integration`, 4 commits ;
|
||||
`fix_client_pki`, demande de fusion n° 5 ouverte), qu'un miroir effacerait. L'exploitant
|
||||
garde les deux forges poussées depuis le poste, par prudence envers les contributions.
|
||||
|
||||
**Éprouvé** : `test_genome_colis.py` (dans `make test`), sur de vrais dépôts git jetables :
|
||||
sans étiquette, la tête passe ; une release annotée arrive sur la forge, même objet ; une
|
||||
étiquette déplacée est refusée, la forge garde l'ancienne. Témoin : sur l'ancien
|
||||
`genome_colis.py`, le test échoue dès « la release voyage jusqu'à la forge ». `make
|
||||
verifier` conforme, **94/94**.
|
||||
|
||||
**Pas encore éprouvé** : la relecture par l'API de la forge (`git/refs/tags`). Le premier
|
||||
`make publier` qui suivra le dira : « N etiquette(s) du poste, toutes sur la forge ».
|
||||
|
||||
### Le wiki de la forge du site était vide
|
||||
|
||||
`make wiki-publier` publie vers UNE forge (`WIKI_REMOTE`) ; son témoin
|
||||
(`docs/audit/wiki-publie.yml`) montrait la dernière publication, le 2026-09-29, vers eregion
|
||||
seulement. Rien n'avait jamais publié vers la forge du site. Sa branche de wiki demandée à
|
||||
l'API de Forgejo (`wiki_branch: main`), le wiki est publié sur les deux forges ; relus :
|
||||
même arbre (`b3a70ba`), 96 pages, 8 figures, des deux côtés. Le témoin ne retient qu'une
|
||||
forge (la dernière publiée, eregion) : P60 ne voit pas l'autre.
|
||||
|
||||
## 2026-10-07 (104) — L'historique de supervision traverse la reconstruction : les deux locataires, sur `2cbfa2d`
|
||||
|
||||
**94 preuves.** Les deux locataires rasés et reconstruits par le site qui les nomme, sur le
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -318,6 +318,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_raser.py
|
||||
python3 scripts/tests/test_raser_resultat.py
|
||||
python3 scripts/tests/test_temoins.py
|
||||
python3 scripts/tests/test_genome_colis.py
|
||||
python3 scripts/tests/test_adressage_derive.py
|
||||
python3 scripts/tests/test_gui_intrants.py
|
||||
python3 scripts/tests/test_runbooks.py
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
|
||||
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
|
||||
source: 3fe7e3c
|
||||
date: 2026-09-29
|
||||
source: e9215a2
|
||||
date: 2026-10-07
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@
|
|||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.dest }}"
|
||||
register: genome_emballe
|
||||
changed_when: true
|
||||
|
||||
- name: Porter les colis jusqu'au runner
|
||||
|
|
@ -255,6 +256,45 @@
|
|||
label: "{{ item.depot }}"
|
||||
index_var: idx
|
||||
|
||||
# LES RELEASES AUSSI (2026-10-07). Le colis ne portait que la branche : `v2026.08.21` et
|
||||
# `v2026.10.07` manquaient a la forge du site. Il porte desormais les etiquettes ; on
|
||||
# demande a la forge si elle a TOUTES celles du poste. 404 = aucune etiquette sur ce
|
||||
# depot, ce qui est juste quand le poste n'en a pas non plus.
|
||||
- name: Demander à la forge les étiquettes qu'elle porte
|
||||
ansible.builtin.uri:
|
||||
url: >-
|
||||
https://forge.{{ domaine_interne }}/api/v1/repos/{{ serveur_ops_forge_organisation
|
||||
| default('genome') }}/{{ item.depot }}/git/refs/tags
|
||||
validate_certs: false
|
||||
status_code: [200, 404]
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
register: genome_forge_etiquettes
|
||||
retries: 4
|
||||
delay: 3
|
||||
until: genome_forge_etiquettes is succeeded
|
||||
changed_when: false
|
||||
|
||||
- name: La forge porte-t-elle toutes les releases du poste ?
|
||||
vars:
|
||||
genome_etiquettes_poste: "{{ (genome_emballe.results[idx].stdout | from_json).etiquettes | default([]) }}"
|
||||
genome_etiquettes_forge: >-
|
||||
{{ (genome_forge_etiquettes.results[idx].json | default([]) if genome_forge_etiquettes.results[idx].status == 200 else [])
|
||||
| map(attribute='ref') | map('regex_replace', '^refs/tags/', '') | list }}
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- genome_etiquettes_poste | difference(genome_etiquettes_forge) | length == 0
|
||||
fail_msg: >-
|
||||
{{ item.depot }} : la forge n'a pas
|
||||
{{ genome_etiquettes_poste | difference(genome_etiquettes_forge) | join(', ') }}.
|
||||
success_msg: >-
|
||||
{{ item.depot }} : {{ genome_etiquettes_poste | length }} etiquette(s) du poste, toutes sur la forge.
|
||||
loop: "{{ genome_a_pousser }}"
|
||||
loop_control:
|
||||
label: "{{ item.depot }}"
|
||||
index_var: idx
|
||||
|
||||
# LE RUNNER DU SITE AVANCE SON PROPRE CLONE, ET SA CONSOLE NE LE SAIT PAS (2026-09-20).
|
||||
#
|
||||
# C'est LUI qui pousse, donc il doit d'abord recevoir : ce playbook fait avancer le
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ DEUX GESTES, DEUX COTES :
|
|||
genome_colis.py etat <depot>
|
||||
Des deux cotes. Ce que cette copie porte, pour le confronter a la forge.
|
||||
|
||||
LES ETIQUETTES VOYAGENT AVEC LA BRANCHE (2026-10-07). Le colis ne portait que `main` : les
|
||||
releases n'arrivaient jamais sur la forge du site, qui fait pourtant autorite (D-81).
|
||||
`v2026.08.21` et `v2026.10.07` y manquaient, et il a fallu les pousser a la main. Le colis
|
||||
emporte desormais toutes les etiquettes du poste ; le runner les recoit et les pousse. Une
|
||||
etiquette qui existe deja SOUS UN AUTRE OBJET est refusee, des deux cotes : une release ne
|
||||
bouge pas.
|
||||
|
||||
`--ff-only` est le garde-fou. Si les deux histoires ont diverge, on s'arrete plutot que de
|
||||
fabriquer une fusion que personne n'a decidee — et le message de git nomme alors exactement
|
||||
ce qu'il faut reconcilier.
|
||||
|
|
@ -55,13 +62,18 @@ def _git(depot: Path, *args: str, env: dict | None = None) -> str:
|
|||
return (r.stdout or "").strip()
|
||||
|
||||
|
||||
def etiquettes(depot: Path) -> list[str]:
|
||||
return sorted(e for e in _git(depot, "tag", "-l").splitlines() if e)
|
||||
|
||||
|
||||
def emballer(depot: Path, colis: Path, branche: str) -> int:
|
||||
if not (depot / ".git").exists():
|
||||
raise SystemExit(f"{depot} n'est pas un depot git — rien a emballer.")
|
||||
colis.parent.mkdir(parents=True, exist_ok=True)
|
||||
_git(depot, "bundle", "create", str(colis), branche)
|
||||
_git(depot, "bundle", "create", str(colis), branche, "--tags")
|
||||
print(json.dumps({"depot": str(depot), "colis": str(colis),
|
||||
"tete": _git(depot, "log", "-1", "--format=%H")}))
|
||||
"tete": _git(depot, "log", "-1", "--format=%H"),
|
||||
"etiquettes": etiquettes(depot)}))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
@ -101,6 +113,10 @@ def recevoir(depot: Path, colis: Path, pushurl: str, cle: str | None,
|
|||
f"refs/heads/{branche}:refs/remotes/colis/{branche}")
|
||||
_git(depot, "merge", "--ff-only", f"refs/remotes/colis/{branche}")
|
||||
apres = _git(depot, "log", "-1", "--format=%H")
|
||||
# Sans `--force` : une etiquette deja presente sous un autre objet fait echouer la
|
||||
# recuperation (« would clobber existing tag ») — c'est voulu.
|
||||
if any("refs/tags/" in l for l in _git(depot, "bundle", "list-heads", str(colis)).splitlines()):
|
||||
_git(depot, "fetch", str(colis), "refs/tags/*:refs/tags/*")
|
||||
|
||||
env = None
|
||||
if cle:
|
||||
|
|
@ -114,14 +130,15 @@ def recevoir(depot: Path, colis: Path, pushurl: str, cle: str | None,
|
|||
# seule l'ECRITURE s'authentifie, et seulement le temps de cette commande.
|
||||
r = subprocess.run(
|
||||
["git", "-C", str(depot), "-c", f"remote.origin.pushurl={pushurl}",
|
||||
"push", "origin", branche],
|
||||
"push", "origin", branche, "refs/tags/*:refs/tags/*"],
|
||||
capture_output=True, text=True, env=env)
|
||||
sortie = ((r.stdout or "") + (r.stderr or "")).strip()
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(f"la poussee a echoue :\n {sortie}")
|
||||
|
||||
print(json.dumps({"avant": avant, "apres": apres,
|
||||
"pousse": f"{branche} -> {branche}" in sortie, "sortie": sortie[:400]}))
|
||||
"pousse": f"{branche} -> {branche}" in sortie,
|
||||
"etiquettes": etiquettes(depot), "sortie": sortie[:400]}))
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ liste que `genome-pousser` — lue, pas recopiee) :
|
|||
demande de fusion acceptee, par exemple) : publier sans les integrer les ecraserait
|
||||
sur la forge du site ;
|
||||
2. signaler ce qui n'est pas committe — cela ne part pas ;
|
||||
3. pousser sur eregion ;
|
||||
puis `make genome-pousser`, puis `make genome-etat` : la forge du site doit porter la meme
|
||||
tete que le poste, sinon on le dit.
|
||||
3. pousser sur eregion, la branche ET les etiquettes (2026-10-07 : les releases n'allaient
|
||||
nulle part d'elles-memes ; une etiquette deplacee est refusee, une release ne bouge pas) ;
|
||||
puis `make genome-pousser`, qui porte aussi les etiquettes et verifie que la forge les a,
|
||||
puis `make genome-etat` : la forge du site doit porter la meme tete que le poste, sinon on
|
||||
le dit.
|
||||
|
||||
make publier tous les depots du genome
|
||||
make publier DEPOT=ops-chezlepro
|
||||
|
|
@ -81,7 +83,7 @@ def main() -> int:
|
|||
if sale:
|
||||
print(f" ~ {d['depot']:<17} {len(sale)} changement(s) NON committe(s) — ils ne partent pas")
|
||||
avance = git(depot, "rev-list", "--count", f"{distant}..HEAD").stdout.strip() or "?"
|
||||
r = git(depot, "push", "-q", "eregion", branche)
|
||||
r = git(depot, "push", "-q", "eregion", branche, "refs/tags/*:refs/tags/*")
|
||||
if r.returncode != 0:
|
||||
refus.append(d["depot"])
|
||||
print(f" !! {d['depot']:<17} push refuse : {r.stderr.strip()[:120]}")
|
||||
|
|
|
|||
112
scripts/tests/test_genome_colis.py
Normal file
112
scripts/tests/test_genome_colis.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#!/usr/bin/env python3
|
||||
"""`genome_colis.py` : le colis porte la branche ET les releases jusqu'à la forge du site.
|
||||
|
||||
POURQUOI (2026-10-07). Le colis ne portait que `main`. Les étiquettes de release
|
||||
n'arrivaient donc jamais sur la forge du site, qui fait autorité (D-81) : `v2026.08.21` et
|
||||
`v2026.10.07` y manquaient, et il a fallu les pousser à la main.
|
||||
|
||||
Sur de vrais dépôts git, dans un répertoire jetable (aucun réseau) :
|
||||
- un poste, une forge nue, un runner cloné depuis la forge ;
|
||||
- le poste fait un commit et étiquette une release ; `emballer`, puis `recevoir` sur le
|
||||
runner, avec la forge pour `pushurl` ;
|
||||
- la forge porte la nouvelle tête ET l'étiquette ; un dépôt sans étiquette passe aussi ;
|
||||
- une étiquette déplacée sur le poste est REFUSÉE : une release ne bouge pas.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parents[2]
|
||||
COLIS = RACINE / "scripts" / "genome_colis.py"
|
||||
ENV = {**os.environ, "GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t",
|
||||
"GIT_COMMITTER_EMAIL": "t@t", "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"}
|
||||
|
||||
ECHECS: list[str] = []
|
||||
|
||||
|
||||
def verifier(cond: bool, msg: str) -> None:
|
||||
print(("OK " if cond else "ECHEC ") + msg)
|
||||
if not cond:
|
||||
ECHECS.append(msg)
|
||||
|
||||
|
||||
def git(*args: str, cwd: Path | None = None) -> str:
|
||||
r = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True, env=ENV)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args)} : {r.stderr.strip()}")
|
||||
return r.stdout.strip()
|
||||
|
||||
|
||||
def colis(*args: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run([sys.executable, str(COLIS), *args], capture_output=True, text=True, env=ENV)
|
||||
|
||||
|
||||
def monter(d: Path) -> tuple[Path, Path, Path]:
|
||||
forge, poste, runner = d / "forge.git", d / "poste", d / "runner"
|
||||
git("init", "-q", "--bare", "-b", "main", str(forge))
|
||||
git("init", "-q", "-b", "main", str(poste))
|
||||
(poste / "a").write_text("1")
|
||||
git("add", "a", cwd=poste)
|
||||
git("commit", "-q", "-m", "un", cwd=poste)
|
||||
git("push", "-q", str(forge), "main", cwd=poste)
|
||||
git("clone", "-q", str(forge), str(runner))
|
||||
return forge, poste, runner
|
||||
|
||||
|
||||
def porter(poste: Path, runner: Path, forge: Path, d: Path) -> subprocess.CompletedProcess:
|
||||
paquet = d / "x.bundle"
|
||||
e = colis("emballer", str(poste), str(paquet))
|
||||
if e.returncode != 0:
|
||||
return e
|
||||
return colis("recevoir", str(runner), str(paquet), str(forge))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d = Path(d)
|
||||
forge, poste, runner = monter(d)
|
||||
|
||||
# Un depot sans etiquette : rien ne change pour lui.
|
||||
(poste / "a").write_text("2")
|
||||
git("commit", "-q", "-am", "deux", cwd=poste)
|
||||
r = porter(poste, runner, forge, d)
|
||||
verifier(r.returncode == 0 and git("rev-parse", "main", cwd=forge) == git("rev-parse", "HEAD", cwd=poste),
|
||||
f"sans etiquette : la forge porte la tete du poste ({r.stderr.strip()[-160:]})")
|
||||
|
||||
# Une release annotee.
|
||||
(poste / "a").write_text("3")
|
||||
git("commit", "-q", "-am", "trois", cwd=poste)
|
||||
git("tag", "-a", "v1", "-m", "release v1", cwd=poste)
|
||||
r = porter(poste, runner, forge, d)
|
||||
recu = json.loads(r.stdout) if r.returncode == 0 else {}
|
||||
verifier(r.returncode == 0 and "v1" in git("tag", "-l", cwd=forge).split(),
|
||||
f"la release voyage jusqu'a la forge ({r.stderr.strip()[-160:]})")
|
||||
verifier(git("rev-parse", "v1", cwd=forge) == git("rev-parse", "v1", cwd=poste),
|
||||
"la forge porte le MEME objet d'etiquette que le poste (annotee, pas recreee)")
|
||||
verifier(recu.get("etiquettes") == ["v1"], f"le runner dit les etiquettes qu'il porte ({recu.get('etiquettes')})")
|
||||
e = json.loads(colis("emballer", str(poste), str(d / "y.bundle")).stdout)
|
||||
verifier(e.get("etiquettes") == ["v1"], "l'emballage dit les etiquettes du poste")
|
||||
|
||||
# Une release deplacee sur le poste : refusee, la forge garde l'ancienne.
|
||||
ancienne = git("rev-parse", "v1", cwd=forge)
|
||||
(poste / "a").write_text("4")
|
||||
git("commit", "-q", "-am", "quatre", cwd=poste)
|
||||
git("tag", "-f", "-a", "v1", "-m", "v1 deplacee", cwd=poste)
|
||||
r = porter(poste, runner, forge, d)
|
||||
verifier(r.returncode != 0 and git("rev-parse", "v1", cwd=forge) == ancienne,
|
||||
f"une etiquette deplacee est refusee, la forge garde la release ({r.stderr.strip()[-120:]})")
|
||||
|
||||
if ECHECS:
|
||||
print(f"\n{len(ECHECS)} echec(s).")
|
||||
return 1
|
||||
print("\nLe colis porte la branche et les releases ; une release ne bouge pas.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue