Aller au contenu principal

Protéger la branche main sur GitHub : pourquoi passer par une pull request plutôt que fusionner en local

Un merge fait sur sa machine puis poussé directement sur main ne laisse aucune chance de rattraper une erreur avant qu'elle soit partagée. Cet article explique ce que la protection de branche et la pull request ajoutent, comment configurer une règle sur GitHub étape par étape, le cas du développeur seul, et les pièges de configuration qui rendent la protection inutile.

Outils & environnement ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

Sur un nouveau dépôt GitHub, n'importe quelle personne disposant des droits d'écriture peut pousser ce qu'elle veut sur main, à tout moment, y compris un push forcé qui remplace l'historique. C'est le réglage par défaut, et beaucoup de projets le gardent pendant des années sans y penser.

Le geste qui en découle paraît anodin : finir sa fonctionnalité, revenir sur main, lancer git merge, puis git push. Trente secondes, aucun clic sur GitHub. Ce raccourci fonctionne tant que personne ne se trompe. Le jour où le merge embarque un fichier de configuration local, un test cassé ou un conflit mal résolu, l'erreur est sur la branche que tout le monde récupère et que le serveur de production déploie.

Ce qui manque dans ce flux, c'est un point d'arrêt entre « mon code est prêt » et « mon code est sur main ». La pull request crée ce point d'arrêt, et la protection de branche le rend obligatoire. Les deux se règlent en dix minutes, et la suite montre comment, avec les cas où la configuration se retourne contre toi.

Le problème : main sert de brouillon partagé

Imaginons une équipe de trois développeurs sur une application Node.js. Le premier fusionne sa branche en local, oublie de récupérer les derniers changements, résout un conflit en gardant sa version et pousse. Le deuxième a poussé un correctif dix minutes plus tôt, que ce merge vient d'écraser sans bruit. Le troisième fait un git pull le lendemain matin et hérite d'une application qui ne démarre plus, sans savoir lequel des commits de la veille est en cause.

Aucune de ces trois personnes n'a fait quelque chose d'extraordinaire. Le problème vient de la configuration, qui laisse chaque erreur individuelle atteindre directement la branche commune. Les tests existent peut-être, mais rien n'oblige à les lancer avant d'intégrer. La relecture de code est peut-être une règle d'équipe, mais rien ne l'impose techniquement.

Le cas extrême est le push forcé. Après un rebase ou un reset local, un git push --force sur main remplace l'historique distant par celui de la machine qui pousse. Les commits des autres disparaissent du dépôt partagé, et seuls leurs clones locaux en gardent une copie. Les commandes de réécriture d'historique et leurs effets sont détaillées dans l'article sur merge, rebase, reset, revert, cherry-pick et reflog.

Le principe : main ne reçoit que du code qui a passé un contrôle

Une pull request est une demande d'intégration : une branche est proposée pour fusion dans une autre, et GitHub affiche les différences, les commentaires et le résultat des vérifications automatiques avant que quiconque appuie sur le bouton de fusion. Elle transforme un geste individuel et invisible en étape visible, datée et commentable.

La protection de branche ajoute la contrainte. Une fois activée sur main, GitHub refuse tout push direct, interdit la suppression de la branche et le push forcé, et n'autorise la fusion qu'une fois les conditions remplies : une relecture approuvée, des tests passés, ou les deux. GitHub propose deux mécanismes pour cela, les anciennes règles de protection de branche et les rulesets, plus récents, qui offrent les mêmes contrôles avec quelques avantages : plusieurs règles peuvent se cumuler, elles peuvent être désactivées sans être supprimées, et toute personne ayant accès en lecture au dépôt peut les consulter.

Sans protection merge en local git push main aucun contrôle avant partage Avec protection branche pull request tests + relecture main Le push direct sur main est refusé : la seule entrée passe par le contrôle.
Le même changement, avec et sans point de contrôle avant la branche partagée.

Ce point de contrôle sert aussi de mémoire. Six mois plus tard, la pull request explique pourquoi une modification a été faite, qui l'a relue et quels tests passaient à ce moment-là. Un merge local poussé ne laisse qu'un commit de fusion sans contexte. Les différents modèles de branches qui s'appuient sur ce mécanisme, GitHub Flow, Git Flow ou trunk-based, sont comparés dans l'article sur les stratégies de branches Git sur GitHub.

Mettre en place la protection, étape par étape

1. Vérifier que ton offre GitHub le permet

Point rarement mentionné et source de beaucoup de recherches inutiles : avec GitHub Free, les rulesets et les branches protégées ne sont disponibles que sur les dépôts publics. Sur un dépôt privé, il faut un compte GitHub Pro, ou une organisation sur l'offre Team ou Enterprise. Un dépôt privé personnel sur l'offre gratuite affiche les écrans de configuration, mais les règles n'y sont pas appliquées.

2. Créer une règle pour la branche par défaut

Dans le dépôt, le chemin passe par l'onglet Settings, puis Rules et Rulesets, puis New ruleset et New branch ruleset. Les réglages qui comptent :

  • Enforcement status sur Active, sinon la règle existe sans rien bloquer.
  • Target branches : ajouter la branche par défaut, ce qui suit automatiquement un éventuel renommage de main.
  • Restrict deletions et Block force pushes cochés.
  • Require a pull request before merging, avec le nombre d'approbations voulu et, si utile, l'obligation de résoudre les conversations ouvertes avant fusion.
  • Require status checks to pass, avec les vérifications automatiques à exiger (étape suivante).

La liste de contournement, la bypass list, reste vide par défaut : la règle s'applique alors à tout le monde, propriétaire du dépôt compris. C'est le comportement souhaité dans la grande majorité des cas.

3. Brancher un test qui bloque la fusion

Une relecture humaine laisse passer beaucoup de choses. Un workflow GitHub Actions qui lance les tests sur chaque pull request donne un verdict que personne ne peut ignorer.

# .github/workflows/ci.yml
name: CI
 
on:
  pull_request:
    branches: [main]
 
jobs:
  tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: npm ci
      - run: npm test

Une fois ce workflow exécuté au moins une fois, le job tests apparaît dans la liste proposée par Require status checks to pass. Le déclencheur pull_request n'est pas un détail : un workflow lancé à la main ou par un autre événement ne compte pas comme vérification requise sur une pull request. La chaîne complète, des tests jusqu'au déploiement, est l'objet de la formation CI/CD avec GitHub Actions.

4. Changer son flux de travail local

Côté terminal, le changement tient en quelques commandes. L'outil en ligne de commande gh, édité par GitHub, évite même d'ouvrir le navigateur.

git switch main
git pull --ff-only
git switch -c feature/export-csv
 
# ... travail et commits ...
 
git push -u origin feature/export-csv
gh pr create --fill
gh pr checks --watch
gh pr merge --squash --delete-branch

Une tentative de push direct sur main est désormais refusée par le serveur, avec un message de ce type :

remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: - Changes must be made through a pull request.
 ! [remote rejected] main -> main (push declined due to repository rule violations)

Ce refus est la protection qui fonctionne. Le réflexe consiste à créer une branche depuis l'état actuel avec git switch -c, à la pousser, puis à remettre la branche main locale au niveau du dépôt distant.

5. Le cas du développeur seul

GitHub n'autorise pas l'auteur d'une pull request à l'approuver lui-même. Une règle qui exige une approbation bloque donc définitivement un développeur seul sur son projet. La configuration adaptée garde la pull request obligatoire avec zéro approbation, et fait porter le contrôle sur les vérifications automatiques.

Mon avis tranché : même seul, cette configuration vaut le détour. La pull request oblige à relire son propre diff dans une autre interface que son éditeur, ce qui suffit à repérer un nombre surprenant de fichiers oubliés, de logs de débogage et de clés d'API commitées par erreur. Sur un portfolio, l'historique de pull requests montre aussi à un recruteur une façon de travailler, un point développé dans le premier projet d'un CV de développeur junior.

Les pièges qui rendent la protection inutile

Une liste de contournement trop généreuse

Ajouter le rôle administrateur à la bypass list « pour les urgences » revient souvent à désactiver la règle pour la personne qui fusionne le plus. Si un contournement est indispensable, mieux vaut le réserver à une situation précise et le retirer ensuite. Le raisonnement rejoint celui de l'article sur le principe de moindre privilège : chaque droit accordé en permanence finit par servir au mauvais moment.

Une vérification requise qui ne s'exécute jamais

Renommer le job tests en test, ou ajouter un filtre de chemins qui empêche le workflow de se lancer sur certaines pull requests, laisse la règle attendre une vérification qui n'arrivera pas. La pull request reste bloquée en attente, sans erreur explicite. Deux jobs portant le même nom dans deux workflows différents créent l'ambiguïté inverse, et la documentation GitHub recommande des noms de jobs uniques dans tout le dépôt.

La branche locale qui refuse de se supprimer après un squash

La fusion par squash rassemble les commits de la pull request en un seul commit nouveau sur main. Pour Git, les commits d'origine de la branche locale n'ont donc jamais été fusionnés, et git branch -d refuse de la supprimer. Une fois la pull request fusionnée et vérifiée, git branch -D est légitime. L'option Automatically delete head branches, dans les réglages généraux du dépôt, fait le ménage côté GitHub.

Protéger main et laisser les secrets sur les branches

La protection de branche contrôle ce qui entre dans main, pas ce qui est poussé sur les autres branches. Une clé d'API commitée sur une branche de fonctionnalité est publique dès le push sur un dépôt public, pull request ou non. Les conséquences de ce type de fuite sont décrites dans l'article sur les risques d'une application non sécurisée, et le secret scanning de GitHub fait partie des outils couverts par la formation Git, GitHub Actions et Cyber Resilience Act.

Approuver sans lire

Une règle qui exige une approbation ne garantit pas une relecture. Dans les équipes où la pull request devient un formulaire à valider, les approbations arrivent en quarante secondes sur des diffs de huit cents lignes. Des pull requests courtes, centrées sur un seul changement, et un fichier .github/CODEOWNERS qui désigne les personnes compétentes pour chaque partie du code, redonnent du sens à l'étape. L'option qui annule une approbation quand de nouveaux commits arrivent ferme une autre faille : l'approbation obtenue sur une version, suivie d'un push de dernière minute que personne n'a vu.

Ce que ça change dans une équipe

Le bénéfice le plus visible arrive le jour où quelque chose casse en production. Avec des pull requests systématiques, la question « qu'est-ce qui a changé hier ? » se règle en ouvrant la liste des pull requests fusionnées, chacune avec sa description, ses tests et sa relecture. Le symptôme décrit dans l'article "Ça marche sur ma machine" devient aussi plus rare, puisque chaque changement passe par une exécution des tests sur une machine neutre avant d'être intégré.

Pour les personnes en reconversion et les développeurs juniors, ce flux est aussi celui qu'ils rencontreront en entreprise dès la première semaine, souvent sans qu'on le leur explique. La formation Git et GitHub : gestion de versions pour débutants couvre les branches, les pull requests et la résolution de conflits, avant d'aller vers l'automatisation. Les bases elles-mêmes, commit, branche, push, sont reprises dans le guide Git pour débutants.

La configuration minimale prend dix minutes sur un dépôt existant : une règle sur la branche par défaut, push forcé et suppression bloqués, pull request obligatoire. Les tests requis peuvent venir la semaine suivante.

Questions fréquentes

Ruleset ou règle de protection de branche classique : lequel choisir ?

Pour une nouvelle configuration, le ruleset. Il offre les mêmes contrôles, peut se cumuler avec d'autres règles, se désactive sans être supprimé et reste visible par toute personne ayant accès en lecture au dépôt. Les deux mécanismes coexistent, et GitHub propose d'importer une règle classique existante sous forme de ruleset.

Peut-on protéger une branche sur un dépôt privé gratuit ?

Non. Avec GitHub Free, les branches protégées et les rulesets ne s'appliquent qu'aux dépôts publics. Les dépôts privés demandent un compte GitHub Pro ou une organisation sur l'offre Team ou Enterprise. L'alternative gratuite consiste à rendre le dépôt public quand son contenu le permet.

Faut-il fusionner en squash, en rebase ou avec un commit de fusion ?

Le squash donne un commit par pull request sur main, pratique pour lire l'historique et annuler un changement complet. Le commit de fusion garde le détail de chaque commit de la branche. Le rebase produit un historique linéaire sans commit de fusion. Le choix dépend surtout de la stratégie de branches de l'équipe, et les réglages du dépôt permettent de n'autoriser qu'une seule méthode pour éviter les mélanges.

Que faire après un commit fait sur main en local alors que la branche est protégée ?

Créer une branche à partir de l'état actuel avec git switch -c nom-de-branche, la pousser et ouvrir une pull request. Revenir ensuite sur main et la réaligner sur le dépôt distant avec git fetch puis git reset --hard origin/main. Le commit n'est pas perdu puisqu'il vit désormais sur la nouvelle branche.

Les pull requests ralentissent-elles une petite équipe ?

Elles ajoutent un délai d'attente quand la relecture traîne. La parade tient dans la taille des pull requests : un changement de cent lignes se relit en quelques minutes, un changement de mille lignes attend jusqu'au lendemain. Le temps perdu en relecture est en général très inférieur au temps passé à chercher quel push a cassé main.

Faut-il protéger d'autres branches que main ?

Oui, toute branche qui alimente un déploiement ou une version publiée : une branche develop dans Git Flow, ou des branches de maintenance du type release/2.x. Un ruleset accepte des motifs de noms, ce qui permet de protéger toutes les branches release d'un coup. Les branches de fonctionnalité individuelles, elles, n'ont pas besoin de protection.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous