Skip to content
Closed
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
129 changes: 129 additions & 0 deletions .github/workflows/stale-branch-update-sweep.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
name: Stale branch update sweep

# Cable le pilote de #16915 : le CI rattrape lui-meme les PRs en retard sur
# leur base, au lieu d'une session de worker dediee par rebase mandate.
# Mandat user 2026-09-19 : « il faut qu'elles soient automatiques pour la
# plupart ».
#
# QUE FAIT CE SWEEP. `scripts/ci/sweep_stale_pr_branches.py` enumere les PRs
# ouvertes, ecarte par metadonnees (brouillon, fork, non-mergeable), mesure le
# retard reel via l'API de comparaison, prend les --max-updates plus ANCIENNES
# candidates (les PRs quietes et agees n'ont pas d'autre voie de rattrapage --
# lecon mesuree du pr-gate-stale-sweep, run 33169455408) et les DELEEGUE a
# l'organe `scripts/ci/update_stale_pr_branches.py` par sous-processus. Le
# pilote n'ecrit jamais : l'organe porte seul les neuf gardes (SHA pinnes,
# registre en vol, jamais de force-push, jamais --rebase).
#
# CE QUE CE SWEEP NE SURVEND PAS. Mesure du 2026-09-19 (213 PRs ouvertes) :
# ~12 DIRTY mises a part, la population rattrapable est les CLEAN/BLOCKED/
# UNSTABLE a behind_by > 0. La valeur n'est pas le decompte debloque, c'est la
# suppression des allers-retours (commentaire + session worker + dossier
# exact-head remplaces par un run).
#
# CADENCE. 6 h declarees, cap 3 mises a jour par run : le gisement MESURE le
# 2026-09-20 par le dry-run du pilote est de 47 candidates a behind_by > 0
# (sur 214 PRs ouvertes ; l'estimation ~12 de l'issue #16915 etait basse) --
# a cap 3, il se vide en ~16 runs (~4 jours), sans vague : chaque update
# perime le dossier [ADJOINT PREFLIGHT] de sa PR (freshness STALE,
# invalidated), et une rafale non bornee perimerait 47 dossiers d'un coup.
# Caveat mesure (#15197/check_scheduler_liveness) : les crons sub-horaires de
# ce depot sont servis 4.7-7.2x en retard ; pour un filet de rattrapage sans
# SLA de latence, servir ~1 fois par 12-24 h reste utile et sans cout.
#
# PLANCHER DWELL. Un update-branch reecrit la tete : avant #16149 il
# re-armait les 120 min que le geste sert a franchir. merge_dwell.py mesure
# desormais last_authoritative_committed_at et EXEMPTE les fusions de base
# prouvees content-free. Ce contrat est verrouille DANS la suite du pilote
# (test_update_branch_shape_does_not_rearm_dwell) : si la exemption regresse,
# la suite rougit avec le gate, et ce cron n'introduit pas la taxe en silence.
#
# CODE DE SORTIE. Le relais de l'organe est honnete : un run ROUGE signifie
# « au moins un REFUSE » (conflit, tete qui a bouge, echec d'appel) -- un
# humain doit regarder. Etouffer ce rouge pour faire joli serait le
# « rapport vert sur un pool vide » que #16149 interdit.

on:
schedule:
# Minute off-:00 (convention anti-stampede du depot).
- cron: '23 */6 * * *'
workflow_dispatch:
inputs:
apply:
description: 'Apply the updates (default: dry-run, list only)'
required: false
type: boolean
default: false
max_updates:
description: 'Max branch updates this run (--max-updates)'
required: false
type: number
default: 3

permissions:
contents: write # gh pr update-branch (via l'organe) reecrit la branche
pull-requests: read # enumeration + lectures de l'organe

concurrency:
# Une classe d'evenement a la fois, JAMAIS d'annulation en cours : tuer un
# balayage mi-course laisserait des reservations du registre en vol posees
# pour rien (elles periment par TTL, mais le run suivant les lirait comme
# des mises a jour vivantes). Deux arrivals concurrents se coalescent.
group: stale-branch-update-sweep-${{ github.event_name }}
cancel-in-progress: false

