10. Écrire sa propre action
Sections
Réglages de lecture
Voix naturelle enregistrée pour ce cours, la même dans tous les navigateurs.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
Code et termes techniques en anglais
Désactivé : la voix de la page lit tout, sans pause entre deux voix.
Aucune voix pour cette langue dans ce navigateur.
Plus de voix
D’autres voix : Microsoft Edge propose des voix naturelles en ligne, et Windows (Paramètres › Heure et langue › Voix), macOS et Android en installent d’autres.
Trois sortes d’actions
Section intitulée « Trois sortes d’actions »La leçon 6 a écrit une action composite : des steps YAML. Les deux autres sortes exécutent du code :
| Composite | JavaScript | Conteneur Docker | |
|---|---|---|---|
runs.using |
composite |
node24 (ou node20) |
docker |
| Exécute | des steps dans le job de l’appelant | node sur le runner |
un conteneur construit ou téléchargé sur le runner |
| Systèmes d’exploitation | tous | tous | « ne peuvent s’exécuter que sur des runners dotés d’un système d’exploitation Linux » |
| Inputs | ${{ inputs.x }} |
variables d’environnement INPUT_X |
variables INPUT_X et args |
| Coût de démarrage | aucun | aucun | la construction ou le téléchargement de l’image |
| Analogie C# | une méthode faite d’appels à d’autres méthodes | une petite application console | une application console livrée avec son propre OS |
| Azure Pipelines | template de step | tâche personnalisée (Node) | job conteneur |
Les deux exemples ci-dessous vivent dans ce dépôt et sont testés par .github/workflows/gha-10-action.yml.
Une action JavaScript sans dépendances
Section intitulée « Une action JavaScript sans dépendances »La même fonction de slug que le code .NET et Java de la leçon 2, sous forme d’action. .github/actions/slugify/action.yml :
# GitHub Actions course, lesson 10: a JavaScript action without dependenciesname: Slugifydescription: Turns a title into a URL slug, like the sample code of lesson 2.
inputs: text: description: The text to turn into a slug required: true max-length: description: Maximum length of the slug, 0 for no limit default: '0'
outputs: slug: description: The slug
runs: using: node24 main: index.jsindex.js :
// GitHub Actions course, lesson 10: inputs arrive as INPUT_<NAME> variables, outputs go to the GITHUB_OUTPUT fileimport fs from "node:fs";
function input(name, { required = false } = {}) { const variable = `INPUT_${name.replace(/ /g, "_").toUpperCase()}`; const value = (process.env[variable] ?? "").trim(); if (required && value === "") { throw new Error(`Input required and not supplied: ${name}`); } return value;}
function slugify(text) { return text .normalize("NFD") .replace(/\p{Mn}/gu, "") .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "");}
try { const text = input("text", { required: true }); const maxLength = Number(input("max-length") || "0"); let slug = slugify(text); if (maxLength > 0) { slug = slug.slice(0, maxLength).replace(/-+$/, ""); } console.log(`slug of "${text}" is "${slug}"`); fs.appendFileSync(process.env.GITHUB_OUTPUT, `slug=${slug}\n`);} catch (error) { console.log(`::error::${error.message}`); process.exitCode = 1;}- Le runner passe chaque input en variable d’environnement : il « convertit les noms des inputs en lettres majuscules et remplace les espaces par des caractères
_» (Runner.Worker/Handlers/Handler.cs). Les tirets restent :max-lengthdevientINPUT_MAX-LENGTH, un nom que bash ne peut pas lire avec$INPUT_MAX-LENGTH, mais Node si. - Les outputs utilisent le même fichier
$GITHUB_OUTPUTque les stepsrun:(leçon 4) ; les erreurs, la même commande de workflow::error::. - La plupart des vraies actions passent pour cela par le paquet
@actions/core(core.getInput,core.setOutput,core.setFailed), et doivent alors commiternode_modulesou un bundledist/index.js: le runner n’exécute pasnpm install. Sans dépendances, il n’y a rien à bundler.
Comme l’action n’est qu’un script, teste-la en local avant tout push, avec les variables que le runner définirait :
$env:INPUT_TEXT = "C# 14 & .NET 10"; $env:GITHUB_OUTPUT = "out.txt"; node .github/actions/slugify/index.jsINPUT_TEXT="C# 14 & .NET 10" GITHUB_OUTPUT=out.txt node .github/actions/slugify/index.jsINPUT_TEXT="C# 14 & .NET 10" GITHUB_OUTPUT=out.txt node .github/actions/slugify/index.jsslug of "C# 14 & .NET 10" is "c-14-net-10"Les trois mêmes cas que les tests .NET et Java ont donné hello-world, github-actions et c-14-net-10 ; un texte vide a donné ::error::Input required and not supplied: text et le code de sortie 1.
L’utiliser
Section intitulée « L’utiliser »Extrait de .github/workflows/gha-10-action.yml, lignes 23-29 :
- id: slug uses: ./.github/actions/slugify with: text: GitHub Actions course, lesson 10 — Wörld! max-length: 30 - shell: bash run: echo "slug output = ${{ steps.slug.outputs.slug }}"Sur les trois systèmes d’exploitation :
javascript (ubuntu-latest) | slug of "GitHub Actions course, lesson 10 — Wörld!" is "github-actions-course-lesson-1"javascript (windows-latest) | slug of "GitHub Actions course, lesson 10 — Wörld!" is "github-actions-course-lesson-1"javascript (macos-latest) | slug of "GitHub Actions course, lesson 10 — Wörld!" is "github-actions-course-lesson-1"javascript (ubuntu-latest) | slug output = github-actions-course-lesson-1Même résultat partout, et le step de l’action a pris moins d’une seconde. Remarque le 1 : max-length: 30 a coupé 10 en deux. Le code fait exactement ce qu’il dit ; qu’un slug doive s’arrêter à la fin d’un mot est une question de spécification que le test n’a pas posée.
Une action conteneur
Section intitulée « Une action conteneur ».github/actions/hello-container contient trois fichiers :
.github/actions/hello-container/action.yml :
# GitHub Actions course, lesson 10: a container action (Linux runners only)name: Hello from a containerdescription: Prints the operating system of the container it runs in.
inputs: who: description: Who to greet default: world
runs: using: docker image: Dockerfile args: - ${{ inputs.who }}.github/actions/hello-container/Dockerfile :
# GitHub Actions course, lesson 10: the image of the container action, built on the runner at every runFROM alpine:3.22COPY entrypoint.sh /entrypoint.shENTRYPOINT ["/entrypoint.sh"].github/actions/hello-container/entrypoint.sh :
#!/bin/sh# GitHub Actions course, lesson 10: runs inside the container; the workspace is mounted at /github/workspaceset -eecho "hello $1 from $(. /etc/os-release && echo "$PRETTY_NAME")"echo "workdir: $(pwd)"echo "INPUT_WHO=$INPUT_WHO"echo "greeting=hello $1" >> "$GITHUB_OUTPUT"Première exécution : permission refusée
Section intitulée « Première exécution : permission refusée »##[command]/usr/bin/docker build -t 128b89:7c5dc9951c794fae826f3098f2786173 -f "/home/runner/work/learn/learn/./.github/actions/hello-container/Dockerfile" "/home/runner/work/learn/learn/.github/actions/hello-container"...docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: exec: "/entrypoint.sh": permission deniedLe fichier a été écrit sous Windows, où Git ne suit pas le bit exécutable (git config core.filemode → false), il a donc été commité en 100644 — laissé ainsi exprès, pour capturer cette erreur, que tout auteur Windows d’une action conteneur rencontre une fois. La correction se fait dans Git, pas dans le Dockerfile :
git update-index --chmod=+x .github/actions/hello-container/entrypoint.shgit diff --cached --summary mode change 100644 => 100755 .github/actions/hello-container/entrypoint.shDeuxième exécution
Section intitulée « Deuxième exécution »hello Grace from Alpine Linux v3.22workdir: /github/workspaceINPUT_WHO=Gracegreeting output = hello GraceLa commande docker run du runner montre comment le conteneur voit le job :
-v "/var/run/docker.sock":"/var/run/docker.sock"-v "/home/runner/work/_temp":"/github/runner_temp"-v "/home/runner/work/_temp/_github_home":"/github/home"-v "/home/runner/work/_temp/_github_workflow":"/github/workflow"-v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands"-v "/home/runner/work/learn/learn":"/github/workspace"Le workspace est monté, donc le conteneur peut lire le code récupéré par le checkout, et $GITHUB_OUTPUT pointe dans le dossier monté file_commands, donc les outputs fonctionnent. Le step a pris 5 secondes, surtout pour construire l’image — contre moins d’une seconde pour l’action JavaScript. Une action conteneur publiée fait en général pointer image: vers une image déjà construite (docker://ghcr.io/…) pour éviter la construction.
Publier, en bref
Section intitulée « Publier, en bref »Une action dans son propre dépôt public, avec action.yml à la racine, s’utilise avec owner/repo@v1. Les conventions, d’après Gérer les actions personnalisées : publier des releases avec des tags de version sémantique (v1.2.0) et déplacer un tag majeur (v1) vers la dernière release compatible — le tag mobile auquel la leçon 7 conseille aux utilisateurs de ne pas se fier aveuglément. La référencer sur la Marketplace est facultatif. Pas fait dans ce cours : les actions d’ici restent locales.
À retenir
Section intitulée « À retenir »- Les actions JavaScript s’exécutent sur tous les runners et démarrent instantanément ; les actions conteneurs apportent leur propre Linux et ne s’exécutent que sur des runners Linux.
- Les inputs arrivent en variables
INPUT_<NAME>(en majuscules, tirets conservés) ; les outputs et les erreurs passent par$GITHUB_OUTPUTet les commandes de workflow, comme pour n’importe quel step. - Le runner n’impose pas
required: true: c’est à l’action de vérifier. - Commite ce dont l’action a besoin pour s’exécuter : pas de
npm installsur le runner, les bits exécutables des scripts, et attention aupackage.jsondu dépôt.
Exercices
Section intitulée « Exercices »- Un workflow passe
txt: Helloau lieu detext: Helloà l’action slugify. Que fait le runner de l’input inconnu, et de l’input obligatoire manquant ?
Solution
D’après gha-10-exercises.yml :
##[warning]Unexpected input(s) 'txt', valid inputs are ['text', 'max-length']##[error]Input required and not supplied: textL’input inconnu ne produit qu’un avertissement, et le step s’est exécuté quand même. L’erreur vient du code de l’action elle-même : « Les actions qui utilisent required: true ne renvoient pas automatiquement d’erreur si l’input n’est pas spécifié. » Sans la vérification dans index.js, l’action aurait produit un slug vide et un step vert.
- Que se passe-t-il quand
hello-containerest utilisée dans un jobwindows-latest?
Solution
##[error]Container action is only supported on LinuxLe step échoue immédiatement, avant toute construction. Une action destinée aux trois systèmes d’exploitation doit être JavaScript ou composite.
- La première version d’
index.jscommençait parconst fs = require("node:fs");, du Node.js valide. S’exécuterait-elle sur le runner ?
Solution
ReferenceError: require is not defined in ES module scope, you can use import insteadThis file is being treated as an ES module because it has a '.js' file extension and '/home/runner/work/learn/learn/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.Node cherche le package.json le plus proche au-dessus du script, et dans ce dépôt c’est celui du site Astro, qui déclare "type": "module". Non, donc. L’erreur ci-dessus vient du runner, qui exécutait la copie exercise-commonjs ; la même erreur était d’abord apparue en local. Corrections possibles : la syntaxe import (choisie ici), un fichier index.cjs, ou un package.json dans le dossier de l’action. Dans un dépôt dédié à l’action, c’est toi qui maîtrises ce fichier.