Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions .github/workflows/pr-gate-stale-sweep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
53 changes: 53 additions & 0 deletions scripts/tests/test_pr_gate_sweep_select.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Loading