jobs:
sweep:
name: Delegate stale-PR updates to the organ
# Pool dedie (mandat : consommer les runs conteneurises). Le balayage est
# gh-driven et l'image coursia-linux ne ship pas gh : bootstrap toolcache.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
timeout-minutes: 20
steps:
- name: Bootstrap gh into the persistent toolcache
env:
GH_BOOTSTRAP_VERSION: "2.80.0"
run: |
set -uo pipefail
if command -v gh >/dev/null 2>&1; then
echo "[gh] preinstalled on this runner"
gh --version
exit 0
fi
GH_BIN="/opt/hostedtoolcache/bin/gh"
if [ ! -x "$GH_BIN" ]; then
VER="${GH_BOOTSTRAP_VERSION}"
echo "[gh] bootstrapping v${VER} into toolcache (one-time, cached)"
curl -fsSL "https://github.com/cli/cli/releases/download/v${VER}/gh_${VER}_linux_amd64.tar.gz" -o /tmp/gh.tgz
tar -xzf /tmp/gh.tgz -C /tmp
mkdir -p /opt/hostedtoolcache/bin
mv "/tmp/gh_${VER}_linux_amd64/bin/gh" "$GH_BIN"
fi
echo "/opt/hostedtoolcache/bin" >> "$GITHUB_PATH"
"$GH_BIN" --version

# Sparse : le pilote importe l'organe (les DEUX fichiers doivent etre
# sur disque), et il n'invoque rien d'autre du depot. L'organe
# n'appelle pas git -- le checkout n'a pas besoin d'histoire.
- name: Checkout sparse (pilot + organ)
uses: actions/checkout@v4
with:
sparse-checkout: |
scripts/ci/sweep_stale_pr_branches.py
scripts/ci/update_stale_pr_branches.py
sparse-checkout-cone-mode: false

