diff --git a/.github/workflows/pr-gate-stale-sweep.yml b/.github/workflows/pr-gate-stale-sweep.yml index 49d3724244..4d121c7cb1 100644 --- a/.github/workflows/pr-gate-stale-sweep.yml +++ b/.github/workflows/pr-gate-stale-sweep.yml @@ -215,7 +215,25 @@ jobs: # (last successful sweep > 1 h, runs on ubuntu-latest so it survives what # it watches). runs-on: [self-hosted, coursia-ephemeral, coursia-linux] - timeout-minutes: 15 + # 25 min, contre 15 jusqu'ici (#15770, datapoint Hermes 2026-09-21 re-mesure + # ici sur les runs annules du matin). Le plafond de 900 s etait FRANCHI a + # chaque passage : cout mesure par etape -- + # etape 3 « Find stale verdicts and re-run their gate » : 10m55s-11m38s + # etape 5 « Flag PRs whose PR gate never ran » : 3m05s-4m15s + # etapes 4/6/7 : quelques secondes + # soit ~15,5-16 min de travail contre un plafond de 15 min. Les deux runs + # 35600494259 et 35602445799 montrent l'etape 5 `cancelled` en plein vol : + # la population « gate absent » cesse alors d'etre etiquetee -- la raison + # meme de son repli dans ce sweep (#14477). Les succes du 20/09 tenaient a + # 830-883 s, soit 17 s de marge : un plafond sous le cout mesure du travail + # n'est pas un garde-fou, c'est le tueur de l'organe. + # + # Le cout des etapes 3 et 5 croit avec la taille du pool ouvert (307 PRs + # mesurees le 21/09) : c'est pourquoi le levier de fond (un appel API en + # moins par PR non candidate) est traite dans l'etape 3 elle-meme, et le + # plafond ne fait que rendre la marge explicite. Re-mesurer apres fusion : + # l'etape 3 doit redescendre nettement sous ses 11 min. + timeout-minutes: 25 steps: - name: Bootstrap gh into the persistent toolcache # The coursia-linux image (#14171: ubuntu:24.04 + python3/jq/curl/git) @@ -332,7 +350,22 @@ jobs: # `|| WFMAP=''`: an unguarded assignment failure would kill the # step under `bash -e` BEFORE the empty-map fallback below runs # (same silent death as run 35188299290, zero runtime output). - WFMAP=$(gh api "repos/$REPO/actions/runs?head_sha=$SHA&per_page=100" --jq '[.workflow_runs[] | {key:(.id|tostring), value:.workflow_id}] | from_entries' 2>/dev/null) || WFMAP='' + # + # #15770 -- cet appel n'est fait QUE si la PR porte une jambe + # `PR gate`. La carte n'est consommee que par `_fold_key` (repli de + # repliement des runs jumeaux, #11808), et le selecteur `continue` + # AVANT de l'utiliser des qu'il n'y a aucune jambe `PR gate` -- donc + # l'appel est du travail mort pour toute la population non + # candidate, qui est la majorite du pool ouvert. C'est le levier de + # cout de cette etape (mesure 2026-09-21 : etape 3 = 11 min pour + # ~100 PRs x 1-2 appels REST, sur un pool ouvert de 307). + # Un payload dont l'appel a echoue ne porte pas de nom de check : + # la carte reste vide, exactement le comportement d'avant. + if printf '%s' "$BODY" | grep -q '"name":"PR gate"'; then + WFMAP=$(gh api "repos/$REPO/actions/runs?head_sha=$SHA&per_page=100" --jq '[.workflow_runs[] | {key:(.id|tostring), value:.workflow_id}] | from_entries' 2>/dev/null) || WFMAP='' + else + WFMAP='' + fi if [ -z "${WFMAP:-}" ]; then WFMAP='{}'; fi printf '{"number":%s,"sha":"%s","fork":%s,"checks":%s,"workflows":%s}\n' "$NUM" "$SHA" "${FORK:-false}" "$BODY" "$WFMAP" >> /tmp/runs.jsonl done < /tmp/openprs.txt diff --git a/scripts/tests/test_pr_gate_sweep_select.py b/scripts/tests/test_pr_gate_sweep_select.py index c176a0a228..449cb9675a 100644 --- a/scripts/tests/test_pr_gate_sweep_select.py +++ b/scripts/tests/test_pr_gate_sweep_select.py @@ -793,3 +793,56 @@ def test_workflow_pins_constituent_rerun_branch(): branch = run.split('TARGET="${TARGET:--}"', 1)[1].split("fi", 1)[0] assert "gh run rerun" in branch assert "PR gate" not in branch + + +def _collector_run() -> str: + """Le `run` de l'etape de collecte + selection (celle qui porte MAX_MATURE).""" + with open(WORKFLOW, encoding="utf-8") as f: + doc = yaml.safe_load(f) + return str(next( + step.get("run", "") for step in doc["jobs"]["sweep"]["steps"] + if "MAX_MATURE" in str(step.get("run", "")) + )) + + +def test_workflows_map_is_inert_without_a_gate_leg(tmp_path): + """La carte `workflows` ne change RIEN pour une PR sans jambe `PR gate`. + + Contrat de l'economie #15770 : le collecteur ne paie l'appel + `actions/runs?head_sha=` QUE si le payload porte un check nomme `PR gate` + (un appel REST en moins par PR non candidate -- la majorite du pool ouvert, + mesure 2026-09-21 : 307 PRs ouvertes). Ce test est ce qui rend l'economie + SURE plutot que supposee : si un jour le selecteur se mettait a lire la + carte dans la branche `gate_legs` vide, le collecteur cesserait de + l'alimenter en silence et le defaut serait invisible (un selecteur plus + etroit que la gate, la classe que ce fichier combat). + """ + # Meme PR, meme rouge non-gate : seule la presence de la carte differe. + assert _run_selector(tmp_path, [_pr(202, [OTHER_RED])]).strip() == "" + assert _run_selector( + tmp_path, [_pr(202, [OTHER_RED], workflows={555: 999})] + ).strip() == "" + + # Controle positif -- la MEME forme de ligne EST candidate des qu'une jambe + # `PR gate` rouge existe : sans lui, un `assert ""` sur une fixture cassee + # passerait aussi, et ne prouverait rien. + out = _run_selector( + tmp_path, [_pr(203, [GATE_FAIL, OTHER_GREEN], workflows={555: 999})] + ) + assert out.strip() == "203 deadbeef false 0" + + +def test_workflow_fetches_wfmap_only_for_gate_bearing_prs(): + """Garde structurelle de l'economie #15770 : le garde PRECEDE la collecte. + + Le selecteur, lui, est garde par `test_workflows_map_is_inert_without_a_gate_leg` ; + ce pin garde le COLLECTEUR des deux derives symetriques : l'appel + inconditionnel (l'economie disparait en silence) et un garde debranche du + motif reel (`"name":"PR gate"` est la forme que le jq du collecteur emet). + """ + run = _collector_run() + guard = run.find("""grep -q '"name":"PR gate"'""") + fetch = run.find("WFMAP=$(gh api") + assert guard != -1, "garde de collecte de la carte absent (#15770)" + assert fetch != -1 + assert guard < fetch, "la carte est demandee hors du garde (#15770)"