Objectif de la journée : transformer le workflow du jour 1 en une vraie chaîne d'intégration (lint, tests, construction de l'image, publication), pour que chaque commit poussé sur main produise une image Docker publiée sur ghcr.io, le registre d'images de GitHub.
À la fin de la journée, votre fichier .github/workflows/ci.yml contient trois jobs :
| Job | Ce qu'il fait | Quand il démarre |
|---|---|---|
lint |
npm ci, puis npm run lint |
dès le push |
test |
npm ci, puis npm test |
dès le push, en même temps que lint |
build |
construit l'image avec le Dockerfile du jour 2 et la publie sur ghcr.io |
seulement quand lint et test sont verts |
Chaque image publiée porte le numéro complet du commit qui l'a produite (sha- suivi des 40 caractères du commit). Demain, c'est exactement cette image qui sera déployée : on construit une fois, on déploie partout la même image, jamais une image reconstruite ailleurs.
Résultat attendu : vous savez dire dans quel ordre les trois jobs s'exécutent et lequel publie l'image.
Ouvrez le lab Atelier projet DevOps. La machine est neuve : relancez atelier-init (un nouveau code vous sera demandé), puis la stack du jour 2, dont les vérifications de l'Atelier lisent les conteneurs en marche au moment d'Évaluer :
atelier-init
cd ~/devops-tasktracker
docker compose up -d --build --wait
docker compose ps
npm testSi vous n'avez pas terminé le jour 2, faites d'abord le rattrapage de la section 9 de docs/J2.md (branche start-j3).
Gardez ouverts sur votre ordinateur deux onglets de votre fork : Actions (les runs du pipeline) et la page de votre compte GitHub, onglet Packages (les images publiées).
Résultat attendu : docker compose ps montre nginx, app et db en marche, et npm test est au vert.
Aujourd'hui, un seul job enchaîne npm ci, npm run lint et npm test. Séparez-le en deux jobs, lint et test. Chaque job démarre sur une machine neuve, sans rien de l'autre job : chacun a donc besoin de checkout, de setup-node et de son propre npm ci.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run lintÉcrivez le job test sur le même modèle, avec npm test -- --coverage comme dernière étape.
Les jobs d'un workflow démarrent tous en même temps, sauf ceux qui déclarent needs : un job avec needs: [lint, test] attend que lint et test soient verts, et il est ignoré (affiché en gris, « skipped ») si l'un des deux échoue. C'est ce qui empêchera demain de publier une image dont les tests échouent. Pour la même raison, n'ajoutez jamais continue-on-error: true ni || true à une étape de lint ou de test : l'étape paraîtrait verte même quand les tests échouent, et l'Atelier ne la compte pas.
Le pipeline ne tourne que sur ce que vous poussez. Enregistrez et poussez la modification :
git add .github/workflows/ci.yml
git commit -m "ci: jobs lint et test séparés"
git pushFaites de même à chaque section suivante qui dit « poussez » : git add, git commit avec un message qui dit ce qui change, puis git push.
Résultat attendu : dans l'onglet Actions, le run de votre dernier commit affiche deux jobs, lint et test, côte à côte et au vert.
Ajoutez un troisième job, build, qui attend les deux autres :
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha,format=long
type=raw,value=latest,enable={{is_default_branch}}
- uses: docker/build-push-action@v7
with:
context: .
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}| Étape | Rôle |
|---|---|
docker/setup-buildx-action@v4 |
prépare le constructeur d'images BuildKit, nécessaire au cache de la section 6 |
docker/metadata-action@v6 |
calcule les noms de l'image : sha- suivi des 40 caractères du commit (type=sha,format=long), et latest uniquement sur la branche par défaut (enable={{is_default_branch}} est une expression de cette action, pas une expression GitHub ${{ }}). Il met aussi le nom en minuscules, obligatoire sur ghcr.io |
labels |
inscrit dans l'image des étiquettes, dont org.opencontainers.image.revision, le commit exact qui l'a produite : demain, elle prouvera quelle version tourne |
push: false |
pour l'instant, l'image est seulement construite, pas publiée : la section 5 active la publication |
Poussez (git add, git commit, git push, comme à la section 3), puis ouvrez le job build dans l'onglet Actions.
Résultat attendu : le job build démarre seulement après lint et test, et il est vert. Le journal de l'étape docker/build-push-action montre les étapes de votre Dockerfile et le nom ghcr.io/<votre-compte>/devops-tasktracker:sha-....
Pour publier, le job build doit se connecter à ghcr.io. Vous n'avez aucun jeton à créer : à chaque run, GitHub fabrique un jeton GITHUB_TOKEN, valable pour votre dépôt seulement, qui expire à la fin du job. Le bloc permissions réduit ce qu'il peut faire :
- Au premier niveau du fichier (sans indentation), entre le bloc
on:etjobs:, ajoutez ce bloc : par défaut, chaque job peut seulement lire le dépôt.
permissions:
contents: read- Dans le job
builduniquement, élargissez ces droits : lui seul peut publier une image.
build:
needs: [lint, test]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write- Dans le job
build, juste aprèsdocker/setup-buildx-action@v4, ajoutez une étape de connexion :
- uses: docker/login-action@v4
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}- Dans l'étape
docker/build-push-action@v7, remplacezpush: falseparpush: ${{ github.event_name != 'pull_request' }}. - Poussez (
git add,git commit,git push).
Pourquoi rien n'est publié sur une pull request : une pull request venant d'un fork reçoit un GITHUB_TOKEN en lecture seule et aucun secret. Le pipeline construit l'image pour vérifier qu'elle se construit, sans la publier.
Après le push, l'image apparaît dans l'onglet Packages de votre compte GitHub (et dans la colonne de droite de la page de votre fork), sous le nom devops-tasktracker. Elle est privée par défaut, même si votre fork est public, et liée à votre dépôt : les workflows du dépôt peuvent la lire avec leur GITHUB_TOKEN, c'est ce que fera le déploiement de demain. Ne la rendez pas publique : ce changement est définitif sur GitHub, et inutile pour le cours.
Résultat attendu : le job build est vert sur un push vers main, et le package devops-tasktracker est visible, avec deux étiquettes : sha- suivi du commit, et latest.
Depuis le jour 1, cache: npm dans setup-node garde les téléchargements npm d'un run à l'autre. Faites de même pour la construction de l'image : dans l'étape docker/build-push-action@v7, ajoutez
cache-from: type=gha
cache-to: type=gha,mode=maxtype=gha range les couches de l'image dans le cache de GitHub Actions de votre dépôt ; mode=max garde aussi les couches de l'étape intermédiaire deps (celle du npm ci), et pas seulement celles de l'image finale.
Poussez (git add, git commit, git push) : ce premier run remplit le cache, le suivant s'en sert.
Mesurez l'effet : la durée d'un job s'affiche à côté de son nom quand vous ouvrez un run dans l'onglet Actions. Relancez le workflow sans commit avec la section 7 pour obtenir la troisième ligne.
| Run | Durée du job build |
Étape npm ci du Dockerfile |
|---|---|---|
Sans cache gha (run de la section 5) |
exécutée | |
| Premier run avec le cache (il le remplit) | exécutée | |
Run suivant, package-lock.json inchangé |
CACHED |
Chaque minute gagnée l'est à chaque push, pour chaque développeur de l'équipe : moins d'attente, moins de machines occupées, moins d'énergie consommée.
Résultat attendu : dans le dernier run, le journal de docker/build-push-action affiche CACHED pour l'étape npm ci du Dockerfile, et le job build est plus court que dans le premier run.
Ajoutez workflow_dispatch: sous on:, à côté de push: et pull_request:, puis poussez. Le workflow peut alors être lancé à la main, sans nouveau commit : dans l'onglet Actions, cliquez sur CI, puis sur Run workflow (branche main), ou, depuis la console de l'Atelier :
gh workflow run ci.yml --ref main
gh run list --limit 3Demain, ce bouton permettra de redéployer sur une machine de l'Atelier neuve sans inventer de commit.
Résultat attendu : gh run list affiche un run dont l'événement est workflow_dispatch, au vert.
Un assistant IA a produit ce workflow en réponse à « écris-moi une CI pour mon projet Node.js ». Il fonctionne, mais contient cinq erreurs qu'une revue doit arrêter. Lisez-le ici : ne le copiez pas dans .github/workflows/, GitHub l'exécuterait.
name: CI generee par une IA
on:
pull_request_target:
push:
branches: [main]
permissions: write-all
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@main
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Installer les dependances
run: npm install
- name: Lint
run: npm run lint
- name: Tests
run: npm test -- --coverage
- name: Verifier le jeton du registre
run: echo ${{ secrets.REGISTRY_TOKEN }}Trouvez les cinq erreurs, écrivez pour chacune le risque et la correction, puis comparez avec la solution ci-dessous.
Solution
| Erreur | Risque | Correction |
|---|---|---|
pull_request_target avec un checkout de github.event.pull_request.head.sha |
le code d'une pull request venant de n'importe quel fork s'exécute avec les secrets et un jeton en écriture sur votre dépôt | pull_request, et le checkout par défaut |
permissions: write-all |
le jeton peut tout modifier dans le dépôt, alors que la CI ne fait que lire | permissions: avec contents: read |
actions/checkout@main |
le code de l'action change sans prévenir à chaque commit de sa branche main |
une version publiée : actions/checkout@v7 (ou le SHA complet d'un commit) |
echo ${{ secrets.REGISTRY_TOKEN }} |
le secret est écrit dans le journal du run ; GitHub masque sa valeur exacte, mais pas une copie transformée (encodée, découpée) | ne jamais afficher un secret ; le passer à l'étape qui en a besoin, par env: ou with: |
npm install |
les versions installées peuvent différer de package-lock.json, et le fichier peut être réécrit |
npm ci, qui installe exactement le package-lock.json et échoue s'il ne correspond pas à package.json |
Aide-mémoire pour relire tout workflow généré :
- Déclencheurs :
push,pull_request,workflow_dispatch; jamaispull_request_targetdans ce cours. - Permissions :
contents: readpour tout le workflow, élargies job par job seulement là où c'est nécessaire. - Secrets : jamais affichés, jamais écrits dans un fichier commité.
- Versions : actions épinglées sur une version publiée (
@v7) ou un SHA, jamais sur une branche. - Erreurs : aucune erreur avalée (
|| true,continue-on-error: true) ;npm ciplutôt quenpm install.
Puis testez le workflow sur une branche, et ne le fusionnez que vert et relu.
Résultat attendu : vos cinq erreurs correspondent à celles de la solution, et vous savez expliquer le risque de chacune.
Votre fork est public : n'importe qui peut le forker à son tour et vous proposer une pull request.
- Avec le déclencheur
pull_request, le workflow exécute le code de la pull request avec unGITHUB_TOKENen lecture seule et sans aucun secret : même un code malveillant ne peut rien publier ni rien lire de secret. - Avec
pull_request_target, le workflow tourne dans le contexte de votre dépôt, avec vos secrets et un jeton en écriture. Combiné à uncheckoutdu code de la pull request, il exécute le code d'un inconnu avec vos droits. Ce déclencheur n'a pas sa place dans ce cours, et l'Atelier refuse tout workflow qui l'utilise. - Le réglage Approval for running fork pull request workflows from contributors (sur votre fork : Settings, Actions, General) avec la valeur Require approval for all external contributors fait attendre votre approbation à chaque run déclenché par une pull request venant d'un fork. Vous l'appliquerez demain, avant d'installer un runner sur la machine de l'Atelier : le playbook du jour 4 refuse d'installer le runner sans ce réglage.
Cette semaine, n'approuvez jamais une pull request venant d'un fork sur votre dépôt. Dès demain, son workflow pourrait s'exécuter sur la machine de l'Atelier, qui contient aussi votre accès à GitHub.
Résultat attendu : grep -rn pull_request_target .github/ n'affiche rien, et vous savez expliquer la différence entre pull_request et pull_request_target.
- Jobs
lintettestséparés, et un jobbuildqui dépend des deux (needs) - Image publiée sur
ghcr.ioavecGITHUB_TOKENsur un push versmain, packagedevops-tasktrackervisible (privé) dans l'onglet Packages de votre compte - Cache
type=ghaen place et tableau de la section 6 rempli -
workflow_dispatchajouté - Les cinq erreurs de la section 8 trouvées et expliquées
- CI au vert sur le dernier commit de
main - Atelier à 12/12 : Évaluer avec la stack du jour 2 en marche (
docker compose up -d --build --wait) -
git statuspropre (rien à committer) etgit pushfait, puis Évaluer et Terminer l'Atelier : ce qui n'est pas poussé est perdu
Si vous n'avez pas terminé, récupérez la correction au début de l'Atelier du jour 4, après atelier-init, dans ~/devops-tasktracker. La branche start-jN n'existe dans le dépôt du cours qu'à partir du matin du jour N. La branche start-j4 contient aussi le kit du jour 4 (métriques de l'application, fichiers de supervision, rôle Ansible du runner) : c'est normal, docs/J4.md s'en sert. Commencez par enregistrer votre travail, puis fusionnez-la en gardant sa version en cas de conflit, et créez un commit à vous :
git add -A
git commit -m "wip: avant rattrapage" # ignorez le message si rien n'est à committer
git fetch upstream
git merge -X theirs --no-edit upstream/start-j4
git commit --allow-empty -m "rattrapage du jour 3"
npm ci
npm test
git push origin main-X theirs garde vos modifications qui n'entrent pas en conflit, et prend la version du cours là où vous aviez écrit la même chose différemment. Le commit vide rattrapage du jour 3 garantit que votre fork contient au moins un commit à vous, même quand la fusion n'en crée aucun. Le git push déclenche le pipeline complet, qui publie l'image de ce commit sur ghcr.io.