Skip to content

fix(ci,#20007): les jonctions NTFS ne sont plus invisibles a clean_tolerated_artifacts - #20010

Merged
myia-ai-01 merged 1 commit into
mainfrom
fix/prune-junction-reparse
Oct 9, 2026
Merged

myia-ai-01 merged 1 commit into
mainfrom
fix/prune-junction-reparse

Conversation

@coursia-lane-po-2027

Copy link
Copy Markdown
Contributor

Grain: MED/tooling — lane myia-po-2027:CoursIA — prev: MED/notebook-dotnet #19823

fix(ci,#20007): les jonctions NTFS ne sont plus invisibles a clean_tolerated_artifacts

Path.is_symlink() ne reconnait que les liens de type name surrogate. Une jonction NTFS — reparse point IO_REPARSE_TAG_MOUNT_POINT, le geste courant pour partager un node_modules ou un cache entre worktrees — rend False sur les trois voies, tout en portant bel et bien FILE_ATTRIBUTE_REPARSE_POINT.

Le defaut

Un artefact tolere qui est une jonction tombait donc dans la branche is_dir(), ou :

shutil.rmtree(target, ignore_errors=True)   # OSError: Cannot call rmtree on a symbolic link
if not target.exists():                     # la jonction est toujours la
    removed.append(rel)                     # -> jamais atteint

shutil.rmtree detecte pourtant la jonction et leve — mais ignore_errors=True avale l'exception. La jonction survivait, removed la taisait, et le git worktree remove qui suit (sans --force) echouait sur l'untracked restant : le rapport annoncait REMOVE pour un --apply incapable d'aboutir. C'est exactement le « removable n'est pas une prevision de applied » que le docstring de clean_tolerated_artifacts (#14619) dit corriger, et la classe de l'incident jonctions po-2023 (c.525).

Mesures

Predicats sur une jonction mklink /J reelle (Windows 11, Python 3.13) :

Predicat Reponse
Path.is_symlink() False
os.path.islink() False
stat.S_ISLNK(os.lstat(p).st_mode) False
os.lstat(p).st_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT True (0x410 = reparse point + directory)

Controle negatif — la logique d'origine rejouee telle quelle sur une jonction reelle : removed == [], jonction survivante, cible intacte. Sans ce controle, le test ajoute ne prouverait rien.

Geste de suppression : os.unlink() retire une jonction sans suivre sa cible (mesure : la cible et son contenu survivent), exactement comme un lien symbolique. Seul le predicat devait changer, pas le geste — la branche unlink existait deja et etait correcte.

Frequence : zero observe. Le scan prealable de 29 351 repertoires (po-2027, 08/10) a trouve 0 reparse point. Le defaut est donc latent — il est signale pour la classe qu'il laisse ouverte, pas pour un incident.

Le correctif

Un helper is_link_like(p) : is_symlink() ou FILE_ATTRIBUTE_REPARSE_POINT, avec repli False sur OSError/AttributeError (plateformes sans st_file_attributes) — on retombe alors sur le comportement d'avant, jamais sur une suppression. Le predicat est utilise dans clean_tolerated_artifacts, et la liste des garde-fous du docstring le nomme.

Validation

  • python -m pytest scripts/tests/test_prune_merged_worktrees.py : 139 passed, 2 skipped, 0 failed (805,9 s — les tests d'intégration lancent de vrais git/gh)
  • Controle negatif ci-dessus : le defaut est reproduit avant le fix
  • Le test de retrait verifie les deux moities du contrat : la jonction est retiree et listee, et la cible survit avec son contenu (on retire le lien, jamais ce qu'il pointe)
  • ruff check sur les 2 fichiers : 49 erreurs — la baseline de main a l'identique (mesuree par git stash + re-run ; le depot ne gate pas ruff) : zero finding net introduit

Perimetre

2 fichiers :

  • scripts/ci/prune_merged_worktrees.py — helper, predicat, docstring ;
  • scripts/tests/test_prune_merged_worktrees.py — 4 tests, premiere couverture de clean_tolerated_artifacts (aucun test ne l'exercait avant, grep sur def test_ + clean_tolerated : 0 hit).

Aucun elargissement du predicat : un repertoire ordinaire, un fichier ordinaire et un chemin absent ne sont pas des liens (TestIsLinkLike), donc la branche unlink ne s'ouvre pas plus large qu'avant.

Closes #20007

🤖 Generated with Claude Code

…lerated_artifacts

`Path.is_symlink()` ne reconnait que les liens de type *name surrogate* : une
jonction NTFS (reparse point IO_REPARSE_TAG_MOUNT_POINT, le geste courant pour
partager un node_modules ou un cache entre worktrees) rend False sur les trois
voies -- is_symlink, os.path.islink, S_ISLNK(st_mode) -- tout en portant
FILE_ATTRIBUTE_REPARSE_POINT (mesure sur une jonction mklink /J : 0x410).

Un artefact tolere qui est une jonction tombait donc dans la branche is_dir(),
ou shutil.rmtree leve "Cannot call rmtree on a symbolic link" -- un OSError
avale par ignore_errors=True. L'artefact n'etait ni retire ni liste, et le
`git worktree remove` qui suit (sans --force) echouait sur l'untracked restant :
le rapport annoncait REMOVE pour un --apply incapable d'aboutir -- exactement le
"removable n'est pas une prevision de applied" que le docstring de
clean_tolerated_artifacts (#14619) dit corriger, et la classe de l'incident
jonctions po-2023 (c.525).

Correctif : is_link_like(p) = is_symlink() OU FILE_ATTRIBUTE_REPARSE_POINT, avec
repli False sur OSError/AttributeError (plateformes sans st_file_attributes) --
on retombe alors sur le comportement d'avant, jamais sur une suppression.
os.unlink retire une jonction sans suivre sa cible (mesure : la cible et son
contenu survivent), donc le geste de la branche lien convient aux deux formes :
seul le predicat changeait.

Preuves :
- controle negatif : la logique d'origine rejouee telle quelle sur une jonction
  reelle rend removed == [], jonction survivante, cible intacte -- le defaut est
  reproduit avant le fix, pas suppose
- pytest scripts/tests/test_prune_merged_worktrees.py : 139 passed, 2 skipped,
  0 failed ; le test de retrait verifie les deux moities du contrat (la jonction
  est retiree ET listee, et la cible survit) ; premiere couverture de
  clean_tolerated_artifacts, qu'aucun test n'exercait
- ruff check sur les 2 fichiers : 49 erreurs = baseline de main a l'identique
  (mesuree par git stash + re-run) -- zero finding net introduit ; le depot ne
  gate pas ruff, la comparaison est donc explicite

Frequence, honnetement : zero observe SUR CE SIEGE (scan de 29 351 repertoires,
0 reparse point). La ligne du dashboard global portant "Recette : delier
jonctions node_modules AVANT git worktree remove" est une compensation manuelle
derivee ailleurs -- preuve que la classe est rencontree en pratique, mais aucune
mesure ne relie ce defaut a un incident.

Closes #20007

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

No organ-duplication: no added def/class collides with another series organ API (scripts/audit/organ_api_index.yaml).

Detector: python scripts/audit/detect_organ_duplication.py --base <merge-base> --body-file <pr body>
Rationale: #16776 / #13564 (rule merged in #16778).

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Path-collision (organ #13359/#13615)

Cette PR #20010 (fix(ci,#20007): les jonctions NTFS ne sont plus invisibles a clean_tolerated_artifacts) touche au moins un chemin de fichier aussi modifie par d'autres PRs ouvertes. Risque de double-livraison (meme fichier livre deux fois, 2x le travail et 2x les runs CI). Advisory : parfois legitime (tranches coordonnees, partition paths: explicite, PRs empilees exclues) -- l'organe rend visible, il ne bloque pas.

Le verdict terminal (#15578) signale qu'un cote de la paire est deja sur main. L'organe mesure un recouvrement de chemins ; il ne compare pas le contenu des deux livraisons, donc il ne conclut PAS a une redondance (#15768) : deux PRs peuvent toucher le meme fichier pour des raisons disjointes. L'arbitrage reste a la lane ou au coordinateur.

@jsboige

jsboige commented Oct 9, 2026

Copy link
Copy Markdown
Owner

[ADJOINT PREFLIGHT]
schema: 1
lane: myia-po-2025:CoursIA-2
pr: 20010
head: fa92e97
complete: true
body: read
comments-reviewed: 2
reviews-reviewed: 0
threads-reviewed: 0
threads-unresolved: 0
surfaces-sha256: adabd47165e38b7793a4ab1310d9f61a657186b9fd4d7cf55d52a8bcf25c8d12
diff-files: 2
diff-additions: 117
diff-deletions: 3
checks: latest-wins-green
b0: clear
scope: pass
domain: not-applicable
verdict: READY
organ: check_adjoint_prevalidation.py
organ-command: python scripts/check_adjoint_prevalidation.py --derive-verdict 20010
organ-rc: 0
[/ADJOINT PREFLIGHT]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants