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
81 changes: 81 additions & 0 deletions .github/workflows/translation-hot-drift-advisory.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Translation Hot-Drift Advisory

# #15322 — une PR de realignement de prose qui modifie la source d'une
# cellule portant une traduction deposee, sans resynchroniser sa ligne CSV,
# fait rougir le cliquet hot-subset (#13551) SUR MAIN — donc sur toutes les
# PRs ouvertes qui l'heritent (36 PRs rouges d'un coup ; incidents documentes
# #15253 / #15136 / #15216). La lane qui voit le rouge le lit comme son
# propre defaut et brule un cycle sur un diff qu'elle n'a pas casse.
#
# Ce garde repond, par PR, la seule question que le cliquet ne peut pas
# poser : quelles cellules CE diff fait-il entrer dans le sous-ensemble
# chaud ? Le predicat est REUTILISE tel quel de
# scripts/translation/check_translation_sync.py (contrainte de l'issue :
# pas de second predicat qui puisse diverger du cliquet qu'il protege) ;
# le delta (cellules source changees entre merge-base et tete) isole la
# contribution propre de la PR — la derive heritee de main ne flagge jamais.
#
# Placement ADVISORY (point 2 de l'issue) : le verdict est publie sous le
# check-run « Translation hot-drift (base vs PR, advisory) » avec une
# conclusion neutre, nommant les cellules. Calibration sur 98 PRs notebook
# atterries (2026-08-20 -> 2026-09-10) : 6 declenchements, 6/6 vrais
# positifs (les 3 incidents documentes + 3 atterrissages repris apres coup
# que le tableau de l'issue ne listait pas). La bascule en bloquant attend
# la poursuite de cette mesure.

# Livré directement dans la voie rapide (#12567) : le verdict par-PR est
# rendu par `fast-lane-shadow.yml` via l'entrée TRANCHE2 de
# `scripts/ci/fast_lane_registry.py`. Ce fichier ne garde que
# `workflow_dispatch` (relance manuelle).
on:
workflow_dispatch:
inputs:
base_ref:
description: 'Base ref to ratchet against (default origin/main)'
required: false
default: 'origin/main'

permissions:
contents: read

concurrency:
group: translation-hot-drift-advisory-${{ github.ref }}
cancel-in-progress: true

jobs:
advisory:
name: Translation hot-drift (base vs PR, advisory)
# Même jambe que les siblings ratchet (#14959) : garde Python pur, sans
# secret. La garde same-repo ci-dessous saute les PRs de fork : aucun
# code de fork n'atteint le runner.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
filter: blob:none

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

# Le self-test passe FIRST et gate le garde : contrôles positifs,
# négatifs et d'attribution sur fixtures synthétiques + replay du cas
# fondateur (#15136, image.csv cell b07ca49a).
- name: Detector self-test (positive + negative + attribution)
run: python scripts/translation/check_pr_translation_drift.py --self-test

- name: Translation hot-drift unit tests
run: python -m pytest scripts/translation/tests/test_check_pr_translation_drift.py -q

