À quoi ça sert

La fiche théorique pre-commit décrit l'outil avec sa config Python type (Ruff, Black, isort). Mais ce repo n'a aucune ligne de Python — c'est du HTML, du CSS et du JavaScript vanilla. Du coup, plusieurs questions concrètes se posent au moment de l'install :

  • Quels hooks garder, lesquels enlever ?
  • Faut-il rajouter un hook lychee local alors que la CI en a déjà deux ?
  • Comment intégrer pre-commit au workflow GitHub Actions existant sans casser le pattern fail-loud/fail-soft qu'on a mis en place ?
  • Que faire si le tout premier run de pre-commit casse plein de fichiers d'un coup ?

Cette fiche raconte les décisions prises à chaque étape, plutôt que de redonner la doc générique.

Différence avec la fiche pre-commit

La fiche pre-commit explique l'outil dans l'absolu : la notion de hook Git, la config YAML, les hooks courants. Cette fiche-ci raconte un cas concret : comment l'installer sur un repo qui n'a pas le profil « Python type » attendu par la plupart des tutos.

Un exemple d'usage

Au tout premier pre-commit run --all-files, sur un repo qui avait déjà du contenu et une CI verte depuis des semaines :

bash
trim trailing whitespace.................................................Passed
fix end of files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook
  Fixing .lycheeignore

check yaml...............................................................Passed
check for merge conflicts................................................Passed
check for added large files..............................................Passed
detect private key.......................................................Passed

