Convoyeurs à rouleaux dans un entrepôt automatisé, image d'un pipeline GitHub Actions

GitHub Actions : le tutoriel pas à pas pour tester et publier une API Node.js

GitHub Actions est le service d’intégration et de livraison continues intégré à GitHub : un fichier YAML placé dans .github/workflows/ suffit pour lancer vos tests à chaque push, puis construire et publier une image Docker. Ce tutoriel construit ce fichier pas à pas pour l’API de tâches de notre guide créer une API REST avec Node.js et Express.

La CI/CD en général et le rollback sont traités dans notre guide pour automatiser ses déploiements. Versions et prix ont été lus le 31 octobre 2026 ; la syntaxe YAML a été vérifiée, mais rien n’a été exécuté sur GitHub.

Points clés

  • Au 31 octobre 2026, les versions majeures en cours sont actions/checkout v7, actions/setup-node v7, docker/build-push-action v7 et docker/login-action v4.
  • L’offre GitHub Free inclut désormais 12 $ de calcul par mois, soit 2 000 minutes sur le runner Linux 2 cœurs. Les dépôts publics ne paient rien sur les runners standards.
  • Déclarez permissions: contents: read en tête de fichier et n’accordez packages: write qu’au job qui publie l’image : les permissions non citées passent à none.
  • Épinglez chaque action sur l’empreinte complète d’un commit. En mars 2025, des tags de tj-actions/changed-files ont été redirigés vers un commit malveillant.
  • Un runner auto-hébergé ne coûte pas de minutes, mais GitHub déconseille presque toujours de l’utiliser sur un dépôt public.

Les six notions de GitHub Actions

Tout workflow GitHub Actions se lit avec six mots, définis dans la page Understanding GitHub Actions de la documentation officielle (consultée le 31 octobre 2026) :

Notion Ce que c’est Dans notre exemple
Workflow Un processus automatisé décrit par un fichier YAML dans .github/workflows/ ci.yml
Événement L’activité du dépôt qui déclenche le workflow push et pull_request sur main
Job Une suite d’étapes exécutées sur un même runner ; les jobs tournent en parallèle sauf dépendance test et image
Step Une commande shell ou une action, exécutées dans l’ordre npm ci, npm test
Runner La machine qui exécute un job, un seul job à la fois ubuntu-latest, hébergé par GitHub
Action Une extension réutilisable, souvent prise sur le GitHub Marketplace actions/checkout, docker/login-action

Chaque exécution part d’une machine virtuelle neuve, et chaque action du Marketplace est du code tiers qui tourne avec vos droits, d’où l’étape 6.

Schéma du workflow GitHub Actions de ce tutoriel : un événement push ou pull_request déclenche deux jobs de test en parallèle sur Node.js 24 et 26, puis un job qui publie l'image Docker sur ghcr.io


Étape 1 : un premier workflow pour l’API Node.js

Le point de départ : récupérer le code, installer Node.js et les dépendances, lancer les tests. L’API du tutoriel déclare "test": "node --test" dans son package.json et exige Node.js 24 ou plus. Créez .github/workflows/ci.yml :

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: npm ci
      - run: npm test

npm ci installe exactement ce que fixe package-lock.json, que le dépôt doit contenir, et npm test lance node --test. Le timeout-minutes évite qu’un test bloqué tourne jusqu’à la limite de 6 heures par job des runners hébergés, fixée par la page des limites de GitHub Actions.

Pourquoi checkout v7 et pas v4 ? Selon son README, la v5 est passée au moteur node24, la v6 range les identifiants Git hors de .git/config, et la v7 refuse par défaut le code d’un fork sous pull_request_target ou workflow_run.

Les dernières versions au 31 octobre 2026 sont actions/checkout v7.0.1 et actions/setup-node v7.1.0. L’onglet Actions affiche ensuite chaque exécution ; pour lire les journaux d’un runner Ubuntu, voyez nos commandes Linux de base.


Étape 2 : tester sur Node.js 24 et 26 avec une matrice

Une matrice exécute le même job plusieurs fois avec des valeurs différentes. Pour l’API, deux versions comptent : Node.js 24, minimum exigé par le package.json, et Node.js 26, passé en LTS avec la version 26.13.0 du 28 octobre 2026 selon l’index des versions de Node.js. Remplacez le job test :

  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        node-version: [24, 26]
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

Les deux jobs tournent en parallèle. fail-fast: false laisse finir le second si le premier échoue. La page des limites plafonne une matrice à 256 jobs, et chaque job est facturé séparément.


Étape 3 : le cache, plus rapide mais pas sans risque

Le cache de GitHub Actions évite de retélécharger les paquets npm à chaque exécution : ajoutez cache: npm sous with. D’après le README de setup-node, la clé dérive du hachage de package-lock.json et l’action met en cache le dossier global de npm, pas node_modules.

      - uses: actions/setup-node@v7
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm

Les limites, lues dans la référence du cache de dépendances le 31 octobre 2026 :

  • 10 Go par dépôt par défaut. Une entrée non lue depuis plus de 7 jours est supprimée.
  • Aucun secret dans le cache. Toute personne ayant un accès en lecture peut ouvrir une pull request et lire un cache.

Le mot-clé cache-mode (read, write, write-only ou none) fixe les droits d’un job sur le cache. Il se place en tête du workflow ou dans un job, par exemple cache-mode: read sous test:, d’après la référence de syntaxe. Selon la documentation du cache consultée le 31 octobre 2026, un déclencheur peu fiable comme pull_request_target n’obtient par défaut que la lecture, et y déclarer write réintroduit le risque d’empoisonnement du cache.


Étape 4 : secrets, variables et environnements

Un jeton n’entre jamais dans le YAML : il se range dans Settings, Secrets and variables, Actions, et se lit avec ${{ secrets.NOM }}. Une valeur non sensible, comme une URL, devient une variable lue avec ${{ vars.NOM }}, au niveau du dépôt, de l’organisation ou d’un environnement, selon la page sur les variables.

Pour rattacher le job image de l’étape 5 à un environnement production, avec une variable PRODUCTION_URL créée dans cet environnement, ajoutez au job :

    environment:
      name: production
      url: ${{ vars.PRODUCTION_URL }}

La référence de syntaxe autorise le contexte vars dans url. Mais la page de gestion des environnements réserve les environnements aux dépôts publics sur GitHub Free ; en privé, il faut Pro, Team ou Enterprise. C’est pourquoi le fichier complet plus bas s’en passe.

Notre workflow n’a besoin d’aucun secret ajouté à la main. Les tests de l’API fixent eux-mêmes JWT_SECRET et DB_PATH=':memory:' dans test/api.test.js, et le job image construit l’image sans démarrer le conteneur. Quant au GITHUB_TOKEN, créé pour chaque job, il suffit à publier sur le registre de GitHub.


Étape 5 : construire et publier l’image Docker sur GHCR

GHCR, le registre de conteneurs de GitHub (ghcr.io), accepte un push authentifié par GITHUB_TOKEN. L’image reprend le Dockerfile multi-étapes de notre tutoriel Docker pour débutants, basé sur node:24-alpine, placé à la racine du projet. Ajoutez ce job à ci.yml :

  image:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v7
        with:
          persist-credentials: false
      - uses: docker/setup-buildx-action@v4
      - uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v6
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=sha
      - uses: docker/build-push-action@v7
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Ce que fait chaque bloc :

  • needs et if. L’image n’est construite que si les deux tests réussissent, et seulement pour un push sur main.
  • docker/metadata-action. L’image prend le nom du dépôt, ghcr.io/<compte>/<dépôt>, soit ghcr.io/<compte>/api-taches si le dépôt s’appelle comme le dossier du tutoriel. L’action produit les tags main et sha- suivi du commit abrégé, plus les labels OCI. Son README précise qu’il met le nom d’image en minuscules, ce qu’exige le registre.
  • context: . avec checkout. Sans checkout, build-push-action lit le dépôt par Git et ignore les modifications des étapes précédentes, selon son README.
  • type=gha. Les couches Docker vont dans le cache de GitHub Actions. Avec plusieurs images, fixez un scope distinct, sinon chaque build écrase le cache du précédent, prévient la documentation de Docker.

Deux pièges documentés par GitHub sur la page du Container registry. Un paquet publié pour la première fois est privé par défaut. Et le GITHUB_TOKEN ne peut pas pousser si une image du même nom a déjà été poussée à la main sans être reliée au dépôt.

Côté coût, la facturation de GitHub Packages indique le 31 octobre 2026 que le stockage du Container registry est gratuit pour l’instant, avec un mois de préavis avant tout changement. L’image se déploie ensuite derrière notre reverse proxy Nginx avec Docker Compose.


Étape 6 : GITHUB_TOKEN minimal et actions épinglées

Cette étape a deux volets : limiter ce que le jeton peut faire, et figer le code des actions exécutées.

Le principe du moindre privilège

Le GITHUB_TOKEN est limité au dépôt et valable le temps du job. Ses droits dépendent du réglage du dépôt, puis du bloc permissions. La référence de syntaxe fixe la règle utile : dès que vous citez une permission, toutes celles que vous ne citez pas passent à none.

D’où notre schéma : contents: read au niveau du workflow, packages: write dans le seul job image. C’est ce que récompense le test Token-Permissions d’OpenSSF Scorecard, qui classe ce risque comme élevé. L’option persist-credentials: false de checkout évite en plus de conserver le jeton pour les commandes Git suivantes.

Épingler les actions sur un SHA

Un tag comme @v7 peut être déplacé. Selon l’avis de sécurité GitHub GHSA-mrrh-fwg8-r2c3 (CVE-2025-30066), des attaquants ont redirigé en mars 2025 plusieurs tags de tj-actions/changed-files vers un commit malveillant, qui affichait les secrets dans les journaux entre le 14 et le 15 mars.

La référence de sécurité de GitHub Actions recommande donc d’épingler chaque action sur l’empreinte complète d’un commit, en vérifiant qu’elle vient du dépôt de l’action et non d’un fork. La forme est uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 : l’empreinte fige le code, le commentaire garde la version lisible. Le fichier complet plus bas épingle ainsi les six actions sur les versions publiées au 31 octobre 2026. Nous avons lu ces empreintes sur l’API de GitHub ; vérifiez-les sur la page des versions de chaque action.

Pour ne pas figer des versions vulnérables, laissez Dependabot proposer les mises à jour avec .github/dependabot.yml, selon la documentation de Dependabot :

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"

Selon la référence de sécurité, les alertes Dependabot ignorent les actions épinglées par SHA ; les mises à jour de version, elles, fonctionnent.

GitHub va plus loin avec les fichiers de verrouillage des workflows : .github/workflows/actions.lock, généré par l’extension gh actions-lock, fige le commit de chaque action, dépendances indirectes comprises, et vérifie l’identité du dépôt source. La référence de sécurité le présente comme la meilleure protection, qui remplace l’épinglage par SHA pour les dépôts qui l’adoptent. Ce tutoriel garde les SHA : la documentation ne dit pas si le dispositif est en préversion ou disponible pour tous, et il exige une extension de GitHub CLI.


Étape 7 : concurrence et workflows réutilisables

Deux mots-clés évitent le gaspillage et la copie de YAML. Le premier, concurrency, se place en tête de ci.yml :

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

D’après la page sur la concurrence, un seul workflow s’exécute à la fois par groupe, et une exécution en attente est remplacée par la plus récente. L’expression annule aussi l’exécution en cours, mais seulement pour une pull request. Sur main, l’exécution en cours va à son terme, mais une exécution en attente reste remplacée par la plus récente : seul le dernier push en file est publié.

Le second, workflow_call, transforme un workflow en brique réutilisable. Déplacez le job de test dans .github/workflows/tests-node.yml :

name: Tests Node.js

on:
  workflow_call:
    inputs:
      node-versions:
        description: Versions de Node.js, au format JSON
        type: string
        default: '[24, 26]'

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        node-version: ${{ fromJSON(inputs.node-versions) }}
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test

Dans ci.yml, le job test devient un simple appel, et image garde son needs: test :

jobs:
  test:
    uses: ./.github/workflows/tests-node.yml
    permissions:
      contents: read
    with:
      node-versions: '[24, 26]'

La forme ./.github/workflows/{filename} est celle que la documentation sur la réutilisation donne pour un workflow du même dépôt. La même page, consultée le 31 octobre 2026, mentionne aussi une forme $/.github/workflows/{filename}, qu’elle dit recommandée et indisponible sur GitHub Enterprise Server ; nous ne l’avons pas testée. La référence des workflows réutilisables fixe dix niveaux d’imbrication au plus et 50 workflows réutilisables distincts par fichier ; les variables env de l’appelant ne sont pas transmises, et le workflow appelé ne peut pas élever les permissions reçues.


Le fichier ci.yml complet

Voici .github/workflows/ci.yml une fois les étapes 1 à 7 assemblées, avec le job test écrit en entier (sans le workflow réutilisable de l’étape 7 ni l’environnement optionnel de l’étape 4) : déclencheurs, permissions minimales, concurrence, matrice avec cache, publication sur GHCR, et chaque action épinglée sur son SHA. Sa syntaxe a été vérifiée avec le module yaml de Python ; il n’a pas été exécuté sur GitHub. Avec le workflow réutilisable de l’étape 7, le job test ci-dessous est remplacé par l’appel à tests-node.yml.

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    strategy:
      fail-fast: false
      matrix:
        node-version: [24, 26]
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test

  image:
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
      - uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=sha
      - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Combien coûte GitHub Actions en octobre 2026

GitHub Actions reste gratuit pour les dépôts publics sur les runners standards, et sur vos propres machines. Pour les dépôts privés, la page de facturation de GitHub Actions, lue le 31 octobre 2026, exprime désormais le quota en dollars de calcul inclus, et non plus en minutes, déduits selon le tarif de chaque runner :

Offre Calcul inclus par mois Soit, sur Linux 2 cœurs Stockage d’artefacts Cache par dépôt
GitHub Free 12 $ 2 000 minutes 500 Mo 10 Go
GitHub Pro 18 $ 3 000 minutes 1 Go 10 Go
GitHub Team 18 $ 3 000 minutes 2 Go 10 Go
GitHub Enterprise Cloud 300 $ 50 000 minutes 50 Go 10 Go

La conversion est notre calcul à 0,006 $ la minute. Les 2 000 minutes souvent citées, y compris dans notre guide CI/CD, correspondent donc aux 12 $ de l’offre Free sur Linux 2 cœurs ; la page de prix de GitHub affiche bien 2 000, 3 000 et 50 000 minutes pour Free, Team et Enterprise, avec Team à 4 $ et Enterprise à 21 $ par utilisateur et par mois.

Le quota en dollars change la donne dès que vous quittez Linux. La grille des runners facture 0,002 $ la minute sur Linux 1 cœur, 0,005 $ sur Linux arm64 2 cœurs, 0,010 $ sur Windows 2 cœurs et 0,062 $ sur macOS 3 ou 4 cœurs. Les 12 $ de l’offre Free couvrent donc environ 193 minutes de macOS seulement.

Graphique en barres horizontales GitHub Actions : selon notre calcul, les 12 dollars inclus dans GitHub Free couvrent 6 000 minutes sur Linux 1 cœur, 2 400 sur Linux arm64 2 cœurs, 2 000 sur Linux 2 cœurs, 1 200 sur Windows 2 cœurs et 193 sur macOS 3 ou 4 cœurs

Notre calcul. Chaque job étant arrondi à la minute supérieure, notre ci.yml coûte au moins 3 minutes par push sur main et 2 par pull request. À 50 de chaque par mois sur un dépôt privé, cela fait au moins 250 minutes, bien dans le quota Free. Une matrice sur trois systèmes d’exploitation changerait tout : une minute de macOS coûte plus de dix minutes de Linux 2 cœurs.

Le stockage se paie à part : 0,25 $ par Go et par mois au-delà de l’allocation d’artefacts, 0,07 $ pour le cache si vous avez relevé sa limite de 10 Go, selon la même page.


Runners auto-hébergés : gratuits, pas sans danger

Un runner auto-hébergé est une machine à vous qui exécute les jobs à la place des machines de GitHub. La page de facturation les range, le 31 octobre 2026, parmi les usages gratuits qui ne réduisent pas le calcul inclus. GitHub avait annoncé en décembre 2025 une redevance de 0,002 $ la minute sur les runners auto-hébergés à partir du 1er mars 2026, puis a écrit la reporter « pour prendre le temps de réévaluer notre approche » (notre traduction), sur sa page consacrée aux changements de prix de 2026.

