Lorsqu’un dépôt Swift de taille moyenne exécute un contrôle complet du formatage sur un Mac cloud, le principal problème vient rarement de la lenteur des outils. À chaque demande de fusion, le pipeline analyse de nouveau des sources inchangées, des répertoires générés et des dépendances externes. La situation se complique encore lorsque les développeurs et la CI utilisent des versions différentes : une même ligne de code peut alors produire deux résultats opposés. La solution ne consiste pas à désactiver des règles, mais à versionner dans le dépôt les outils, leurs fichiers de configuration et le calcul des différences.
Séparer clairement les responsabilités des deux outils
SwiftFormat convient aux opérations de mise en forme déterministes et automatiquement corrigibles, comme l’indentation, les retours à la ligne et les espaces superflus. SwiftLint est mieux adapté à la détection de pratiques à risque, notamment les conversions forcées, les fonctions trop longues et les contraintes de nommage. Si les deux outils gèrent la même règle, un développeur peut formater son code, puis voir celui-ci immédiatement rejeté par l’analyse statique.
Commencez par établir un tableau simple de répartition des règles :
| Type de contrôle | Outil responsable | Comportement dans la demande de fusion |
|---|---|---|
| Indentation, espaces, retour à la ligne des paramètres | SwiftFormat | Contrôle uniquement, sans réécriture automatique |
| Conversions forcées et déballages forcés | SwiftLint | Échec avec indication de l’emplacement du fichier |
| Code généré et dépendances externes | Exclus des deux outils | Absents de la liste des fichiers modifiés |
| Avertissements historiques | Référence SwiftLint | Seuls les nouveaux problèmes sont bloqués |
Dans la CI, n’exécutez pas directement de commandes qui réécrivent les sources. Le contrôle doit uniquement signaler les écarts. Les développeurs les corrigent en local avant de soumettre leurs changements, afin que l’état du dépôt reste reproductible.
Versionner les outils et les règles dans le dépôt
Ne dépendez pas des versions « actuellement installées » sur le Mac cloud. Choisissez d’abord des versions validées par l’équipe, puis inscrivez les versions attendues dans le script. L’exemple ci-dessous utilise SwiftFormat 0.54.3 et SwiftLint 0.55.1 ; dans un projet réel, remplacez-les par les versions que vous avez effectivement validées.
Le fichier .swiftformat peut rester concis :
--swiftversion 5.10
--indent 4
--wraparguments before-first
--exclude .build,DerivedData,Vendor,Generated
Le fichier .swiftlint.yml définit explicitement le périmètre des sources et les répertoires ignorés :
included:
- Sources
- Tests
excluded:
- .build
- DerivedData
- Vendor
- Generated
only_rules:
- force_cast
- force_try
- trailing_whitespace
- unused_import
Les fichiers de règles doivent être examinés au même titre que le code. Avant d’ajouter une règle, lancez une analyse complète du dépôt dans une branche dédiée, mesurez le volume d’avertissements, puis choisissez entre une correction immédiate et la création d’une référence. N’intégrez pas directement des centaines d’anciens avertissements au contrôle bloquant des demandes de fusion : l’équipe finirait simplement par ignorer les échecs.
Extraire correctement les différences Swift d’une demande de fusion
L’utilisation directe de git diff HEAD^ ne convient qu’aux branches comportant un seul commit. Lorsqu’une demande de fusion contient plusieurs commits ou a été rebasée, cette méthode risque d’omettre des fichiers. Une approche plus fiable consiste à calculer d’abord l’ancêtre commun entre le commit courant et la branche cible, puis à extraire les fichiers ajoutés, copiés, modifiés et renommés.
set -euo pipefail
expected_format="0.54.3"
expected_lint="0.55.1"
[[ "$(swiftformat --version)" == "$expected_format" ]]
[[ "$(swiftlint version)" == "$expected_lint" ]]
base_ref="${LINT_BASE_REF:-origin/main}"
merge_base="$(git merge-base HEAD "$base_ref")"
changed=("${(@0)$(git diff --name-only -z --diff-filter=ACMR "$merge_base" HEAD)}")
swift_files=()
for file in "${changed[@]}"; do
[[ "$file" == *.swift ]] || continue
[[ "$file" == .build/* ]] && continue
[[ "$file" == DerivedData/* ]] && continue
[[ "$file" == Vendor/* ]] && continue
[[ "$file" == Generated/* ]] && continue
swift_files+=("$file")
done
(( ${#swift_files[@]} > 0 )) || exit 0
swiftformat --lint "${swift_files[@]}"
swiftlint lint --strict --force-exclude "${swift_files[@]}"
La liste de fichiers est ici séparée par des caractères nuls, ce qui évite de fragmenter les chemins contenant des espaces. L’option --diff-filter=ACMR exclut les fichiers supprimés afin que les outils de contrôle ne reçoivent pas de chemins inexistants. Le nom de la branche cible est transmis par une variable d’environnement : le même script peut ainsi fonctionner sur la branche par défaut comme sur les branches de publication.
Gérer les clones superficiels
Si git merge-base ne trouve aucun ancêtre commun, le script n’est généralement pas en cause : la CI a probablement récupéré un historique trop limité. Augmentez d’abord la profondeur du checkout et vérifiez que la référence de la branche cible existe avant d’exécuter le script. Ne revenez pas à HEAD^, car le contrôle produirait alors des résultats incohérents selon la structure des branches.
Rendre les échecs localisables et reproductibles
Lorsqu’un contrôle échoue, les journaux doivent répondre à au moins trois questions : quelles versions des outils ont été utilisées, quel commit de référence a servi à la comparaison et quels fichiers ont été contrôlés. Une incompatibilité de version doit interrompre immédiatement l’exécution au lieu de laisser le pipeline générer une multitude de différences de formatage.
Pour reproduire le problème en local, il suffit au développeur de récupérer la référence de la branche cible et d’exécuter le même script :
git fetch origin main
LINT_BASE_REF=origin/main zsh Scripts/lint-changed-swift.zsh
Une erreur courante consiste à placer la commande de contrôle dans un pipeline, puis à lire le mauvais code de sortie. Une autre consiste à ajouter || true pour produire des journaux plus propres, au risque de masquer un véritable échec. Avec set -euo pipefail, tout statut non nul renvoyé par un outil interrompt la tâche. Si la CI doit mettre en forme les journaux, elle doit d’abord conserver le code de sortie initial, afficher le résumé, puis quitter avec ce même code.
Maîtriser les coûts à long terme avec une stratégie à deux niveaux
Le contrôle incrémental garantit uniquement que « les changements actuels n’introduisent pas de nouveaux problèmes ». Il ne prouve pas que l’ensemble du code historique respecte les règles en vigueur. Il est donc possible d’organiser les contrôles en deux niveaux :
- Exécutez un contrôle des différences pour chaque demande de fusion. Il doit être rapide, stable et entièrement reproductible en local.
- Planifiez un contrôle complet du dépôt afin de détecter la dérive de la référence, les exclusions devenues inefficaces et les anciens avertissements restés trop longtemps sans traitement.
- Soumettez les mises à niveau des règles séparément, sans les mélanger aux évolutions fonctionnelles, afin de faciliter la revue des modifications massives provoquées par le formatage.
- Les répertoires générés doivent avoir une origine clairement définie. Excluez-les à la fois dans la configuration des outils et dans le script de filtrage des différences, afin que l’ordre des tâches de génération n’influence pas le résultat.
Avant la mise en service, effectuez une dernière vérification : la référence de la branche cible existe-t-elle, les versions des outils correspondent-elles exactement, les fichiers renommés sont-ils inclus, les fichiers supprimés sont-ils exclus, les chemins contenant des espaces sont-ils traités sans risque et le script se termine-t-il correctement lorsqu’aucun fichier Swift n’a changé ? Une fois ces points validés, le contrôle du style ne sera plus « un script qui échoue de temps en temps », mais une frontière fiable pour l’acceptation des changements.
Questions fréquentes
Le contrôle incrémental remplace-t-il l’analyse complète du dépôt ?
Non. Il accélère les demandes de fusion, tandis qu’une analyse complète planifiée détecte les violations présentes dans les anciens fichiers non modifiés.
SwiftFormat et SwiftLint doivent-ils appliquer les mêmes règles ?
Non. SwiftFormat gère le formatage déterministe et SwiftLint les motifs risqués ainsi que les règles d’équipe. Chaque règle commune doit avoir un seul responsable.
Utilisez un Mac mini dans le cloud pour votre prochain développement ou build
Choisissez parmi deux configurations M4, quatre durées de location et cinq nœuds disponibles à la vente. La disponibilité réelle est indiquée en temps réel dans la console.