Skip to content

Latest commit

 

History

History
288 lines (210 loc) · 15.9 KB

J3.md

File metadata and controls

288 lines (210 loc) · 15.9 KB

Jour 3 : pipeline GitHub Actions complet

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.

1. La chaîne du jour

À 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.

2. Relancer l'Atelier

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 test

Si 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.

3. Séparer lint et test en deux jobs

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 push

Faites 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.

4. Construire l'image dans le pipeline

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-....

5. Publier sur ghcr.io avec GITHUB_TOKEN

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 :

  1. Au premier niveau du fichier (sans indentation), entre le bloc on: et jobs:, ajoutez ce bloc : par défaut, chaque job peut seulement lire le dépôt.
permissions:
  contents: read
  1. Dans le job build uniquement, élargissez ces droits : lui seul peut publier une image.
  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
  1. Dans le job build, juste après docker/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 }}
  1. Dans l'étape docker/build-push-action@v7, remplacez push: false par push: ${{ github.event_name != 'pull_request' }}.
  2. 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.

6. Sobriété : mettre en cache ce qui ne change pas

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=max

type=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.

7. Relancer sans commit : workflow_dispatch

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 3

Demain, 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.

8. Revue d'un workflow généré par une IA

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é :

  1. Déclencheurs : push, pull_request, workflow_dispatch ; jamais pull_request_target dans ce cours.
  2. Permissions : contents: read pour tout le workflow, élargies job par job seulement là où c'est nécessaire.
  3. Secrets : jamais affichés, jamais écrits dans un fichier commité.
  4. Versions : actions épinglées sur une version publiée (@v7) ou un SHA, jamais sur une branche.
  5. Erreurs : aucune erreur avalée (|| true, continue-on-error: true) ; npm ci plutôt que npm 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.

9. Forks et sécurité

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 un GITHUB_TOKEN en 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é à un checkout du 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.

10. Fin de journée

  • Jobs lint et test séparés, et un job build qui dépend des deux (needs)
  • Image publiée sur ghcr.io avec GITHUB_TOKEN sur un push vers main, package devops-tasktracker visible (privé) dans l'onglet Packages de votre compte
  • Cache type=gha en place et tableau de la section 6 rempli
  • workflow_dispatch ajouté
  • 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 status propre (rien à committer) et git push fait, puis Évaluer et Terminer l'Atelier : ce qui n'est pas poussé est perdu

11. Rattrapage

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.