Éviter qu’un pipeline CI Mac masque un échec xcodebuild

DevOps et CI/CD ·~6 min de lecture

Éviter qu’un pipeline CI Mac masque un échec xcodebuild

Après l’ajout de tee aux journaux de build, la sortie du terminal devient complète, mais il arrive que la CI considère comme réussi un xcodebuild qui a manifestement échoué. Le plus dangereux n’est pas la perte de journaux, mais le fait que le pipeline poursuive les étapes d’envoi, de notification ou de déploiement. La cause est généralement simple : par défaut, le shell renvoie le code de sortie de la dernière commande du pipeline. Si tee parvient à écrire la sortie, son succès masque alors l’échec précédent de xcodebuild.

Vérifier d’abord comment l’erreur est masquée

Cette forme est très courante :

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App \
  -destination "$CI_DESTINATION" \
  2>&1 | tee build.log

Le pipeline contient deux processus. Celui de gauche exécute les tests, tandis que celui de droite écrit la sortie à la fois dans un fichier et sur la sortie standard. Dans Bash, lorsque pipefail n’est pas activé, le statut de la commande entière est par défaut celui de tee. Même si les tests renvoient une valeur non nulle, le script peut donc encore recevoir 0 dès lors que l’écriture du journal réussit.

Commencez par effectuer une vérification minimale sur le Mac cloud :

/bin/bash -c 'exit 65' | tee /tmp/pipeline-check.log
printf 'pipeline=%s\n' "$?"

Si la sortie indique pipeline=0, le script actuel peut produire un faux succès. Ne vous contentez pas de rechercher error: dans le journal : le format des messages varie selon le compilateur, le framework de test et les scripts personnalisés. Le code de sortie reste l’interface stable utilisée par la CI pour déterminer si une tâche a réussi ou échoué.

Les journaux servent à expliquer un échec ; le code de sortie sert à le déclarer. Les deux doivent être conservés séparément, sans remplacer l’état du processus par une recherche textuelle.

Fixer le shell avant de gérer l’état du pipeline

Le shell interactif par défaut de macOS peut différer de l’interpréteur réellement utilisé par les scripts de CI. Bash utilise PIPESTATUS, tandis que zsh utilise pipestatus, avec également une indexation différente des tableaux. La méthode la plus fiable consiste à déclarer l’interpréteur dans un script versionné dans le dépôt, puis à faire exécuter directement ce fichier par le pipeline au lieu de transmettre plusieurs lignes de commandes à un shell indéterminé.

#!/bin/bash
set -uo pipefail

L’option -u intercepte les variables non définies, tandis que pipefail fait renvoyer un statut non nul à l’ensemble du pipeline dès qu’une de ses commandes échoue. Toutefois, pipefail seul ne fournit pas assez de précision : si tee échoue à cause d’un problème de disque ou de permissions alors que xcodebuild réussit, le $? final indique uniquement que le pipeline a échoué, sans préciser quelle étape est en cause.

Il faut donc copier PIPESTATUS immédiatement après la fin du pipeline. Une affectation, un test conditionnel ou une commande de journalisation peut actualiser cette valeur ; une lecture trop tardive renverrait alors l’état d’une autre commande.

Écrire un wrapper xcodebuild réutilisable

Le script suivant conserve simultanément le journal brut, le paquet de résultats et le véritable code de sortie de xcodebuild. Il désactive temporairement errexit afin d’éviter que le shell ne quitte avant l’enregistrement des différents statuts.

#!/bin/bash
set -uo pipefail

run_id="${CI_RUN_ID:-local-$(date +%s)}"
artifact_dir="${CI_ARTIFACT_DIR:-$PWD/artifacts/$run_id}"
result_path="$artifact_dir/TestResults.xcresult"
log_path="$artifact_dir/xcodebuild.log"

mkdir -p "$artifact_dir"

set +e
xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Debug \
  -destination "$CI_DESTINATION" \
  -resultBundlePath "$result_path" \
  2>&1 | tee "$log_path"

pipeline_status=("${PIPESTATUS[@]}")
xcode_status="${pipeline_status[0]}"
tee_status="${pipeline_status[1]}"
set -e

printf 'xcodebuild_exit=%s\n' "$xcode_status"
printf 'tee_exit=%s\n' "$tee_status"

if (( tee_status != 0 )); then
  printf 'Log capture failed\n' >&2
fi

exit "$xcode_status"

CI_DESTINATION doit être fourni explicitement par la configuration de la tâche, par exemple en le liant à une destination de simulateur déjà validée. Évitez de sélectionner de manière approximative le premier appareil disponible dans le script, car une mise à jour de l’environnement d’exécution pourrait conduire à choisir une autre version du système. Toutes les variables de chemin doivent être placées entre guillemets pour éviter le découpage des arguments lorsque le répertoire de travail contient des espaces.

Ici, le statut de xcodebuild devient le statut final de la tâche. Si un échec d’écriture du journal doit lui aussi bloquer la tâche, ajoutez une vérification avant la sortie : renvoyez un échec dès que l’un des deux statuts est non nul, tout en enregistrant séparément chaque statut dans le résumé de la tâche.

Traiter le paquet de résultats comme une preuve de diagnostic

La sortie texte de la console est pratique pour une lecture rapide, tandis que le fichier xcresult conserve la hiérarchie des tests, les pièces jointes associées aux échecs, les journaux d’activité et les informations de diagnostic. Leurs usages étant différents, il est recommandé d’archiver les deux.

Artefact Usage principal Conservation en cas d’échec
xcodebuild.log Rechercher les sorties du compilateur et les journaux des scripts Oui
TestResults.xcresult Consulter les tests, les pièces jointes et les journaux d’activité Oui
exit-status.txt Enregistrer l’état de chaque étape du pipeline Oui
DerivedData temporaire Conservation à court terme si une reproduction sur place est nécessaire Selon la stratégie de la tâche

Pour produire un résumé lisible par une machine, utilisez l’outil système xcresulttool. Vérifiez d’abord que le paquet de résultats existe réellement avant de l’analyser, afin qu’un échec d’analyse ne remplace pas l’état initial du build.

if [[ -d "$result_path" ]]; then
  xcrun xcresulttool get test-results summary \
    --path "$result_path" \
    > "$artifact_dir/test-summary.json" || true
fi

printf 'xcodebuild=%s\ntee=%s\n' \
  "$xcode_status" "$tee_status" \
  > "$artifact_dir/exit-status.txt"

Ici, || true autorise uniquement la poursuite de l’archivage lorsque la génération du résumé échoue. Il ne doit pas être ajouté après la commande principale xcodebuild. L’échec d’un rapport auxiliaire ne doit pas effacer l’état principal déjà enregistré, et l’échec du build principal ne doit pas non plus être transformé en succès par une commande auxiliaire.

Valider avec des scénarios d’échec, pas seulement avec des tâches au vert

Avant d’intégrer le wrapper, exécutez au minimum les quatre catégories de validation suivantes :

Échec de compilation

Référencez temporairement un symbole inexistant. Le résultat attendu est un statut xcodebuild non nul, un échec de la CI et la présence du journal. La création du paquet de résultats dépendra de l’étape à laquelle l’échec se produit.

Échec de test

Ajoutez une assertion qui échoue systématiquement. La compilation doit se terminer, mais la tâche de test doit échouer. Le cas de test en échec doit être identifiable dans xcresult, et le pipeline ne doit pas poursuivre la branche réservée aux succès.

Échec d’écriture du journal

Faites pointer le répertoire des artefacts vers un emplacement non accessible en écriture et vérifiez que tee_status est non nul. Si l’équipe exige des artefacts de diagnostic complets avant toute livraison, ce scénario doit lui aussi bloquer la tâche.

Interruption et expiration du délai

Demandez à la CI d’envoyer un signal de terminaison, vérifiez qu’aucune étape de publication ultérieure ne s’exécute et assurez-vous que les processus enfants sont bien arrêtés. Ne transformez pas une expiration du délai en échec de test ordinaire : ces deux situations n’ont pas la même signification pour la stratégie de nouvelle tentative ni pour l’analyse de l’incident.

Enfin, vérifiez la définition même de la tâche. Les étapes d’envoi des artefacts de diagnostic peuvent être configurées pour s’exécuter même après un échec, mais le déploiement, la distribution et les notifications de succès doivent dépendre du véritable statut renvoyé par le wrapper. Ainsi, même si le filtre de sortie, l’outil d’archivage ou le générateur de résumé change, l’échec de xcodebuild continuera d’être transmis à la CI par un chemin unique et vérifiable.

Questions fréquentes

Pourquoi lire PIPESTATUS lorsque pipefail est déjà actif ?

pipefail indique qu’une commande du pipeline a échoué sans garantir que le statut final appartient à xcodebuild. PIPESTATUS[0] conserve précisément le code de la première commande.

Comment valider la propagation correcte des erreurs ?

Provoquez séparément une erreur de compilation, un test en échec et un répertoire de journaux non accessible en écriture. Chaque cas doit faire échouer la tâche et conserver les diagnostics attendus.

Mac physique dédié

Confiez votre prochaine tâche de développement à un Mac dans le cloud

Choisissez Berry M4 ou Berry M4 Pro, vérifiez les cinq nœuds, la durée de location et les options supplémentaires, puis passez commande.

Choisir une configuration et commander