Lorsqu’un même Mac cloud enchaîne les tâches de build, le problème le plus difficile à détecter n’est souvent pas un échec de compilation, mais un état résiduel qui permet à la tâche suivante de « réussir par hasard » : des dépendances sont déjà présentes dans le cache, un service en arrière-plan occupe encore un port, des identifiants temporaires n’ont pas été supprimés ou un agent utilisateur subsiste dans la session graphique. Ce type de pipeline peut sembler stable à court terme, puis révéler tous ses défauts dès qu’il est déplacé sur une autre machine ou que l’ordre des tâches change. La solution ne consiste pas à ajouter une commande de suppression brutale à la fin, mais à délimiter clairement les identités d’exécution, les répertoires, les caches et les domaines de session, puis à prévoir une vérification pour chaque frontière.
Définir d’abord les frontières à isoler
Il faut contrôler au moins cinq catégories d’état : l’utilisateur d’exécution, le répertoire de travail, le répertoire temporaire, les caches et les processus en arrière-plan. Le service CI lui-même peut être lancé par un démon système, mais les builds ne doivent pas s’exécuter durablement avec des privilèges administrateur. Une approche plus sûre consiste à créer un compte standard sans droits d’administration, par exemple ci_runner, et à lui confier le checkout, la compilation et les tests.
L’objectif de l’isolation n’est pas d’empêcher une tâche de « voir toute la machine », mais d’éviter que l’état mutable produit par une exécution n’affecte la suivante sans que cette dépendance ait été déclarée.
Commencez par enregistrer un état de référence afin de disposer d’un point de comparaison lors des diagnostics ultérieurs :
id
umask
printf 'HOME=%s\nTMPDIR=%s\n' "$HOME" "$TMPDIR"
launchctl print "gui/$(id -u)" >/tmp/launchctl-baseline.txt 2>&1 || true
ps -axo user,pid,ppid,command
Si le runner doit effectuer quelques opérations privilégiées, limitez-les à un script contrôlé et accordez uniquement les autorisations nécessaires à ces actions précises. Évitez d’exécuter l’ensemble du build avec des privilèges élevés. Les scripts de compilation ne doivent pas non plus modifier la chaîne d’outils système, les paramètres réseau globaux ni les répertoires d’autres utilisateurs.
Créer un espace de travail privé pour chaque tâche
Le répertoire de la tâche doit être généré à partir d’un identifiant imprévisible mais auditable, en refusant les barres obliques, les espaces et les caractères de contrôle. Définissez ses permissions sur 700 et placez également les fichiers temporaires à l’intérieur de cet espace afin que plusieurs tâches simultanées ne partagent pas le répertoire temporaire du système.
set -eu
case "${RUN_ID:-}" in
""|*[!A-Za-z0-9._-]*)
echo "Invalid RUN_ID" >&2
exit 64
;;
esac
RUN_ROOT="/Users/ci_runner/Jobs/$RUN_ID"
install -d -m 700 "$RUN_ROOT"
install -d -m 700 "$RUN_ROOT/src" "$RUN_ROOT/tmp" "$RUN_ROOT/cache"
export HOME="/Users/ci_runner"
export TMPDIR="$RUN_ROOT/tmp/"
export XDG_CACHE_HOME="$RUN_ROOT/cache"
umask 077
Ne placez pas l’espace de travail dans un répertoire partagé accessible en écriture à toutes les tâches. Si un cache d’amorçage en lecture seule doit être partagé, copiez-le dans le cache local au début de la tâche, puis autorisez le build à ne modifier que cette copie. Vous profitez ainsi des données préchargées sans qu’une tâche en échec puisse contaminer la base commune.
Isoler avec les permissions, pas avec une convention de nommage
La présence d’un numéro de tâche dans le nom du répertoire ne garantit aucune sécurité. Utilisez stat -f '%Su %Sp %N' "$RUN_ROOT" pour vérifier le propriétaire et les permissions, puis ls -lde pour rechercher d’éventuelles ACL supplémentaires. Si des règles héritées sont détectées, identifiez d’abord leur origine, puis retirez les autorisations inutiles au moyen d’une procédure d’initialisation contrôlée. N’élargissez pas récursivement les permissions depuis le script de build.
Répartir les caches selon trois durées de vie
Vider tous les caches ralentit le pipeline, tandis que les réutiliser sans distinction augmente les risques de contamination. En pratique, ils peuvent être classés selon leur durée de vie :
La clé de cache doit au minimum inclure la version de la chaîne d’outils, l’empreinte du fichier de verrouillage des dépendances et l’architecture cible. Une clé limitée au nom de la branche peut continuer à restaurer d’anciens artefacts après un changement de chaîne d’outils. Après la restauration, effectuez une vérification légère : contrôlez par exemple l’empreinte du fichier de verrouillage, l’architecture des binaires essentiels et le propriétaire des répertoires. Si les conditions ne sont pas remplies, rejetez cette copie au lieu d’essayer de la réparer sur place.
Les données sensibles ne doivent pas être mises en cache. Les identifiants de courte durée doivent être injectés dans l’environnement de la tâche et n’exister que dans les sous-processus qui en ont besoin. Les journaux ne doivent jamais afficher l’environnement complet. À la fin de la tâche, exécutez explicitement unset sur les variables concernées et vérifiez qu’aucun fichier généré n’a été placé dans le répertoire d’archivage.
Bien comprendre les domaines de session launchctl
Sous macOS, un agent en arrière-plan peut appartenir au domaine système, au domaine utilisateur ou au domaine de la session graphique. Le fait qu’un build soit exécuté par ci_runner ne signifie pas automatiquement que tous les agents qu’il lance se trouvent dans le domaine attendu. Commencez par vérifier l’identifiant de l’utilisateur, puis examinez le domaine ciblé :
uid="$(id -u ci_runner)"
sudo -u ci_runner launchctl print "user/$uid" >/tmp/user-domain.txt
if launchctl print "gui/$uid" >/dev/null 2>&1; then
sudo -u ci_runner launchctl print "gui/$uid" >/tmp/gui-domain.txt
fi
Un build entièrement exécuté en ligne de commande ne dépend généralement pas d’une session graphique. En revanche, les tests nécessitant un simulateur ou une interface graphique doivent d’abord vérifier qu’une session valide existe. Il ne faut pas masquer l’absence de session en relançant continuellement les agents. Pour les services temporaires démarrés par une tâche, enregistrez leur PID et envoyez-leur un signal d’arrêt normal pendant la phase de sortie. Une terminaison globale par nom de processus pourrait interrompre d’autres tâches exécutées sur la même machine.
Détecter les processus résiduels entre les tâches
Capturez un instantané de ps avant et après la tâche, puis établissez les correspondances selon l’utilisateur, le processus parent et le répertoire de travail. La seule vérification des ports est insuffisante : les observateurs de fichiers et les démons de test sans port d’écoute consomment eux aussi des ressources. Tout processus encore actif après la sortie et dont la ligne de commande pointe vers le RUN_ROOT actuel doit être considéré comme un échec, et non ignoré silencieusement.
Mettre en place une vérification de sortie susceptible d’échouer
La phase de nettoyage doit préserver le code de sortie d’origine de la tâche tout en rendant visibles les anomalies de nettoyage. Il est recommandé d’effectuer les contrôles dans un ordre fixe :
- Arrêter les sous-processus lancés par la tâche et attendre leur terminaison.
- Vérifier que l’espace de travail ne contient ni sockets, ni points de montage, ni fichiers aux permissions anormales.
- Copier les rapports de test dans un répertoire d’archivage contrôlé situé hors de l’espace de travail.
- Supprimer les identifiants temporaires et les variables d’environnement de la tâche.
- Vérifier le préfixe du chemin, puis supprimer le répertoire de la tâche.
- Capturer de nouveau l’état des processus et de launchctl, puis le comparer à l’état de référence.
Protégez impérativement le chemin avant toute suppression :
cleanup() {
case "$RUN_ROOT" in
/Users/ci_runner/Jobs/*)
rm -rf -- "$RUN_ROOT"
;;
*)
echo "Refusing unsafe cleanup path" >&2
return 1
;;
esac
}
trap cleanup EXIT HUP INT TERM
Le script de vérification ne doit pas ignorer toutes les erreurs dans le seul but de conserver un statut vert. Un échec d’archivage, un processus résiduel ou un changement de propriétaire de répertoire doit produire un statut non nul explicite. Pour une CI fonctionnant en continu sur SoarMac, commencez par vérifier dans la console les configurations actuellement disponibles, puis décidez s’il faut répartir les runners selon la pression exercée par les tâches simultanées sur la mémoire et le stockage. Quelle que soit la configuration choisie, les frontières d’identité et d’état doivent rester identiques.
Liste de contrôle avant la mise en production
Lors de la première intégration, exécutez successivement deux tâches au contenu différent mais suivant les mêmes étapes. Faites volontairement créer par la première un fichier de cache, un processus en arrière-plan et une variable temporaire. La seconde ne doit pouvoir lire aucun fichier non déclaré, ni hériter des processus ou identifiants de la précédente. Vérifiez ensuite les points suivants :
- le runner utilise un compte dédié sans droits d’administration ;
- chaque répertoire de tâche possède des permissions
700et son chemin est validé ; - les répertoires temporaires et les caches modifiables ne sont pas partagés par défaut entre les tâches ;
- le cache d’amorçage de la machine est accessible en lecture seule à l’utilisateur de build ;
- les tests graphiques indiquent explicitement le domaine de session launchctl dont ils dépendent ;
- les sous-processus sont arrêtés selon leur PID ou leur relation avec la tâche, jamais globalement par nom ;
- la suppression n’est effectuée qu’après la fin de l’archivage ;
- un échec de nettoyage fait échouer la tâche au lieu de simplement ajouter une ligne au journal.
Lorsque ces contrôles deviennent reproductibles, le succès du pipeline repose enfin sur des entrées déclarées, et non sur un état laissé par hasard sur la machine. Vous pouvez alors modifier la concurrence, migrer les tâches ou changer de répertoire d’exécution tout en utilisant les mêmes frontières pour déterminer rapidement l’origine des différences.
Questions fréquentes
Faut-il créer un compte macOS pour chaque tâche CI ?
Généralement non. Utilisez d’abord un compte standard sans droits administrateur réservé au runner, puis un répertoire de mode 700 par tâche. Séparez les comptes si les équipes ne se font pas confiance ou si les niveaux de sécurité diffèrent.
Pourquoi la suppression de l’espace de travail ne suffit-elle pas ?
Une tâche peut laisser des données temporaires, des caches utilisateur, des identifiants, des processus et un état launchctl. La phase de sortie doit contrôler chaque frontière et ne supprimer qu’un chemin de tâche validé.
Choisissez un Mac dans le cloud selon votre charge de travail
Vérifiez la puce, la mémoire, le stockage, la durée de location et le nœud, puis commandez une machine physique dédiée Apple Silicon.