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
177 changes: 177 additions & 0 deletions .github/workflows/always-on-metadata-guards.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
name: Always-on metadata guards

# FUSION VAGUE 2 TRANCHE 1 (#14057, dispatch ai-01 msg-20260901T120252-k7qy3p)
# -- trois gardes/advisories always-on de METADONNEES en un seul workflow, un
# seul checkout, un seul slot de runner par PR. Patron #13384 (umbrella des
# cinq gardes metadata) applique a la vague suivante.
#
# Constat fondateur (mesure etape 1 de #14057, 2026-09-01) : la file CI tenait
# 166 runs en attente, et le cluster quasi-universel comptait, par fenetre,
# base-not-main 72 + stale-base-warning 44 + concurrency-conj-guard 33 = 149
# runs pour ~2-3 minutes de travail reel additionne. Comme #13384 l'a mesure,
# c'est le SLOT de runner, pas la minute-CPU, qui est la ressource rare.
#
# Chiffre (mesure avant PR, exigee par #14057) :
# - evenement nominal (synchronize sur PR base main) : 3 runs -> 1
# = -2 runs/evenement (-67 %) ;
# - fenetre de la mesure etape 1 : 149 -> ~72 runs (la cadence de
# l'umbrella suit l'union des declencheurs, deja porte par base-not-main)
# = -52 % estime.
# Sources restantes (tranche suivante, bloquees par PRs en vol) :
# always-on-guards.yml (#13922) et orphaned-delivery-scan.yml (#13947).
#
# Sources absorbees (les fichiers RESTENT, dormants -- declencheurs
# pull_request retires, jobs conserves verbatim pour la tracabilite, patron
# #13384) :
#
# base-not-main-advisory.yml -> advisory base != main (organes ci-dessous,
# porte le verrou #13232 : verdict METADONNEES)
# stale-base-warning.yml -> advisory base >14j (graphe de commits seul)
# concurrency-conj-guard.yml -> garde BLOQUANT (conjonction #13488)
#
# PIEGE NOMME PAR #13384 : `scripts/pr_gate.py::derive_always_on_jobs` (canary
# de livraison, regle 8) DERIVE l'ensemble des jobs tournant sur CHAQUE
# pull_request depuis ce repertoire. La fusion change cet ensemble : les trois
# anciens noms de job disparaissent, le nom du job umbrella ci-dessous les
# remplace. La canary reste armee tant que ce workflow garde un declencheur
# pull_request SANS filtre `paths:` -- ne jamais en ajouter.
#
# VERROU #13232 (criteres ai-01) : les organes de cette umbrella lisent des
# METADONNEES de la PR (baseRefName, base.sha, age du graphe de commits) ou
# les fichiers de workflows -- PAS le diff. Un `paths:` desarmerait
# base-not-main sur les PR ne touchant pas les chemins listes, precisement
# la surface qu'il doit scanner. Reduire le fan-out par types:/concurrency,
# jamais par paths: (regle 2 d'#10045 : un check filtre par chemins rapporte
# `pending` a vie sur les PR hors scope).
#
# SEMANTIQUE DE BLOCAGE (patron #13384) : l'organe BLOQUANT (concurrency-conj)
# porte `continue-on-error: true` et un `id:` ; l'etape finale AGREGAT fait
# rougir le job s'il a echoue -- sans cela, un echec premature empecherait les
# organes suivants de tourner. Les deux advisories tournent EN PREMIER (elles
# ne peuvent pas rougir) pour que les labels restent poses meme quand le
# bloquant echoue.
#
# Le nom du job ne porte ni "advisory" ni "optional" : il agrege un verdict
# bloquant, `is_advisory()` de pr_gate.py ne doit pas le taire.

on:
pull_request:
types: [opened, reopened, synchronize, edited, ready_for_review]
# PAS de `paths:` -- canary + #13232 + regle 2 d'#10045.
# PAS de filtre `branches:` -- l'organe base-not-main doit voir les PR
# dont la base n'est PAS main (c'est sa raison d'etre, #10918).
workflow_dispatch:

permissions:
contents: read
pull-requests: write
issues: write

