Fiabiliser les scripts CI sur Mac

DevOps et CI/CD ·~6 min de lecture

Fiabiliser les scripts CI sur Mac

Un script de build peut fonctionner parfaitement sur le Mac d’un développeur, puis échouer sur un Mac distant avec Permission denied, bad interpreter ou un mystérieux $'\r' en fin de ligne. En général, ces erreurs ne viennent pas du code métier, mais d’un contrat incohérent entre le mode de fichier Git, les fins de ligne et l’interpréteur réellement utilisé. Commencez par conserver la commande en échec, l’identifiant du commit et le chemin du script, puis effectuez les vérifications ci-dessous dans l’ordre. Cette méthode est souvent plus rapide que de vider les caches à répétition.

Identifier d’abord la couche où se produit l’échec

Face à un « échec de script », n’ajoutez pas immédiatement chmod +x. Un script Shell possède au moins quatre niveaux d’état : les permissions dans l’arborescence de travail, le mode enregistré dans l’index Git, les octets réellement présents dans le fichier et la manière dont le runner l’exécute.

script="ci/build.sh"

ls -l "$script"
git ls-files -s -- "$script"
file "$script"
head -n 1 "$script" | cat -vet

Dans la sortie de git ls-files -s, la première colonne vaut 100755 lorsque Git a enregistré le bit exécutable, et 100644 pour un fichier ordinaire. Si cat -vet affiche ^M en fin de ligne, le fichier contient des fins de ligne CRLF. Vérifiez également que la première ligne contient un shebang valide, comme #!/bin/zsh ou #!/usr/bin/env bash.

Le mode d’appel doit lui aussi être consigné. bash ci/build.sh n’exige pas que le fichier soit exécutable, tandis que ./ci/build.sh vérifie les permissions et lit le shebang. Si une phase de build Xcode, un script d’orchestration et un terminal local n’utilisent pas la même méthode, cela peut donner l’impression que le problème ne se produit que dans la CI.

Le but n’est pas de faire passer une exécution par hasard, mais d’indiquer clairement dans le dépôt quel interpréteur doit exécuter le script, si celui-ci peut être lancé directement et quelles fins de ligne sont obligatoires.

Corriger le bit exécutable dans l’index Git

Les scripts servant de points d’entrée et lancés directement doivent être enregistrés comme exécutables. Les fragments chargés avec source ou transmis explicitement à un interpréteur restent généralement en 100644. Commencez par dresser la liste des points d’entrée afin de ne pas rendre exécutables tous les fichiers .sh.

git update-index --chmod=+x ci/build.sh
git update-index --chmod=+x ci/test.sh
git diff --summary
git ls-files -s -- ci/build.sh ci/test.sh

Si git diff --summary n’affiche aucun changement de mode, vérifiez si le suivi des permissions est désactivé localement :

git config --get core.fileMode
git config core.fileMode true

core.fileMode détermine uniquement comment l’arborescence de travail locale détecte les changements. C’est le mode enregistré dans l’index Git qui est propagé avec les commits. N’utilisez pas chmod -R +x . au démarrage du runner comme solution durable : cette commande masque les erreurs du dépôt et peut accorder inutilement des droits d’exécution à des fichiers de configuration, à des secrets ou à de simples fichiers de données.

Vous pouvez déclarer, ligne par ligne dans ci/entrypoints.txt, les scripts autorisés à être lancés directement. La validation ne porte alors que sur cette liste. Tout nouveau point d’entrée doit également être ajouté au fichier, ce qui permet à la revue de code de comprendre pourquoi la frontière des permissions a changé.

Imposer LF et normaliser l’historique avec précaution

Le fichier .gitattributes situé à la racine du dépôt doit constituer l’unique source de vérité pour les règles de fins de ligne. Pour les fichiers de build courants, utilisez par exemple la base suivante :

*.sh       text eol=lf
*.command  text eol=lf
*.zsh      text eol=lf
*.xcconfig text eol=lf
*.pbxproj  text eol=lf
*.png      binary
*.zip      binary

L’ajout de ces règles ne réécrit pas automatiquement les fichiers déjà commités. Exécutez les commandes suivantes dans une branche propre :

git add --renormalize .
git status --short
git diff --check
git diff --cached --stat