- name: Sweep and delegate
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
APPLY_INPUT: ${{ github.event.inputs.apply }}
MAX_INPUT: ${{ github.event.inputs.max_updates }}
run: |
set -uo pipefail
ARGS=(--repo "$REPO" --max-updates "${MAX_INPUT:-3}")
# Le cron balaye en --apply ; un dispatch manuel ne l'applique que
# sur demande explicite (defaut : dry-run, liste seulement).
if [ "${GITHUB_EVENT_NAME}" = "schedule" ] || [ "${APPLY_INPUT:-false}" = "true" ]; then
ARGS+=(--apply)
fi
python3 scripts/ci/sweep_stale_pr_branches.py "${ARGS[@]}"
1 change: 1 addition & 0 deletions docs/reference/scripts-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ Pipeline d'audit qualité et de **matrice de coût** (EPIC #8056) + audit séman
| `scripts/ci/measure_runner_demand.py` | **Baseline exhaustive de demande GitHub Actions** (#12704) : collecte une fenêtre UTC avec bisection anti-cap 1 000 + pagination de tous les jobs, mesure attente (`started_at-created_at`), travail runner (`completed_at-started_at`), provenance same-repo/fork/unknown et dénominateurs ; replay offline par `--input`. **Co-résidence** (#15574) : bloc `co_residence` (hôte présumé du préfixe du `runner_name`, pic et moyenne de concurrence par job) et bloc `runners_inventory` (`--runners`, slots enregistrés par hôte, trois états `measured`/`unavailable`/`not_collected`, jamais un parc vide). Exit `0` mesure valide / `2` instrument ou snapshot incomplet. Procédure : [docs/ci/self-hosted-runners.md](../ci/self-hosted-runners.md) · organe : `.github/workflows/runner-coresidence-advisory.yml` |
| `scripts/ci/manage_self_hosted_runner.py` + `self_hosted_runner_profiles.json` | **Cycle de vie Windows des runners éphémères isolés** (#12704) : profils distribués po-2023..po-2026 avec archive/SHA-256 épinglés ; commandes `install`, `register`, `verify`, `teardown` en dry-run par défaut, mutations uniquement avec `--apply`; compte local dédié, ACL négatives `.secrets`/SSH/gh, tokens via `ACTIONS_RUNNER_INPUT_*`, extraction anti-Zip-Slip/ADS et teardown borné par manifeste. `register --apply` est le bouton d’activation séparé, jamais lancé pendant la préparation. Procédure : [docs/ci/self-hosted-runners.md](../ci/self-hosted-runners.md) |
| `scripts/ci/pool_review_verdicts.py` | **Vue de triage du pool : verdict lu dans les corps, pas `reviewDecision`** (#16926) : sous token COMMENT-only (#15511) le champ `reviewDecision` vaut `null` à perpétitude sur ~82 % du pool y compris les PRs portant un `VERDICT: LGTM` argumenté — trier dessus fabrique le faux compte « sans review ». L'organe balaye le pool en GraphQL paginé borné (page 50 + retry 5xx unique : les fenêtres `reviews`+`comments` font 504/502 au-delà, mesures 2026-09-20), lit le préfixe `VERDICT:` en début de ligne sur les **deux surfaces** (reviews[] + commentaires de persona, marqueur importé du canon `check_unaddressed_nits`), latest-wins, et distingue `SANS-REVIEW` (aucune voix) de `VOIX-SANS-VERDICT` et des états réels CHANGES_REQUESTED/APPROVED qui gouvernent. `--gradient` rend la répartition par quartile d'âge (l'enseignement #16926 : les PRs vieillissent parce qu'elles portent une réserve vivante). Advisory : exit 0, ou 2 si le balayage est illisible. Tests : `scripts/tests/test_pool_review_verdicts.py` |
| `scripts/ci/sweep_stale_pr_branches.py` | **Pilote de balayage des retards de base** (#16915, mandat user 19/09 « les MAJ de rebase doivent être automatiques ») : énumère les PRs ouvertes (GraphQL — l'API REST rend `mergeable: null` sur 211/214 PRs, mesuré 2026-09-20), écarte par métadonnées nommées (brouillon/fork/non-mergeable), mesure le retard réel (`behind_by`, compare API), sélectionne les `--max-updates` plus anciennes candidates et **délègue** la mise à jour à `update_stale_pr_branches.py` par sous-processus (`--pr` explicites) — le pilote n'écrit jamais, l'organe porte seul ses neuf gardes (SHA épinglés, registre en vol, jamais de force-push/`--rebase`). Dry-run par défaut. Câblé par `.github/workflows/stale-branch-update-sweep.yml` (cron 6 h, cap 3/run). Le contrat DWELL presupposé (update-branch content-free ≠ re-arm du plancher, #16149) est verrouillé dans `scripts/tests/test_sweep_stale_pr_branches.py` |
| `scripts/mcp-maintenance/` | Maintenance MCP (config, docs, scripts) — cf `README_MCP_MAINTENANCE.md` |
| `scripts/validation/dispatch.py` + `matrix.yml` | Matrice de validation / dispatch |
| `scripts/genai-stack/genai.py` | GenAI Docker (ComfyUI + Qwen) + validation — cf [docs/genai/genai-services.md](../genai/genai-services.md) |
Expand Down
8 changes: 8 additions & 0 deletions scripts/ci/check_self_hosted_runner_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,14 @@
"review-coverage-advisory.yml",
"slides-build-advisory.yml",
"slow-lane.yml",
# stale-branch-update-sweep.yml (#16915, owner myia-po-2023:CoursIA) :
# pilote de rebase automatique des PRs quietes -- cron `23 */6 * * *`
# + workflow_dispatch, pur-Python + organe `update_stale_pr_branches.py`
# par sous-processus, aucun trigger pull_request (meme profil que
# pr-gate-stale-sweep, tranche 4 #14283). Permissions GH_TOKEN bornees
# par le workflow ; dry-run par defaut, --apply --max-updates 3.
# Rollback = revert de la PR (l'entree disparait de l'allowlist).
"stale-branch-update-sweep.yml",
"stale-guard-red-sweep.yml",
"translation-parity.yml",
"twin-parity-cron.yml",
Expand Down
Loading
Loading