# Refs reach the script via env, never inline in the run script
# (script-injection guard, same as the siblings).
- name: Hot-drift check (advisory)
env:
BASE_REF: ${{ github.event.inputs.base_ref }}
PR_BASE: ${{ github.base_ref }}
run: |
BASE="${BASE_REF:-origin/${PR_BASE:-main}}"
python scripts/translation/check_pr_translation_drift.py "$BASE"
12 changes: 12 additions & 0 deletions scripts/ci/check_self_hosted_runner_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,18 @@
# (LINUX_RUNNER_LABELS, meme profil que md-content-loss-gate tranche 5
# #13378). Rollback = revert de la PR (l'entree disparait de l'allowlist).
"notebook-plan-loss-gate.yml",
# #15322 (owner myia-po-2023:CoursIA) : vehicule workflow_dispatch-ONLY
# servant de cible d'identite au check-run absorbe par fast-lane
# (registre TRANCHE2) et de re-run manuel du self-test sur main.
# Detecteur check_pr_translation_drift.py (attribution par merge-base,
# predicat REUTILISE de check_translation_sync.check_csv -- aucune
# reimplementation), advisory pur-Python stdlib-only, conclusion neutre,
# jamais exit != 0 sur le verdict, aucun secret, aucun GITHUB_TOKEN cote
# job. Runner = jambe Linux containerisee (LINUX_RUNNER_LABELS, meme
# profil que repeated-prose-advisory.yml / markdown-deaccent-
# advisory.yml). Rollback = revert de la PR (l'entree disparait de
# l'allowlist).
"translation-hot-drift-advisory.yml",
}
GITHUB_HOSTED_LABELS = {
"ubuntu-latest",
Expand Down
38 changes: 38 additions & 0 deletions scripts/ci/fast_lane_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,44 @@ class Guard:
absorbed=True,
warn_rc=(2,),
),
# Translation hot-drift ATTRIBUTION, advisory (#15322): a prose-realignment
# PR that edits the source of a cell carrying a deposited translation
# without resyncing its CSV row turns the repo-wide hot-subset ratchet
# (#13551) red on MAIN -- and every open PR inherits the red (36 PRs at
# once; incidents #15253 / #15136 / #15216). The ratchet must stay
# repo-wide; what was missing is the per-PR question: which cells does
# THIS diff put into the hot subset? The predicate is reused verbatim from
# scripts/translation/check_translation_sync.py (issue constraint: no
# second predicate that could diverge from the ratchet it protects); the
# delta (source cells changed between merge-base and head) isolates the
# PR's own contribution, so inherited main-side drift never flags. Advisory
# per the issue: neutral conclusion naming the cells; blocking only after
# the trigger rate is measured (calibration on 98 landed notebook PRs:
# 6 flagged, 6/6 true positives -- the 3 documented incidents plus 3
# repaired-after-the-fact landings the issue table did not list).
# Source : translation-hot-drift-advisory.yml (stub dispatch-only).
Guard(
name="Translation hot-drift (base vs PR, advisory)",
source="translation-hot-drift-advisory.yml",
paths=[
"MyIA.AI.Notebooks/**/*.ipynb",
"translations/**/*.csv",
"scripts/translation/check_pr_translation_drift.py",
"scripts/translation/tests/test_check_pr_translation_drift.py",
".github/workflows/translation-hot-drift-advisory.yml",
],
pre_argv=[
"python", "scripts/translation/check_pr_translation_drift.py",
"--self-test",
],
argv=[
"python", "scripts/translation/check_pr_translation_drift.py",
"{base_ref}",
],
blocking=False,
needs_base=True,
absorbed=True,
),
]


Expand Down
22 changes: 22 additions & 0 deletions scripts/tests/test_check_self_hosted_runner_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,28 @@ def test_mixed_lean_and_linux_labels_are_rejected(tmp_path):
assert codes(result) == {"RUNNER_LABELS"}


def test_translation_hot_drift_advisory_is_allowlisted(tmp_path):
"""#15322/#15438 : le vehicule workflow_dispatch-ONLY du garde hot-drift
par-PR (absorbe fast-lane TRANCHE2) est autorise sur la jambe Linux
containerisee -- precedent repeated-prose-advisory / markdown-deaccent
(cible d'identite + re-run manuel du self-test). Son non-allowlistage
etait exactement le WORKFLOW_NOT_ALLOWED prouve par le run CPU frais."""
assert "translation-hot-drift-advisory.yml" in policy.SELF_HOSTED_WORKFLOW_ALLOWLIST
write_workflow(tmp_path, "translation-hot-drift-advisory", """
name: translation-hot-drift
on: workflow_dispatch
jobs:
advisory:
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
steps:
- run: echo safe
""")
result = policy.scan_workflows(tmp_path)
assert result.broken == []
assert result.violations == []
assert result.self_hosted_jobs == 1


def test_mixed_linux_and_fast_guards_labels_are_rejected(tmp_path):
# Mixing the two dedicated sets must stay a violation: a job eligible
# for both the Windows runners and the Linux container would make the
Expand Down
11 changes: 6 additions & 5 deletions scripts/tests/test_fast_lane.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,11 +671,12 @@ def test_tranche2_guards_are_absorbed_and_declare_their_warn_rc():
d'etre plus strict que ce qu'il remplace. Le ratchet, lui, porte un
pre-contrôle self-test."""
from fast_lane_registry import TRANCHE2
assert len(TRANCHE2) == 4, (
"la tranche 2 documente trois formes moteur portees par quatre "
"gardes (deux instances du ratchet autonome : failure-text + "
"output-flood) ; si le nombre change, le commentaire du registre "
"et ce test suivent")
assert len(TRANCHE2) == 5, (
"la tranche 2 documente trois formes moteur portees par cinq "
"gardes (deux ratchets autonomes bloquants : failure-text + "
"output-flood ; deux gardes d'iteration figure/texte ; le ratchet "
"autonome advisory translation hot-drift #15322) ; si le nombre "
"change, le commentaire du registre et ce test suivent")
for guard in TRANCHE2:
assert guard.absorbed, f"{guard.name} doit porter absorbed=True"
warners = {g.name for g in TRANCHE2 if g.warn_rc}
Expand Down
Loading
Loading