Repository navigation
fix(gametheory,#13468): REPAIR4 GT-13b Kuhn + pedagogie Safe Subgame reintégrée (vs #13501 contenu supprimé) #5090
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Translation Guard | |
| # Purpose | |
| # ------- | |
| # PREVENT agents/humans from hand-editing translation-owned files on feature | |
| # branches. Translations are DERIVED artifacts (like the catalog), owned by: | |
| # - translation-sync.yml (per-PR auto-commit bot, same-repo PRs) | |
| # - the T1/T3/T4 toolchain (extract_cells_to_csv.py -> translate_csv.py | |
| # -> render_notebook.py, scripts/translation/) | |
| # | |
| # The source of truth for multilingual alignment is the NOTEBOOK in its source | |
| # language (FR) + the translation CSV (translations/<famille>/<serie>.csv). A | |
| # PR that edits a *_<lang>.ipynb or a translations/** CSV by hand breaks the | |
| # invariant: the next run of translation-sync.yml would regenerate (and | |
| # silently overwrite) the hand-edit. This is the exact analog of the | |
| # stale-catalog-silent-revert pattern (#2744/#2632), applied to translations. | |
| # | |
| # Owned files (FAIL if a non-bot author touches them): | |
| # - translations/** (the sync CSVs, derived from notebooks) | |
| # - MyIA.AI.Notebooks/**/*_{en,ru,pt,es,ar,fa,zh}.ipynb (rendered translations) | |
| # | |
| # Exceptions: | |
| # - github-actions[bot] (translation-sync.yml auto-commits) | |
| # - workflow_dispatch (emergency maintainer override) | |
| # | |
| # See #10038 (i18n umbrella), #10042 (grain C), #4957 (infra design). | |
| on: | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: translation-guard-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| guard: | |
| name: "No hand-edited translation files on feature branch" | |
| runs-on: ubuntu-latest | |
| steps: | |
| # Tranche 1 de #12690 : ce workflow n'interroge QUE le graphe de commits | |
| # et des NOMS de chemins (`git diff --name-only`, `git cat-file -e` pour | |
| # verifier l'existence d'un blob sans le lire, `git fetch origin main`). | |
| # Aucun blob historique n'est materialise -- `filter: blob:none` laisse | |
| # le graphe complet disponible et rend les blobs paresseux (jamais | |
| # transferes, jamais refetches). `sparse-checkout` limite le working | |
| # tree materialise aux seuls chemins requis : `scripts/ci/` (le helper | |
| # d'override) et `.github/` (le commentaire HTML invisible du bot). Le | |
| # clone plein transferait ~1,82 Gio (cf en-tete stale-base-warning.yml | |
| # pour la mesure de reference) ; ce checkout transfere <10 Mo. | |
| # Pourquoi c'est safe ici : | |
| # * `git diff --name-only BASE...HEAD -- <paths>` : graphe only. | |
| # * `git cat-file -e "BASE:$f"` : existence check, pas | |
| # lecture de contenu. | |
| # * `python3 scripts/ci/translation_override_required.py` : helper | |
| # sous scripts/ci/, | |
| # materialise par le | |
| # sparse-checkout. | |
| # See #11843 (pattern), #12690 (goulot narrow structurel). | |
| - name: "Checkout base (sparse-checkout + filter: blob:none, tranche 1 issue 12690)" | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| filter: blob:none | |
| sparse-checkout: | | |
| .github | |
| scripts/ci | |
| - name: Check for translation-owned file changes | |
| id: check | |
| env: | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| run: | | |
| set -euo pipefail | |
| # Allow the translation auto-sync bot to bypass this check entirely. | |
| if [ "$PR_AUTHOR" = "github-actions[bot]" ]; then | |
| echo "::notice title=Translation guard::Author is github-actions[bot] (translation-sync automation) — guard bypassed." | |
| echo "bypass=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # For workflow_dispatch (no pull_request context), skip. | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "::notice title=Translation guard::Manual dispatch — guard bypassed." | |
| echo "bypass=true" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # 3-dot diff: translation-owned files changed on the PR branch. | |
| # translations/** = sync CSVs (derived). *_<lang>.ipynb = rendered | |
| # translation notebooks (derived by render_notebook.py T4). Target | |
| # langs match check_perimeter.py TARGET_LANGS (en/es/ar/fa/zh/ru/pt), | |
| # the single source of truth (#10109). | |
| # | |
| # Hand-edit = modification d'un fichier dérivé déjà présent sur | |
| # origin/<base>. L'ajout d'un nouveau fichier dérivé (jamais présent | |
| # sur main, premier rendu d'un notebook source) est **légitime** : il | |
| # ne peut pas avoir été hand-édité puisque pas encore commit. On le | |
| # laisse passer ; translation-sync.yml le reprendra ensuite depuis | |
| # le CSV comme tout autre _en.ipynb. Cf. #10307 (cycle c.190 -- | |
| # premier cas déclencheur : PR #10306 deep_research_optimization_en | |
| # jamais rendu auparavant). | |
| ALL_CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD -- \ | |
| 'translations/**' \ | |
| 'MyIA.AI.Notebooks/**/*_en.ipynb' \ | |
| 'MyIA.AI.Notebooks/**/*_ru.ipynb' \ | |
| 'MyIA.AI.Notebooks/**/*_pt.ipynb' \ | |
| 'MyIA.AI.Notebooks/**/*_es.ipynb' \ | |
| 'MyIA.AI.Notebooks/**/*_ar.ipynb' \ | |
| 'MyIA.AI.Notebooks/**/*_fa.ipynb' \ | |
| 'MyIA.AI.Notebooks/**/*_zh.ipynb' \ | |
| || true) | |
| # Reduce to files that ALREADY EXIST on the base. New files | |
| # (additions) are produced by the legitimate rendering pipeline | |
| # for a notebook that wasn't translated before — see comment above. | |
| CHANGED="" | |
| for f in $ALL_CHANGED; do | |
| if git cat-file -e "origin/${{ github.base_ref }}:$f" 2>/dev/null; then | |
| CHANGED="$CHANGED $f" | |
| else | |
| echo "::notice title=Translation guard::$f -- new file on feature branch (not on base), allowed." | |
| fi | |
| done | |
| CHANGED="${CHANGED# }" # strip leading whitespace | |
| if [ -z "$CHANGED" ]; then | |
| echo "::notice title=Translation guard::No translation-owned files MODIFIED — clean." | |
| echo "violated=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::error title=Translation guard FAILED::PR modifies translation-owned files that are generated by automation ($CHANGED). See #10042 and #10038. Edit the source notebook (FR) instead; the bot re-derives translations." | |
| echo "" | |
| echo "## Translation Guard Violation" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "This PR modifies translation-owned (derived) files:" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| for f in $CHANGED; do | |
| echo "- \`$f\`" >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "### How to fix" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Translation CSVs (\`translations/**\`) and rendered notebooks (\`*_<lang>.ipynb\`) are **derived** from the source notebook + the T1/T3/T4 toolchain. Edit the **source notebook** (FR) instead; \`translation-sync.yml\` re-extracts, re-translates (changed cells only), and re-renders automatically." >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```bash' >> "$GITHUB_STEP_SUMMARY" | |
| echo "git checkout origin/main -- $CHANGED" >> "$GITHUB_STEP_SUMMARY" | |
| echo '```' >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "See [#10038](https://github.com/${{ github.repository }}/issues/10038) (i18n umbrella) and [#10042](https://github.com/${{ github.repository }}/issues/10042) (grain C)." >> "$GITHUB_STEP_SUMMARY" | |
| echo "violated=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Override path (#10332): when the violation path fires, the override | |
| # gate consults the dual-key (label `translation-override` AND comment | |
| # marker `[TRANSLATION-OVERRIDE] <motif>`). Both required -- the | |
| # cliquet is not disarmed. The decision lives in | |
| # `scripts/ci/translation_override_required.py` (testable, injectable | |
| # fetchers -- unit tests never touch the network). | |
| - name: Resolve translation-guard override (#10332) | |
| id: override | |
| if: steps.check.outputs.violated == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -uo pipefail | |
| python3 scripts/ci/translation_override_required.py \ | |
| --pr-number "$PR_NUMBER" > /tmp/override.json | |
| RC=$? | |
| echo "--- override verdict ---" | |
| cat /tmp/override.json | |
| OVERRIDE_APPLIED=$(python3 -c "import json; print(json.load(open('/tmp/override.json')).get('override_applied', False))") | |
| if [ "$OVERRIDE_APPLIED" = "True" ]; then | |
| MOTIF=$(python3 -c "import json; print(json.load(open('/tmp/override.json')).get('motif',''))") | |
| echo "override_applied=true" >> "$GITHUB_OUTPUT" | |
| echo "motif=$MOTIF" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "override_applied=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Exit code of the python script decides: 0 = pass, 1 = block. | |
| # We always proceed to the next step so we can print a structured | |
| # log either way; the `Fail if violated (or unsatisfied override)` | |
| # step below reads `override_applied`. | |
| exit 0 | |
| - name: Fail if violated (or unsatisfied override) | |
| if: steps.check.outputs.violated == 'true' && steps.override.outputs.override_applied != 'true' | |
| run: | | |
| REASON=$(python3 -c "import json; print(json.load(open('/tmp/override.json')).get('reason','unknown'))" 2>/dev/null || echo "unknown") | |
| echo "::error title=Translation guard FAILED::$REASON" | |
| gh pr comment "${{ github.event.pull_request.number }}" --body "$(cat <<EOF | |
| <!-- translation-guard-violation --> | |
| **Translation guard violation (#10332).** | |
| $REASON | |
| Three exits to clear this gate: | |
| 1. Restore the derived files (\`git checkout origin/main -- translations/ <lang-notebooks>\`) and edit the source notebook instead -- the bot re-derives. | |
| 2. Add the **\`translation-override\` label** AND a PR comment whose FIRST line is **\`[TRANSLATION-OVERRIDE] <motif>\`** (both required -- dual-key, see #10332). | |
| 3. Ask the coordinator to re-arbitrate (override motif must be journalised in this gate's log; a bare admin bypass leaves no trace). | |
| See #10332 for the override protocol. | |
| EOF | |
| )" 2>/dev/null || true | |
| exit 1 | |
| - name: Journalise accepted override (#10332) | |
| if: steps.override.outputs.override_applied == 'true' | |
| run: | | |
| MOTIF="${{ steps.override.outputs.motif }}" | |
| echo "::notice title=Translation guard::OVERRIDE accepted -- motif: $MOTIF. The override is auditable; see #10332." | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "## Translation Guard OVERRIDE accepted (#10332)" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Dual-key satisfied: label \`translation-override\` AND comment marker \`[TRANSLATION-OVERRIDE]\`." >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "**Motif:** $MOTIF" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "See [#10332](https://github.com/${{ github.repository }}/issues/10332) for the override protocol." >> "$GITHUB_STEP_SUMMARY" |