Cet article présente un modèle de rupture iOS CI/CD à l'échelle (Scaling Failure Model) — pas un calculateur de capacité. Avant notre intervention, l'équipe est passée de 8 à 20 personnes en quatorze mois sans que la topologie des runners bouge : semaines de release avec PR tout rouge, builds bloqués longtemps en Queued, TestFlight avec deux jours de retard. Nous expliquons pourquoi la CI ralentit quand l'effectif augmente, et où se situe le goulot dans la pipeline.
Pour « combien de Mac pour 500 builds/jour », voir l'article jumeau 3 Cloud Mac pour 500 builds iOS par jour — c'est la mise en œuvre après reprise. Ici : pourquoi ça a cassé avant.
Vue d'ensemble : comment la CI glisse de « plus de monde » vers la Failure Zone
La figure 1 est le schéma de mécanisme global — plus adapté à l'ouverture d'un postmortem ou à l'onboarding qu'au détail pipeline. La rupture est rarement un point unique, mais la superposition de la chaîne ci-dessous.
Fig. 1 · Mécanisme de rupture CI (Scaling Failure Cascade)
Chez ce client : blocage Queue à 12 personnes, bifurcation Runner/Archive allumée à 16, zone rouge en bas à 20. Comparaison avec la healthy baseline ci-dessous.
Architecture iOS CI/CD : carte des goulots de la PR à TestFlight
La figure 2 montre le chemin d'exécution d'un job macOS (L0/L1/L2) et les deux premiers points de saturation. Utile pour distinguer : compilation lente, ou job qui ne tourne pas du tout.
Fig. 2 · Pipeline iOS CI/CD : couches L0 / L1 / L2 et goulots typiques (client 2024–2025)
L0 : lint / checks légers ; L1 : build d'intégration PR et tests Simulator ; L2 : Archive, notarisation, upload. Rupture en Queue et pool mixte, pas en compilation L1 — l'inflation matrix est souvent lue à tort comme « Xcode ralentit ».
Point de départ : pourquoi la CI « suffisante » à 8 personnes n'a pas suivi
Mi-2024, configuration classique :
- 1 Mac mini M2 au bureau, aussi Self-hosted Mac Runner (voir doc GitHub runners self-hosted)
- Autres jobs via runners macOS hébergés GitHub
- Monorepo avec 2 apps, un seul YAML workflow pour tout
- Environ 80–120 jobs macOS/jour, P95 queue sous 5 minutes
Le CTO avait dit : « La CI n'est pas un goulot, ne perdez pas de temps dessus. » — Vrai à 8 personnes ; après doublement de l'effectif productif, la CI devint une taxe invisible : aucune planification de capacité, seulement des feature teams qui empilaient des workflows.
Healthy baseline : que considérer comme « normal » ?
En ne regardant que la semaine de rupture à 20 personnes, on conclut vite : « grande équipe = beaucoup de Mac ». Nous ajoutons un comparatif baseline saine vs semaine de rupture — chiffres mesurés à 8 personnes chez ce client et fourchettes d'équipes iOS mid-size que nous avons accompagnées (pas une norme sectorielle, suffisant pour un review interne).
| Indicateur | Healthy baseline (8–12 pers. · topologie saine) | Client · semaine rupture 20 pers. |
|---|---|---|
| L1 queue_wait P95 | < 8 min | 47 min |
| L1 run duration P95 | 12–18 min | 14 min (compilation inchangée) |
| Part jobs Archive | < 15% | 38% |
| Jobs macOS par push · P99 | < 6 | 19 |
| Jobs macOS / jour | 80–180 | 500+ (semaine release) |
| Topologie runners | L1/L2 séparés par labels | hosted + mini bureau mélangés |
Point clé : run duration quasi identique en semaine de rupture, queue time ×10 — le problème est dans les files et workflows, pas la compilation Xcode. C'est là que « CI plus lente » induit le plus en erreur.
Chronologie : trois alertes, trois fois traitées comme « ponctuel »
Première (12 personnes, +4 iOS) : merges PR passent de 6 à 14/jour. La build queue sur runners hébergés affiche 25–40 min l'après-midi. Réaction : « GitHub déconne aujourd'hui. » Personne ne trace la courbe queue_wait_seconds.
Deuxième (16 personnes, +2 backend sur le même repo) : ajout d'un second Mac mini au bureau, tous deux en self-hosted, sans labels de files. Archive et PR Build partagent le pool — jobs légers passant sporadiquement de 8 à 35 min, notarisation en échec aléatoire. Réaction : « on achète un disque en plus. »
Troisième (20 personnes, grosses versions des deux apps) : semaine release, jobs macOS de 120 à 280+/jour. Canal Slack #ios-ci avec astreinte 24h/24. Réaction : « achats en cours pour 8 Mac mini » — le scénario 8 machines de notre cas 500 builds/jour ; personne n'avait ventilé les jobs.
Fig. 3 · Taille équipe vs jobs macOS/jour vs niveau de risque (client 2024–2025, mesuré)
| Effectif | Jobs macOS / jour | GitHub Actions queue time (L1 P95) | Niveau risque | Note de phase |
|---|---|---|---|---|
| 8 pers. | ~100 | < 5 min | Baseline | Un mini bureau suffisait |
| 12 pers. | ~180 | 25–40 min | Warning Zone | Queue dépasse le seuil en continu |
| 16 pers. | ~260 | ~35 min | Structural Debt | Archive mélangé, signature en échec sporadique |
| 20 pers. | 500+ | 47 min | Failure Zone | Pic semaine release, CI effondrée |
Source : logs GitHub Actions (fenêtre glissante 14 jours) ; à 20 personnes, pic semaine release des deux apps. Volume jobs > effectif — matrix et Archive PR, pas seulement « plus de monde ».
Deux détails : entre 16 et 20, volume jobs en forte pente, part Archive ~18 % → 38 % ; queue time et volume non linéaires — effectif pas doublé, attente oui. Là où les budgets scaling se trompent.
Modèle de seuils : 12 Warning / 16 Structural Debt / 20 Failure Zone
Nous avons condensé le cas en cadre utilisable en weekly review — pas une formule exacte, mais suffisant pour décider de toucher à la topologie runners :
(fréquence PR × largeur matrix × part Archive) ÷ capacité effective pool runners
Les trois termes du numérateur viennent des logs GitHub Actions ; le dénominateur « pool runners effectif » n'est pas le nombre de machines enregistrées, mais slots Mac concurrents routables par labels — mini bureau bloqué par un dev, dénominateur divisé par deux. Si deux conditions ci-dessous sont vraies, arrêter « un Mac mini de plus » et refactoriser la pipeline :
- queue_wait_seconds P95 > 30 min et jobs Archive > 30 % → séparer L1/L2, stop Archive complet sur PR
- jobs macOS par push · P99 > 15 → matrix/path filter hors contrôle, réduire jobs avant hardware
- L2 queue P95 > 40 min pendant 3 jours et cache hit > 60 % → scaling structurel, pool release dédié
Semaine rupture à 20 : les trois conditions remplies — Failure Zone, pas Warning corrigeable avec des packs de minutes.
GitHub Actions queue time : premier indicateur hors contrôle après 12 personnes
Beaucoup lisent « CI plus lente » comme compilation lente et optimisent DerivedData. Chez ce client, c'est queue time qui a dérapé — jobs longtemps en Queued. GitHub sépare queue_wait_seconds et run duration ; ne regarder que ce dernier, c'est optimiser la compilation alors que le goulot est concurrence org et pools runners.
Concurrence macOS org : GitHub Actions usage limits. Après 12 personnes, tempêtes PR l'après-midi : runners pas lents, jobs en attente de slot — Queued à l'écran, souvent imputé au réseau ou Xcode. Au debug, temps d'exécution et d'attente : séparer wait et run.
À 12 personnes, queue P95 déjà 25–40 min. Labels fast/archive (au lieu de deux mini mélangés) auraient retardé la contention signature. Topologie : matrice pool élastique vs nœuds permanents.
Cinq goulots de scaling (Scaling Taxonomy)
Cinq catégories — labels internes mobile CI, alignés fig. 2, pratiques pour postmortem.
① Capacity bottleneck · Concurrence macOS org saturée
Concurrence hosted pleine : jobs légers et Archive en file commune — files rapides bloquées par jobs lents, comme la courbe GitHub Actions queue time ci-dessus. Mieux que packs de minutes : sortir L2 du pool hosted ; sinon concurrence org reste le premier plafond.
② Resource contention · Mac mini bureau comme « Mac de dev de secours »
Runner sur machine SSH-able par les devs — un jour xcodebuild debug en local. Runner et dev quotidien se disputent CPU/disque, timeouts ou compile errors sporadiques. À 16 : deux mini « vendredi mort, lundi ok » — Remote Desktop et changement de branche.
③ Workflow design debt · Archive complet sur chaque PR
À 8 personnes, raccourci « PR vert = prêt release » : Archive + upload en fin de chaque pull request workflow. 6 merges/jour encore tenable ; à 20, part Archive de 10 % à 35 %+ — jobs lents inondent une queue déjà pleine. Bonne pratique : séparer build intégration L1 et Archive L2 — fait après reprise, voir stratification jobs dans l'article jumeau.
④ Scaling explosion · Jobs matrix monorepo en croissance exponentielle
À 20 personnes, un push déclenche jusqu'à 22 jobs macOS — 2,5× plus de monde, 4× plus de jobs. Point de rupture le plus souvent ignoré dans le rythme produit.
⑤ Crypto / signing contention · Keychain et signature le jour release
Deux Mac self-hosted en Archive parallèle — 16 Go RAM, encore ok sporadiquement ; plus déverrouillage Match, notarisation, upload TestFlight, verrous Keychain et codesign deviennent « même code erreur au hasard ». Semaine release : quatre fois « certificat introuvable », OK en local — jobs parallèles sur même environnement signature, pas certificat expiré. Doc : Apple Xcode Signing and Capabilities.
Leurs « correctifs » — et pourquoi ça a empiré
Plus de minutes GitHub : soulage la queue hosted, part Archive inchangée — argent dépensé, P95 > 60 min.
Troisième Mac mini bureau : toujours sans labels fast/archive, trois machines mélangées — taux d'échec signature en hausse.
Interdire merges le vendredi : fréquence PR artificiellement basse, pic release encore plus haut.
timeout-minutes: 180 par job : temps Queued hors timeout — plus de jobs occupent longtemps les slots.
Seule tentative partiellement valide inachevée : Release sur machine séparée — toujours mini bureau, nuit sans déverrouillage Keychain, queue L2 lundi matin.
Trois chiffres à monitorer — absents avant la rupture
Avant rupture, Grafana n'avait que taux de succès et durée moyenne. Semaine 1 après reprise, trois métriques ont localisé 80 % du problème (découpage queue_wait_seconds : doc monitoring GitHub) :
- queue_wait_seconds P95 (par L0/L1/L2) — « compilation lente » vs « en file »
- Part jobs Archive (hebdo) — workflow design hors contrôle ?
- Jobs macOS par push · P99 — explosion matrix ?
Semaine rupture 20 pers. : L1 queue_wait P95 47 min, Archive 38 %, jobs/push P99 19. Deux seuils dépassés → topologie, pas achats en premier.
Ordre d'urgence : topologie d'abord, nombre de Mac ensuite
Deux premières semaines après reprise : zéro machine neuve, quatre actions :
- Retirer Archive du workflow PR, L2 uniquement nightly + release branches
- Path filter resserré — README/backend ne déclenchent plus matrix iOS
- Mini bureau retiré des runners, plus de conflit avec le dev
- Runners hébergés pour pics L1 seulement, L2 sur nœuds dédiés
Rien que ça : queue_wait P95 semaine release de 47 à 22 min — insuffisant encore, mais preuve que cause principale topologie et workflow, pas « formule mystérieuse de Mac ». Ensuite pools fast/release Cloud Mac et validation capacité ; pool élastique vs fixe : GitHub Actions runners macOS self-hosted : matrice de décision.
Trois signaux pour équipes en croissance
De 12 vers 20 personnes, si l'un de ces signes apparaît, prévoir 30 min CI cette semaine — pas attendre la collision release :
- Les devs demandent « on merge sans CI verte ? »
- Post-it sur le Mac bureau : « ne pas redémarrer, runner actif »
- Build TestFlight OK mais « attente fenêtre upload » devient la norme
Ces signaux cachent souvent au moins deux des cinq points de rupture ci-dessus.
Postmortem Summary (citable)
Root cause
Topologie CI non scalée avec la croissance : fréquence PR et largeur matrix en hausse, runners restés « hosted + mini bureau mélangés », workflows PR avec L2 Archive — queue et signing double saturation.
Contributing factors
- Pas de monitoring
queue_wait_seconds, Queued pris pour compilation lente - Second Mac mini sans labels fast/archive, Archive et PR même pool
- Path filter monorepo trop large, README déclenche matrix iOS
- Pics jobs semaine release non liés aux achats/changements topologie
Fixes tried (inefficaces ou aggravants)
- Plus de minutes GitHub — queue hosted seulement, part Archive identique
- Troisième mini bureau mélangé — échecs signature plus fréquents
- Interdiction merge vendredi — pic déplacé semaine release
timeout-minutes: 180— Queued hors timeout
What worked (deux premières semaines après reprise)
- Archive retiré des PR ; L2 nightly + release branches seulement
- Path filter resserré ; mini bureau sans runner
- L2 sur nœuds release dédiés — queue P95 47min → 22min
FAQ
GitHub Actions reste en Queued — pas compilation lente ?
Vérifier queue_wait_seconds et run duration. Forte part Queued → goulot file ou pool runners, pas compilation. Nœud Queue fig. 2 et healthy baseline fig. 3.
À combien de personnes la CI iOS casse le plus ?
Ce cas : 12 Warning / 16 Structural Debt / 20 Failure Zone. Plus fiable : queue P95 > 30 min et Archive > 30 % → refactoriser pipeline.
En croissance : hardware ou pipeline en premier ?
Stratification jobs, stop Archive PR, matrix resserrée. Sinon hardware ne repousse que l'effondrement — pics queue reviennent à la prochaine release.
C'est le même article que « 500 builds/jour, combien de Mac » ?
Non. Ici pourquoi la rupture (Failure Model) ; jumeau capacité et nombre de machines (Sizing). Lire d'abord celui-ci + diagnostic, puis Sizing.
Prescription Failure Zone : isoler L2, puis valider le pool runners
Les trois conditions réunies → Failure Zone (comme fig. 1 en bas)
- queue_wait_seconds P95 > 30 min
- Jobs Archive > 30 %
- Jobs macOS par push > 15
Actions engineering d'abord (avant achats)
1. L2 isolation — retirer Archive des PR ; release/nightly exclusifs label macos-archive.
2. Dedicated Mac pool — L2 hors dev bureau et pool hosted mixte ; L1 reste hosted ou self-hosted élastique pour pics.
3. Validation — workflow release sur nœud isolé, queue P95 vs healthy baseline fig. 3 (< 8 min).
Nombre de machines et algorithme pools fast/release : 3 Cloud Mac pour 500 builds iOS par jour ; élastique vs fixe : matrice décision runners macOS self-hosted GitHub Actions.
Pour un PoC journalier d'un nœud release dédié validant l'isolation L2 : hébergement Mac cloud ou accueil VPSSpark — file Archive isolée sur Cloud Mac. Valider la topologie, pas acheter tout le cluster d'emblée.