Un bloc run: commence par une seule ligne. Puis quelqu’un ajoute une reprise. Puis une vérification. Six mois plus tard, ce sont 50 lignes de Bash dans du YAML, et personne ne peut les exécuter en dehors de la CI.
Cet article montre comment déplacer cette logique dans des scripts, à partir de l’exemple Docker de rzv-ci-plb, le guide CI de Runzivo.
L’erreur courante
À ne pas faire. Un test de fumée écrit en ligne :
- name: Smoke test image
run: |
docker run -d --name app -p 8080:8080 "app:${{ github.sha }}"
for attempt in 1 2 3 4 5; do
if curl -fsS http://127.0.0.1:8080/healthz; then
break
fi
sleep 1
done
docker rm -f appÇa paraît correct. Pourtant, il y a quatre problèmes :
- Il réussit quand l’application est défaillante. Si chaque
curléchoue, la boucle se termine en silence etdocker rmréussit ; l’étape se termine donc avec le code 0. Je l’ai exécuté avec uncurlen échec pour le confirmer. - Le nettoyage est ignoré en cas d’échec. Si
docker runéchoue, l’étape s’arrête avantdocker rm. - Vous ne pouvez pas l’exécuter en local sans le copier hors du YAML.
${{ github.sha }}est collé dans le texte du script avant même le démarrage du shell. C’est sans danger pour un SHA. La même habitude appliquée au titre d’une demande de tirage (pull request) constitue une injection de script.
Un détail de plus : sans shell: bash, GitHub exécute ceci avec bash -e {0}, qui n’a pas de pipefail. Avec shell: bash, il exécute bash --noprofile --norc -eo pipefail {0}. La référence de la syntaxe des flux de travail présente les deux. Le guide définit defaults.run.shell: bash dans chaque flux de travail.
La règle des trois lignes
Ma règle : un bloc run: de trois lignes ou moins peut rester en ligne. Déplacez-le dans un script dès qu’il comporte :
- des conditions ou des boucles
- des reprises
- du nettoyage
- de la gestion d’erreurs
- plus de trois lignes
Il s’agit d’une convention de Runzivo, pas d’une exigence de GitHub. Elle est plus stricte que le Google Shell Style Guide, qui suggère un langage structuré dès qu’un script dépasse 100 lignes ou que son flux de contrôle n’est pas simple. L’objectif est le même : garder le shell court et facile à lire.

L’approche recommandée
Les étapes du flux de travail deviennent de simples appels. Les valeurs arrivent par env:. Extrait de example-docker.yml :
- name: Build image
run: bash "${GITHUB_WORKSPACE}/examples/docker/ci/01-build.sh"
- name: Smoke test image
env:
REPORT_DIR: ${{ runner.temp }}/reports
run: bash "${GITHUB_WORKSPACE}/examples/docker/ci/02-test.sh"Le nom de l’image provient d’un bloc env: au niveau de la tâche (IMAGE) ; aucune expression ne se retrouve donc dans un script.
Le script de test de fumée contient la logique. Cet extrait provient de examples/docker/ci/02-test.sh :
container_id=""
save_logs_and_remove() {
if [[ -n "$container_id" ]]; then
# Logs are best effort: a container that failed to start may have none.
docker logs "$container_id" >"${REPORT_DIR}/docker-container.log" 2>&1 ||
log_warn "could not save container logs"
docker rm -f "$container_id" >/dev/null
fi
}
cleanup_register save_logs_and_remove
log_group_start "Start container"
container_id="$(docker run --detach --publish 127.0.0.1::8080 \
--label "ci.example=docker" --read-only --cap-drop ALL "$IMAGE")"
host_port="$(docker port "$container_id" 8080/tcp)"
host_port="${host_port##*:}"
log_info "container ${container_id:0:12} listening on 127.0.0.1:${host_port}"
log_group_end
log_group_start "Check /healthz"
body="$(retry 10 1 curl --fail --silent --show-error "http://127.0.0.1:${host_port}/healthz")"
[[ "$body" == "ok" ]] || die "unexpected /healthz body: ${body}"
log_group_endChaque problème de la version en ligne est corrigé :
retryrenvoie le dernier code de sortie lorsque toutes les tentatives échouent ; une application défaillante fait donc échouer l’étape. Voirretrydansscripts/lib/03-errors.sh.cleanup_registersupprime le conteneur en cas de réussite, d’échec ou d’annulation. Voirscripts/lib/04-cleanup.sh.- Le script s’exécute de la même façon sur un portable :
bash scripts/ci/02-test.sh dockerCette commande construit l’image avec 01-build.sh, puis exécute 02-test.sh.
Numérotez les scripts selon l’ordre d’exécution
Chaque script du guide commence par un nombre à deux chiffres : 01-build.sh s’exécute avant 02-test.sh, et 00 est réservé à l’installation. Les listes de répertoires montrent ainsi l’ordre, et un script inutilisé se remarque. Voir scripts numérotés.
Ce que vous obtenez
Exécutions locales. Une personne développeuse reproduit un échec de CI avec une seule commande.
Une vraie analyse statique. ShellCheck lit directement un fichier .sh. actionlint exécute aussi ShellCheck sur les blocs run:, mais un fichier de script est plus facile à vérifier, à formater et à réviser.
Des tests. Bats teste les fonctions utilitaires partagées, y compris les chemins d’échec. Voir tests/bats/03-errors.bats pour retry.
Des flux de travail portables. Le YAML ne fait que coordonner ; passer à un autre exécuteur compatible revient donc à changer runs-on, sans réécrire les étapes.
Commandes pour vérifier vos scripts
Tirées du README du dépôt :
brew install actionlint shellcheck shfmt bats-core zizmor # on a Mac
bash scripts/ci/01-validate.sh # actionlint, shellcheck, shfmt, zizmor
bash scripts/ci/02-test.sh # Bats tests and every example
bash scripts/ci/02-test.sh golang # one suiteQuand le code en ligne convient
Les commandes courtes et à usage unique peuvent rester dans le YAML. Un exemple simplifié :
- name: Install dependencies
run: npm ciEn cas de doute, demandez-vous : « Voudrais-je exécuter ceci sur mon portable s’il échoue ? » Si oui, faites-en un script.
Exécuter sur Runzivo
Runzivo est en accès anticipé. Il exécute chaque tâche GitHub Actions admissible dans une microVM neuve à usage unique. Comme la logique se trouve dans des scripts, déplacer une tâche revient à changer une seule ligne :
runs-on: rzv-ubuntu-24.04-x64-2vcpu-8gbVoir examples/runzivo-runners et la documentation de Runzivo.
Dans le dépôt
examples/docker/ci/01-build.shet02-test.sh.github/workflows/example-docker.ymlscripts/lib/03-errors.sh(retry,die) etscripts/lib/04-cleanup.shscripts/ci/02-test.sh: exécute une suite ou toutes les suites- Règles sur les scripts de
AGENTS.md: la règle des trois lignes et la numérotation, écrites pour les agents de programmation
La suite : la gestion des erreurs avec les traps, le nettoyage, la journalisation et les codes de sortie.
Chaque exemple de cette série est testé dans rzv-ci-plb, the Runzivo CI playbook, fixé à l’étiquette v0.1.0.