Examinez d’abord les différences, puis créez un commit distinct pour la normalisation, sans y mêler de modifications fonctionnelles. Si de nombreux fichiers de projet sont modifiés en une seule fois, vérifiez sur un échantillon que seuls les caractères de fin de ligne ont changé. Les archives, les générateurs de code et les scripts externes peuvent toujours réintroduire des CRLF : .gitattributes ne remplace donc pas un contrôle bloquant.

Résultat du contrôle Cause fréquente Action correcte
100644 avec exécution directe Les permissions ne sont pas enregistrées dans l’index Corriger avec git update-index --chmod=+x
^M apparaît en fin de ligne Le fichier utilise CRLF Normaliser en LF et commiter
bad interpreter Le shebang contient un CR ou désigne un chemin invalide Corriger la première ligne et vérifier que l’interpréteur existe
Succès en local, échec dans la CI La méthode d’appel ou le Shell diffère Afficher la commande d’appel et choisir explicitement le Shell

Transformer les différences d’interpréteur en contrat explicite

Sous macOS, le Shell interactif par défaut et celui réellement utilisé dans la CI ne sont pas nécessairement identiques. Un script qui utilise des tableaux Bash, [[ ]] ou la substitution de processus ne doit pas être interprété implicitement par sh. S’il dépend de la syntaxe zsh, il doit également le déclarer explicitement.

#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"

Ne comptez pas sur les fichiers de configuration interactifs pour fournir le PATH, des alias ou des variables d’environnement. Le point d’entrée de la CI doit définir lui-même un PATH minimal et vérifier la présence des outils avec des commandes comme command -v xcodebuild ou command -v ruby. Les variables d’environnement requises doivent être injectées par la configuration de la tâche. Le script doit seulement vérifier leur nom, sans afficher les valeurs sensibles.

Évitez également d’exécuter avec sh script.sh un fichier dont le shebang indique Bash, car l’appel explicite à sh contourne le shebang. Sur les nœuds DebugMac aussi, les tâches doivent respecter les déclarations du dépôt au lieu de déduire l’environnement d’exécution à partir du Shell disponible par hasard dans une session donnée.

Mettre en place un double contrôle avant commit et dans la CI

Un hook local réduit le délai de retour, mais ne peut pas constituer l’unique défense, car les hooks ne sont pas activés automatiquement lors d’un clone standard. La CI doit lancer des contrôles légers avant l’installation des dépendances et le build Xcode.

#!/usr/bin/env bash
set -euo pipefail

while IFS= read -r script; do
  [[ -z "$script" ]] && continue
  mode="$(git ls-files -s -- "$script" | awk '{print $1}')"
  [[ "$mode" == "100755" ]] || {
    printf 'entrypoint is not executable: %s\n' "$script" >&2
    exit 1
  }
done < ci/entrypoints.txt

if git grep -Il $'\r' -- '*.sh' '*.zsh' '*.command' > /tmp/crlf-files; then
  printf 'CRLF detected:\n' >&2
  cat /tmp/crlf-files >&2
  exit 1
fi

Le contrôle doit aussi vérifier que les chemins répertoriés existent réellement et refuser toute différence de normalisation non commitée. La validation finale peut suivre quatre étapes : effectuer un nouveau checkout, lancer directement les scripts d’entrée, vérifier que git status --porcelain ne renvoie rien, puis réaliser un build minimal dans une session non interactive. Les problèmes de permissions et de fins de ligne apparaîtront ainsi avant le téléchargement coûteux des dépendances et le début de la compilation, sans qu’il soit nécessaire de modifier le runner à la volée pour maintenir le pipeline opérationnel.

Questions fréquentes

Le bit exécutable est-il utile si la CI lance le fichier avec bash ?

La commande bash script.sh n’en dépend pas, mais une exécution directe ou une phase de build peut l’exiger. Les vrais points d’entrée doivent donc être suivis en mode 100755 dans Git.

Un fichier .gitattributes corrige-t-il immédiatement les anciens fichiers ?

Non. Il faut lancer git add --renormalize ., examiner le diff puis valider la normalisation. Un contrôle CI reste nécessaire pour les fichiers générés ou importés.

Debug M4

Lancez votre prochaine compilation sur un nœud physique Apple Silicon dédié

Choisissez la durée de location et la région du nœud, vérifiez le Mac Mini M4, les 16 Go de RAM, le SSD de 256 Go et le montant en USD, puis envoyez votre commande.

Louer un Mac cloud maintenant