# #11649 : event_name dans la cle -- les evenements ne s'annulent pas entre
# eux. Pas de trigger push:main ici, donc la conjonction refoulee par
# #13488 (push + github.ref + cancel-in-progress literal) ne s'applique pas.
concurrency:
group: always-on-metadata-guards-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
always-on-metadata-guards:
name: "Always-on metadata guards -- 3 organes, 1 checkout"
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
# Checkout UNION des trois organes : fetch-depth 0 + blob:none (le
# graphe complet est requis par l'organe stale-base -- rev-list BASE..main
# et dates de commits, cf #11835 ; les blobs restent paresseux) ;
# sparse-checkout .github + scripts (organes base-not-main et
# concurrency-conj n'ont besoin que de leurs scripts).
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: blob:none
sparse-checkout: |
.github
scripts

- uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install PyYAML if absent
# Pattern partage avec l'umbrella #13384 : import-test en dry-run,
# fallback pip install silencieux. Sans cette etape, exit 2
# BROKEN_INSTRUMENT masquerait la conjonction en rouge de complaisance
# (cf #11850).
run: python -c "import yaml" 2>/dev/null || pip install --quiet pyyaml

# ------------------------------------------------------------------
# ORGANE 1 (advisory) : base-not-main. Porte depuis
# base-not-main-advisory.yml (partie (a) de #10918). Ne lit QUE l'API
# `gh pr` (scripts/base_not_main.py:143 -- baseRefName + title), jamais
# l'arbre. Skip le travail quand base == main (~95 % des PRs).
# ------------------------------------------------------------------
- name: "Organe base-not-main : signaler une base != main (advisory)"
if: github.event_name == 'pull_request' && github.event.pull_request.base.ref != 'main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python scripts/base_not_main.py --pr "${{ github.event.pull_request.number }}"

# ------------------------------------------------------------------
# ORGANE 2 (advisory) : stale-base-warning. Porte verbatim depuis
# stale-base-warning.yml. Ne lit AUCUN contenu de blob : rev-list et
# show -s interrogent le graphe seul (#11835). Poser label + commentaire
# quand la base a >14j.
# ------------------------------------------------------------------
- name: "Organe stale-base : avertissement base >14j (advisory)"
if: github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'main'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
STALE_DAYS: "14"
BASE_REF: ${{ github.event.pull_request.base.sha }}
run: |
git fetch origin main
BASE_SHA="$BASE_REF"
MAIN_SHA="$(git rev-parse origin/main)"
if [ "$BASE_SHA" = "$MAIN_SHA" ]; then
echo "Base is up-to-date."
exit 0
fi
BEHIND=$(git rev-list "$BASE_SHA..origin/main" --count)
BASE_DATE=$(git show -s --format=%ct "$BASE_SHA" 2>/dev/null || echo 0)
MAIN_DATE=$(git show -s --format=%ct "$MAIN_SHA" 2>/dev/null || echo 0)
if [ "$BASE_DATE" -eq 0 ] || [ "$MAIN_DATE" -eq 0 ]; then
echo "Could not determine commit dates."
exit 0
fi
AGE_DAYS=$(( (MAIN_DATE - BASE_DATE) / 86400 ))
echo "Base age: ${AGE_DAYS} days, behind: ${BEHIND} commits"
if [ "$AGE_DAYS" -le "$STALE_DAYS" ]; then
exit 0
fi
echo "::warning::Base is ${AGE_DAYS}d behind main (${BEHIND} commits)"
gh pr comment "$PR_NUMBER" --body "## Base Branch Stale Warning - ${AGE_DAYS}d behind main (${BEHIND} commits). Consider rebasing." 2>/dev/null || true
gh label create "base-stale-14d" --description "PR base >14d behind main" --color "FFA500" --force 2>/dev/null || true
gh pr edit "$PR_NUMBER" --add-label "base-stale-14d" 2>/dev/null || true

# ------------------------------------------------------------------
# ORGANE 3 (BLOQUANT) : concurrency-conj-guard. Porte depuis
# concurrency-conj-guard.yml (#13488) : refoule la conjonction
# push:main + github.ref + cancel-in-progress literal. Scanne les
# workflows du depot -- tourne sur tout evenement, y compris dispatch.
# ------------------------------------------------------------------
- name: "Organe concurrency-conj : conjonction annulation-cascade (#13488)"
id: conj
continue-on-error: true
run: python scripts/ci/check_concurrency_conj.py --check --json

# ------------------------------------------------------------------
# AGREGAT (patron #13384) : fait rougir le job si l'organe bloquant a
# echoue, APRES que les advisories ont pose leurs labels.
# ------------------------------------------------------------------
- name: Agregat
if: always() && steps.conj.outcome == 'failure'
run: |
echo "::error::Organe concurrency-conj en echec (conjonction push:main + github.ref + cancel-in-progress litteral reintroduite ? Cf #13488 / #13372)."
exit 1
18 changes: 13 additions & 5 deletions .github/workflows/base-not-main-advisory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,21 @@ name: Base-not-main advisory
# ADVISORY, never blocking (#10918): label + comment only, the job exits 0
# regardless. Idempotent: the comment is marker-guarded and updated (not
# re-posted) on synchronize.
#
# DORMANT depuis #14057 (Vague 2 tranche 1, patron #13384) : l'organe
# pull_request ci-dessous est absorbe par always-on-metadata-guards.yml --
# un seul workflow, un seul checkout, un seul slot de runner par PR (149
# runs/fenetre pour ~3 min de travail reel, mesure etape 1 de #14057). Le
# job est conserve verbatim pour la tracabilite ; le declencheur
# pull_request est retire. Le verrou #13232 (verdict METADONNEES, jamais de
# paths:) est porte par l'umbrella, verrouille par
# scripts/tests/test_base_not_main_no_paths_filter.py.

on:
pull_request:
types: [opened, reopened, synchronize, edited, ready_for_review]
# #12773 tranche 1a + ai-01 decision 2026-08-27 : ce workflow lit UNIQUEMENT
# l'API `gh pr` (cf scripts/base_not_main.py:143 -- baseRefName + title), pas
# l'arbre. Son verdict depend des METADONNEES de la PR, pas du DIFF.
workflow_dispatch:
# #12773 tranche 1a + ai-01 decision 2026-08-27 : l'organe lit UNIQUEMENT
# l'API `gh pr` (cf scripts/base_not_main.py:143 -- baseRefName + title),
# pas l'arbre. Son verdict depend des METADONNEES de la PR, pas du DIFF.
# Aucune surface de paths: ne le reduit sans le rendre muet sur tout PR
# qui ne touche pas exactement ces 3 chemins. Surface nominale = toutes
# les pull_request. Reduction de fan-out via types:/concurrency, pas paths:.
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/concurrency-conj-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ name: concurrency-conj-guard
# successifs detruit la moitie de la verification. Cet organe rend la
# regression impossible a relivrer : la prochaine PR qui reintroduit la
# conjonction rougit ici, nomme le fichier + la ligne + la forme du fix.
#
# DORMANT depuis #14057 (Vague 2 tranche 1, patron #13384) : l'organe
# pull_request ci-dessous est absorbe par always-on-metadata-guards.yml --
# un seul workflow, un seul checkout, un seul slot de runner par PR (149
# runs/fenetre pour ~3 min de travail reel, mesure etape 1 de #14057). Le
# job est conserve verbatim pour la tracabilite ; le declencheur
# pull_request est retire.

on:
pull_request:
workflow_dispatch:

permissions:
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/stale-base-warning.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
name: Stale Base Branch Warning

# DORMANT depuis #14057 (Vague 2 tranche 1, patron #13384) : l'organe
# pull_request ci-dessous est absorbe par always-on-metadata-guards.yml --
# un seul workflow, un seul checkout, un seul slot de runner par PR (149
# runs/fenetre pour ~3 min de travail reel, mesure etape 1 de #14057). Le
# job est conserve verbatim pour la tracabilite ; le declencheur
# pull_request est retire.

on:
pull_request:
types: [opened, synchronize, reopened]
branches: [main]
workflow_dispatch:

permissions:
Expand Down
9 changes: 6 additions & 3 deletions scripts/notebook_tools/audit_workflow_path_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@
# porte leurs organes metadata + le moteur fast-lane.
"always-on-guards.yml",
"translation-guard.yml",
# Les quatre gardes fusionnes ci-dessus sont DORMANTS depuis #13384
# (declencheurs pull_request retires, jobs conserves en reference) :
# #14057 (Vague 2 tranche 1) : fusion des trois gardes metadata always-on
# (base-not-main-advisory, stale-base-warning, concurrency-conj-guard --
# tous dormant) en une seule umbrella non filtree qui porte leurs organes.
"always-on-metadata-guards.yml",
# Les workflows fusionnes ci-dessus sont DORMANTS (#13384, #14057 :
# declencheurs pull_request retires, jobs conserves en reference) :
# retires de cette liste, ils ne declenchent plus sur les PR.
# catalog-pr-guard.yml retire par #11012 : le workflow n'a jamais tourne sur
# une PR (0 run pull_request), c'est catalog-drift.yml qui tient la ligne.
Expand All @@ -57,7 +61,6 @@
"regression-guard.yml",
# Advisories bon marche
"repo-size-advisory.yml",
"stale-base-warning.yml",
}


Expand Down
23 changes: 15 additions & 8 deletions scripts/tests/test_base_not_main_no_paths_filter.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
#!/usr/bin/env python3
"""Lock test for ``.github/workflows/base-not-main-advisory.yml`` (#13232 + #13193).
"""Lock test for the base-not-main organ (#13232 + #13193 + #14057).

The workflow reads PR-level metadata via the ``gh`` API ONLY
L'organe vit depuis #14057 (Vague 2 tranche 1, patron #13384) dans
``.github/workflows/always-on-metadata-guards.yml`` ; le fichier source
``base-not-main-advisory.yml`` reste DORMANT (declencheur pull_request
retire, job conserve verbatim pour la tracabilite).

The organ reads PR-level metadata via the ``gh`` API ONLY
(``scripts/base_not_main.py`` uses ``gh pr view --json baseRefName,title``)
and never inspects the working tree. Its verdict therefore depends on
PR METADATA, not the DIFF. Per the criterion ai-01 wrote in #13232:
Expand All @@ -12,9 +17,10 @@

A ``paths:`` filter on a metadonnee-dependant workflow silently turns
the check into a no-op on the very PRs that need it most: ones that do
NOT touch the listed files. This test reads the YAML and asserts no
``paths:`` block is present under ``on.pull_request``. If a future
editor re-adds one, the test fails.
NOT touch the listed files. This test reads the UMBRELLA's YAML and
asserts no ``paths:`` block is present under ``on.pull_request``, and
that the union trigger types cover the five events the advisory must
react to. If a future editor re-adds one, the test fails.

Run::

Expand All @@ -28,7 +34,7 @@
import yaml

REPO_ROOT = Path(__file__).resolve().parents[2]
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "base-not-main-advisory.yml"
WORKFLOW = REPO_ROOT / ".github" / "workflows" / "always-on-metadata-guards.yml"


def _load_workflow() -> dict:
Expand All @@ -55,9 +61,10 @@ def test_no_paths_filter_under_pull_request():
pr_block = on_block.get("pull_request", {})

assert "paths" not in pr_block, (
f"base-not-main-advisory.yml MUST NOT carry paths: under "
f"always-on-metadata-guards.yml (umbrella de l'organe base-not-main "
f"depuis #14057) MUST NOT carry paths: under "
f"on.pull_request (decision ai-01 #13232, 2026-08-27). "
f"Workflow reads PR METADATA only via gh api (baseRefName, "
f"Organ reads PR METADATA only via gh api (baseRefName, "
f"title). Got: {sorted(pr_block.keys())}"
)

Expand Down
4 changes: 4 additions & 0 deletions scripts/tests/test_variation_tag_required.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ def test_check_name_is_not_advisory_friendly():
# non-filtree (un `paths:` desermerait tag, light-cap, genre-signals et
# lane-claim d'un seul geste).
".github/workflows/always-on-guards.yml": "porte les organes metadata des cinq gardes fusionnes (#13384)",
# #14057 (Vague 2 tranche 1) : l'umbrella porte l'organe base-not-main
# (lit baseRefName, metadonnee) -- c'est SA surface pull_request qui doit
# rester non-filtree.
".github/workflows/always-on-metadata-guards.yml": "porte l'organe base-not-main (lit baseRefName, metadonnee) (#14057)",
".github/workflows/variation-tag-guard.yml": "lit le tag Grain: du body",
".github/workflows/variation-light-genre.yml": "lit le tag Grain: du body",
".github/workflows/base-not-main-advisory.yml": "lit baseRefName (metadonnee)",
Expand Down
Loading