Selon la page sur les runners auto-hébergés, vous payez la machine et mettez à jour vous-même système et outils. Checkout et setup-node, passées au moteur node24, exigent le runner v2.327.1 ou plus selon leurs README. Un job peut y tourner 5 jours d’après la page des limites, mais le GITHUB_TOKEN ne se renouvelle que pendant 24 heures, précise la page GITHUB_TOKEN.

Le vrai risque est ailleurs. Selon la référence de sécurité, un runner auto-hébergé ne garantit pas une machine propre à chaque job et peut être compromis durablement : il ne devrait presque jamais servir à un dépôt public, où n’importe qui peut ouvrir une pull request. Elle mentionne aussi des runners éphémères, dits JIT, qui n’exécutent qu’un job.

Verdict : pour cette API, les runners hébergés suffisent. Notre comparatif de l’hébergement cloud pour développeurs aide à choisir la machine.


Ce que nous n’avons pas pu vérifier

Ce tutoriel est une synthèse documentaire. Restent incertains :

  • L’exécution des workflows. Leur syntaxe YAML a été vérifiée avec un analyseur Python, mais aucun n’a tourné sur GitHub. Testez-les d’abord sur une branche.
  • Le sort de la redevance des runners auto-hébergés. Reportée, mais nous n’avons trouvé aucune annonce qui l’annule.
  • Le prix de GitHub Pro. La page de prix lue ne l’affiche pas, ni aucun prix en euros.
  • Le statut exact des fichiers de verrouillage. La documentation les décrit sans préciser s’ils sont en préversion ou disponibles pour tous.
  • La durée réelle des jobs. Nos chiffres de minutes sont des minimums dus à l’arrondi, pas des mesures.

Questions fréquentes

Combien de minutes GitHub Actions gratuites pour un dépôt privé ?

Sur l’offre Free, 12 $ de calcul par mois, soit 2 000 minutes sur le runner Linux 2 cœurs à 0,006 $ la minute, mais 193 seulement sur macOS, selon la page de facturation de GitHub lue le 31 octobre 2026. Les dépôts publics sur les runners standards et les runners auto-hébergés ne consomment rien.

Quelle version d’actions/checkout utiliser ?

La v7 : au 31 octobre 2026, la dernière version publiée est la 7.0.1. Épinglez-la sur son SHA complet, avec la version en commentaire.

Quelle différence entre une action et un workflow ?

Le workflow est votre fichier YAML, déclenché par un événement, qui contient des jobs. Une action est une brique appelée dans un step avec uses, comme actions/setup-node. Un workflow réutilisable s’appelle, lui, au niveau d’un job.

Comment publier une image Docker avec GitHub Actions ?

Connectez-vous à ghcr.io avec docker/login-action et le GITHUB_TOKEN, puis construisez et poussez avec docker/build-push-action, dans un job qui déclare packages: write (étape 5).


À propos de l’auteur

Abdelilah Ourti est ingénieur IA et architecte logiciel. Il conçoit et déploie des applications web et des systèmes d’IA, et publie sur NeurauTech des guides et des comparatifs destinés aux développeurs francophones. Ses autres articles sont réunis sur sa page auteur.

Méthode. Synthèse documentaire fondée exclusivement sur des sources publiques consultées le 31 octobre 2026 : documentation de GitHub Actions, de GitHub Packages et de Dependabot, pages de facturation et de prix de GitHub, README et pages de versions des actions officielles et des actions Docker, documentation de Docker, OpenSSF Scorecard, avis de sécurité de GitHub, index des versions de Node.js. Les empreintes SHA ont été lues sur l’API de GitHub. La syntaxe YAML a été vérifiée localement, mais aucun workflow n’a été exécuté. Aucun lien n’est affilié et aucune relation commerciale ne lie l’auteur aux éditeurs mentionnés.

Une erreur ou une version périmée ? Écrivez à [email protected] : l’article sera corrigé et la date de mise à jour modifiée.

Dernière mise à jour : 31 octobre 2026.

Image : Alberto Rodríguez (Unsplash License).

Publications similaires