Un fichier corrigé tout seul (.lycheeignore n'avait pas de newline finale). Aucune autre erreur. La CI était verte, mais pas le repo : pre-commit attrape ce que les yeux humains laissent passer. C'est exactement à ça qu'il sert.

How-to : ce que j'ai fait, dans l'ordre

  1. Choisir les hooks pour ce repo

    La plupart des tutos pre-commit commencent par ruff ou black. Inutile ici : pas de Python. La règle que j'ai suivie : choisir les hooks selon le langage du repo, pas selon la liste standard.

    Au final, six hooks « hygiène générale » suffisent :

    • trailing-whitespace + end-of-file-fixer — propreté des fichiers texte.
    • check-yaml — valide les workflows GitHub Actions et le YAML de pre-commit lui-même.
    • check-merge-conflict — détecte les marqueurs oubliés.
    • check-added-large-files (max 500 ko) — bloque les gros binaires committés par erreur (capture d'écran lourde, node_modules…).
    • detect-private-key — par hygiène, même si peu probable sur ce repo.
  2. Décider : pas de hook lychee local

    Tentation : ajouter un hook qui lance lychee avant chaque commit. Refusé. Raisons :

    • La CI a déjà deux jobs lychee (interne strict + externe informatif). Doublonner localement ralentit chaque commit pour zéro gain.
    • Le check externe a besoin du réseau, ce qui casse le principe « pre-commit doit être rapide ». 2-3 secondes max.
    • Le check interne offline serait redondant avec ce que GitHub Actions fait en ~2 s, sans valeur ajoutée locale.

    Règle dégagée : ne pas dupliquer en pre-commit ce que la CI couvre déjà bien. pre-commit = filet rapide, CI = vérité définitive.

  3. Installer l'outil avec uv tool

    Pre-commit étant un outil global (comme Ruff ou UV lui-même), je l'ai installé avec uv tool plutôt que comme dépendance du repo :

    bash
    uv tool install pre-commit

    Avantage : disponible dans tous les repos, sans polluer le pyproject.toml du projet (qui n'existe d'ailleurs pas ici).

  4. Créer le YAML à la racine

    Piège : pre-commit cherche son fichier de config exactement à .pre-commit-config.yaml à la racine du repo. On ne peut pas le mettre dans .github/ ou ailleurs. À ne pas confondre avec le workflow CI lui-même, qui lui vit dans .github/workflows/ci.yml.

    bash
    devianotes/
    ├── .pre-commit-config.yaml          ← config pre-commit (racine)
    ├── .github/workflows/
    │   └── ci.yml                       ← workflow GitHub Actions
    └── ...
  5. Activer le hook Git local

    bash
    pre-commit install

    Ça crée .git/hooks/pre-commit. À refaire à chaque clone du repo (le dossier .git/ n'est jamais versionné par Git lui-même). À mentionner dans le README pour les futurs collaborateurs.

  6. Premier run sur tout l'existant

    bash
    pre-commit run --all-files

    Sur ce repo, un seul fichier corrigé (.lycheeignore sans newline finale). Sur un repo plus ancien ou plus gros, attends-toi à un diff plus large — ce n'est pas grave : tu re-stages et tu commits d'un coup le « grand nettoyage ».

    Le piège : si trop de fichiers changent

    Sur un gros repo qui n'a jamais eu pre-commit, le premier run peut modifier des centaines de fichiers (whitespace, newlines). Mieux vaut faire un commit dédié « chore: pre-commit run --all-files » après avoir installé pre-commit mais avant de mélanger avec d'autres changements. Le diff reste lisible.

  7. Ajouter le job pre-commit en CI

    pre-commit local n'est pas suffisant : il faut un filet de sécurité côté CI, sinon un git commit --no-verify ou un contributeur qui n'a pas pre-commit installé casse l'historique. J'ai ajouté un nouveau job dans ci.yml :

    yaml
    pre-commit:
      name: pre-commit (strict)
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v6
        - uses: actions/setup-python@v5
          with:
            python-version: "3.12"
        - uses: pre-commit/action@v3.0.1

    L'action officielle pre-commit/action installe pre-commit, met en cache les environnements des hooks, et lance pre-commit run --all-files. Trois lignes, c'est tout.

  8. Brancher pre-commit dans deploy.needs

    Le point crucial. Un job CI qui ne bloque pas le déploiement n'est qu'une décoration. Donc :

    yaml (avant)
    deploy:
      needs: link-check-internal
    yaml (après)
    deploy:
      needs: [pre-commit, link-check-internal]

    Côté pattern, ça respecte le fail-loud/fail-soft du projet : pre-commit rejoint le check de liens internes (strict) comme bloquant, pendant que le check de liens externes reste informatif. Cohérent : c'est de l'hygiène qu'on contrôle, donc bloquant.

  9. Vérifier le tout

    bash
    # Re-run en local pour confirmer que rien ne reste à fixer
    pre-commit run --all-files
    
    # Tenter un commit normal : les hooks doivent passer silencieusement
    git add .
    git commit -m "chore: setup pre-commit"

    Si le commit passe sans rejet, le setup est complet. Au prochain push, le job pre-commit s'exécutera en CI en parallèle des deux checks lychee, et bloquera le déploiement en cas de problème.

Ce que j'ai appris

  • Les hooks pre-commit se choisissent selon le langage du repo, pas selon la « liste standard » qu'on voit partout. Pour un repo non-Python, six hooks d'hygiène générale suffisent largement.
  • Pre-commit et CI sont complémentaires, pas concurrents. Pre-commit = rapide et local, pour éviter les allers-retours. CI = définitive et bloquante, pour ce qui sort du repo. On ne duplique pas ce que la CI fait déjà bien (ex : lychee chez moi).
  • Un job pre-commit en CI qui ne bloque pas le deploy ne sert à rien. Le mettre dans needs: de deploy est ce qui transforme l'outil en vrai filet de sécurité.
  • end-of-file-fixer a attrapé un truc dès le premier run. Preuve immédiate que ça apporte de la valeur même sur un repo « propre » à l'œil.
  • pre-commit install est par-machine, pas versionné. Ça doit aller dans le README pour les futurs cloneurs du repo, sinon ils contournent le filet local sans le savoir.

Pour aller plus loin