
Un workflow GitHub Actions complet pour un rôle Ansible 2026 : lint en fail-fast, puis matrice Molecule (Rocky Linux 9, AlmaLinux 10, Ubuntu 24.04 croisées avec ansible-core 2.17 et 2.18). Actions épinglées par SHA, permissions minimales, persist-credentials: false.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Structure d'un workflow
.github/workflows/test.yml. - Configurer un job lint en fail-fast (avant Molecule).
- Définir une matrice multi-dimensions : distros × versions Ansible.
- Sécuriser le workflow (SHA pinning, permissions minimales).
- Déclencheurs : push, pull_request, schedule (re-test périodique).
Le workflow .github/workflows/test.yml
Section intitulée « Le workflow .github/workflows/test.yml »Voici le fichier complet, à poser tel quel à la racine du dépôt de votre rôle. Lisez-le en repérant d'abord ses deux jobs : lint s'exécute seul et rapidement, molecule ne démarre qu'ensuite et se démultiplie en six exécutions parallèles. Tout le reste, les permissions vides, les SHA à la place des tags, persist-credentials: false, relève du durcissement et se justifie section par section plus bas. Les commandes pip install figent volontairement des versions : un rôle Ansible testé sur une version d'ansible-core qui bouge sans prévenir produit des échecs impossibles à rattacher à un changement de code.
---name: Test webserver role
on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 6 * * 1' # lundi 6h, re-test contre les dernières versions
permissions: {} # ← deny-by-default au top
jobs: lint: name: Lint (ansible-lint + yamllint) runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Setup Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12'
- name: Install lint tools run: pip install ansible-lint==25.* yamllint
- name: Run yamllint run: yamllint roles/
- name: Run ansible-lint (profile production) run: ansible-lint --profile=production roles/
molecule: name: Molecule (${{ matrix.distro }} / ansible-core ${{ matrix.ansible }}) runs-on: ubuntu-24.04 needs: lint # ← lint doit passer en premier permissions: contents: read strategy: fail-fast: false matrix: distro: - rockylinux9 - alma10 - ubuntu2404 ansible: - 2.17 - 2.18
steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Setup Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.12'
- name: Install Molecule + plugins run: | pip install \ "ansible-core==${{ matrix.ansible }}.*" \ "molecule>=26.0" \ "molecule-plugins[podman]>=25.0" \ "ansible-lint==25.*"
- name: Install Ansible collections run: ansible-galaxy collection install -r requirements.yml
- name: Run molecule test run: molecule test env: MOLECULE_DISTRO: ${{ matrix.distro }} PY_COLORS: 1 ANSIBLE_FORCE_COLOR: 1Le workflow ligne par ligne
Section intitulée « Le workflow ligne par ligne »on:, déclencheurs
Section intitulée « on:, déclencheurs »Trois déclencheurs valent mieux qu'un, parce qu'ils attrapent des régressions différentes. Le couple push et pull_request couvre vos propres modifications, avant et après fusion. Le schedule répond à un cas que les deux autres ne voient jamais : votre rôle n'a pas changé, mais son environnement, si. Une nouvelle version d'ansible-core, une image de base mise à jour, une collection Galaxy remontée, et le rôle casse sans le moindre commit de votre part.
on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '0 6 * * 1' # lundi 6hpushsurmain/develop: re-test après merge.pull_requestsurmain: test avant merge.schedulehebdomadaire : détecte les régressions liées aux mises à jour upstream (nouvelles versions Ansible, nouvelles images Docker base).
permissions: {} et moindre privilège
Section intitulée « permissions: {} et moindre privilège »Sans cette ligne, le GITHUB_TOKEN fourni au workflow hérite des permissions par défaut du dépôt, qui restent en écriture sur beaucoup de dépôts créés avant 2023. Un attrset vide révoque tout, et chaque job redéclare ensuite le strict nécessaire. Un job de test n'a besoin que de lire le code, d'où contents: read et rien d'autre.
permissions: {}Deny-by-default au top du workflow. Chaque job redéclare ses permissions explicitement (contents: read pour les jobs qui n'écrivent rien).
Pourquoi : si une dépendance malveillante s'exécute pendant le job, elle ne peut pas pousser sur le dépôt, créer une release ni poser un tag. C'est la recommandation explicite de la documentation GitHub sur le durcissement des workflows.
Actions pinnées par SHA
Section intitulée « Actions pinnées par SHA »- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1Pas @v4 ni @main. Le SHA de 40 caractères du commit garantit que vous exécutez exactement ce code, pas une version remontée silencieusement.
Pourquoi : actions/checkout@v4 est un tag mobile, quiconque a les droits sur le dépôt de l'action peut le repointer vers un autre commit. Un SHA de commit, lui, ne se réécrit pas.
persist-credentials: false
Section intitulée « persist-credentials: false »- uses: actions/checkout@<SHA> with: persist-credentials: falsePar défaut, actions/checkout écrit le jeton GitHub dans le .git/config du runner, pour que les opérations Git suivantes s'authentifient sans configuration. Toute étape exécutée ensuite peut lire ce fichier et réutiliser le jeton pour pousser sur le dépôt.
persist-credentials: false supprime cette persistance. À poser systématiquement dès que le job installe des dépendances tierces, ce qui est le cas ici avec pip et ansible-galaxy.
Matrice 2D, distros × versions
Section intitulée « Matrice 2D, distros × versions »strategy: fail-fast: false matrix: distro: [rockylinux9, alma10, ubuntu2404] ansible: [2.17, 2.18]3 distros × 2 versions = 6 jobs en parallèle. Si un job échoue, les 5 autres continuent (fail-fast: false), vous voyez toutes les régressions, pas juste la première.
needs: lint
Section intitulée « needs: lint »molecule: needs: lintLe job molecule attend que lint passe. Sans cette dépendance, les six jobs de matrice démarrent en parallèle du lint et consomment plusieurs dizaines de minutes de runner avant de révéler une simple erreur d'indentation. La règle vaut aussi pour votre confort de relecture : un rapport de CI où le lint a échoué en 40 secondes se lit plus vite que six échecs Molecule à interpréter.
Structure du dépôt attendue
Section intitulée « Structure du dépôt attendue »Le workflow suppose une arborescence précise, et deux chemins y sont vérifiés à l'exécution. Le job lint passe yamllint et ansible-lint sur roles/, ce qui impose que vos rôles soient bien sous ce répertoire et non à la racine. Le job molecule appelle ansible-galaxy collection install -r requirements.yml, qui échoue si le fichier manque, même quand le rôle n'utilise aucune collection externe. Si votre dépôt ne contient qu'un seul rôle et le publie à sa racine, adaptez les chemins du job lint en conséquence.
repo-rôle/├── .github/│ └── workflows/│ └── test.yml ← ce fichier├── roles/│ └── webserver/│ ├── tasks/│ ├── defaults/│ └── meta/├── molecule/│ └── default/├── requirements.yml ← collections requises└── README.mdMettre en pratique
Section intitulée « Mettre en pratique »Relire un workflow durci et l'écrire soi-même sont deux exercices très différents. Ce lab vous remet un .github/workflows/test.yml amputé de ses valeurs sensibles, à vous de rétablir le job lint en amont, la matrice distros × ansible-core, l'épinglage par SHA et les permissions minimales par job. La correction contrôle la structure obtenue puis passe actionlint sur le fichier, exactement comme le ferait une revue de code sérieuse.
Pièges courants
Section intitulée « Pièges courants »Ces cinq situations reviennent systématiquement lors de la première mise en place. Deux d'entre elles se ressemblent et n'ont rien à voir : Unable to resolve action signale un SHA introuvable dans le dépôt de l'action, alors que Permission denied sur l'API GitHub vient du permissions: du job. Le réflexe utile est de lire la colonne Cause avant de toucher au fichier, car la correction naturelle du second symptôme, réélargir les permissions, est aussi le meilleur moyen d'annuler le durcissement mis en place ici.
| Symptôme | Cause | Fix |
|---|---|---|
Error: Unable to resolve action | SHA invalide ou repo renommé | Mettre à jour le SHA après avoir vérifié le repo officiel |
| Tests passent localement, ratent en CI | Version de Python ou d'Ansible différente | Aligner basepython de tox avec python-version du workflow |
molecule test lent (>20 min) | Pas de cache Docker layers | Utiliser actions/cache sur ~/.local |
secret manquant | Pas de secrets: configuré pour la publication | Aller dans repo Settings → Secrets and variables |
Permission denied sur l'API GitHub | permissions: {} trop restrictif | Ajouter contents: read ou pull-requests: write selon besoin |
À retenir
Section intitulée « À retenir »permissions: {}au top + redéclaration par job = least-privilege.- Actions pinnées par SHA +
persist-credentials: false= sécurité supply chain. needs: lintavant Molecule évite de consommer des minutes de runner sur une erreur de lint.- Matrice 2D distros × versions = couverture complète automatique.
schedule:hebdomadaire = détecte régressions upstream.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Installer rôles Galaxy : le
requirements.ymlépinglé sans lequel la matrice teste un contenu qui bouge. - Versionner et publier : transformer un tag Git en release publiée automatiquement, la suite naturelle de ce pipeline.
- HashiCorp Vault / OpenBao : récupérer les jetons du pipeline depuis un coffre, plutôt que de multiplier les secrets de dépôt.