Livrer une app iOS tous les jours, dès le premier jour !

Nicolas VERINAUD
Ryfacto
15 min readJan 23, 2018

--

Chez Ryfacto, nous livrons tous les jours. Pourquoi ? J’explique tout ici. 🙂

Livrer une app iOS, c’est quand même long à la main : compiler, tester, signer, archiver, uploader, publier…

C’est pour cela qu’on automatise tout ça !

Dans la suite de cet article, je vais vous présenter comment j’automatise la livraison d’une app iOS juste avec un git push!

☝️ Attention, j’écris cet article en janvier 2018, les technologies et techniques présentées sont peut-être dépassées au moment où vous lisez ces lignes.

Mise à jour Février 2021 : Apple force la double authentification (2FA), cet article n’en tient pas compte. Je vous invite à consulter l’article suivant (en anglais) qui contient les ajustements nécessaires à apporter.

Rappelons l’objectif : je fais un push sur master avec git, automatiquement l’application est compilée, testée (tests unitaires + UI), signée, archivée, publiée, et les partenaires reçoivent un mail ou une notification de mise à jour.

De plus, je recherche une solution gratuite (au moins au début) et ne nécessitant aucune infrastructure de mon côté (je n’ai pas de machines dédiées à ça).

J’ai donc étudié plusieurs solutions, je ne vais pas trop m’attarder sur celles que je n’ai pas retenues, les voici quand même rapidement :

  • Gitlab Runner : nécessite une machine dédiée de mon côté (gitlab.com ne fournit que des runners linux, pas macOS 😞).
  • CircleCI : pas de version macOS gratuite (même pas de free trial !).
  • Buddybuild : entreprise rachetée par Apple, ils n’acceptent pas les nouvelles inscriptions et le service sera arrêté en mars 2018.
  • TravisCI : trop cher par rapport à CircleCI donc je n’avais pas envie de tester (un jour peut-être ?).
  • Codeship : pas de support pour le développement mobile.
  • App Center : très prometteur mais malheureusement pas d’automatisation de publication TestFlight, uniquement la publication Ad Hoc est possible en automatique et les testeurs doivent s’enregistrer chez Microsoft ce qui n’est pas idéal selon moi niveau UX.
  • Xcode Server : nécessite une machine macOS dédiée donc bye bye.

fastlane + Bitrise + Bitbucket

Finalement, la solution que j’ai retenue est Bitrise combiné à fastlane.

Pour mettre en place tout le tintouin, voici les quelques l̶o̶n̶g̶u̶e̶s̶ ̶e̶t̶ ̶f̶a̶s̶t̶i̶d̶i̶e̶u̶s̶e̶s̶ étapes à suivre (prévoyez une demi-journée environ, du café, et du jazz en fond pour vous détendre face aux erreurs !).

Créer un compte dédié à la CI

(Petite note : CI = Continuous Integration = Intégration Continue in french)

Afin de gérer les différents droits d’accès et pour plus de sécurité, la première étape consiste à créer un compte dédié à la CI sur les différentes plateformes que l’on va utiliser.

Ces plateformes étant :

  • Bitbucket
  • iTunes Connect (via un Apple ID dédié)

Je commence par créer une adresse mail dédiée tuto-ci@ryfacto.fr. À vous de choisir où créer cette adresse e-mail, comme je dispose d’un nom de domaine j’en profite !

Je crée un nouveau compte sur Bitbucket avec cette adresse mail.

Je crée aussi un nouvel Apple ID.

Je vous conseille de générer un mot de passe aléatoire que vous sauvegarderez dans le trousseau d’accès de macOS ou dans un gestionnaire de mot de passe. Pareil pour les questions de sécurité, je génère des mots de passe comme réponses à ces questions.

Créer les dépôts git

⚠️ Attention, il faut utiliser le “vrai” compte Bitbucket (pas celui dédié à la CI). ⚠️

Je vais créer deux dépôts git.

Deux ?! Oui, nous allons utiliser fastlane match pour gérer les certificats et les provisioning profiles. Il faut donc deux dépôts git : un dédié à l’app et l’autre aux certificats / profiles.

Je crée donc :

  • un dépôt privé tuto-ci-app-ios,
  • et un autre dépôt privé tuto-ci-app-ios-codesigning.

Comme Bitrise utilisera le compte CI que nous venons de créer pour accéder aux dépôts git, nous devons donner les bons droits d’accès à ce fameux compte.

Je donne donc à tuto-ci@ryfacto.fr les droits d’accès suivants :

  • écriture sur le dépôt tuto-ci-app-ios (le serveur de CI doit pouvoir commit / push / tag),
  • lecture seule sur le dépôt tuto-ci-app-ios-codesigning (le serveur de CI n’a pas le droit de gérer les certificats / profiles).

Créer l’app sur iTunes Connect

Nous allons utiliser TestFlight pour distribuer notre app. Il faut donc que l’app existe sur iTunes Connect.

⚠️ Attention, il faut utiliser le “vrai” compte Apple (pas celui dédié à la CI). ⚠️

Pour ce faire je crée un App ID sur le portail développeur Apple avec comme bundle identifier fr.ryfacto.internal.TutoCI.

Puis je crée l’app sur iTunes Connect, je l’appelle “TutoCI”. Je vais dans l’onglet “TestFlight” puis “Test Information” et renseigne les informations obligatoires.

J’ajoute ensuite le compte CI sur iTunes Connect en tant que “Developer” en limitant son accès à l’app “TutoCI”. J’active le compte grâce au mail que j’ai reçu (sur tuto-ci@ryfacto.fr).

Créer le projet Xcode

Je clone le dépôt tuto-ci-app-ios.

J’ajoute le gitignore qui va bien.

Je crée le projet Xcode (Single-View app) avec tests unitaires et tests UI. J’appelle l’app TutoCI et je mets le bon bundle identifier (fr.ryfacto.internal.TutoCI).

Voilà l’arborescence pour l’instant :

Je vérifie que ça compile bien dans le simulateur, que les tests run bien et je commit.

Et c’est le moment où je prends un petit café ☕️ bien mérité car la suite nécessitera une attention maximale of the dead.

Installer et Configurer fastlane

(Optionnel : je configure Cocoapods ou Carthage afin de gérer mes dépendances, pour la suite de ce tuto j’utiliserai Cocoapods).

Je vérifie que le Scheme “TutoCI” est bien partagé (Shared est bien coché quand je vais dans “Manage Scheme” dans Xcode).

À l’aide du terminal je me déplace dans le dossier contenant mon .xcodeproj.

J’installe fastlane :

  • je lance la commande xcode-select --install pour vérifier que les command line tools de Xcode sont bien présent (les installer le cas échéant),
  • puis la commande brew cask install fastlane (oui j’utilise hombrew, vous aussi n’est-ce pas ? 😉). Vous pouvez aussi installer via gem (Ruby) avec la commande suivante : [sudo] gem install fastlane -NV.

Et enfin j’initialise fastlane grâce à fastlane init !

Je choisis “Automate beta distribution to TestFlight” puis le scheme “TutoCI”.

Comme Apple ID Username je rentre le compte de ma CI à savoir tuto-ci@ryfacto.fr. Et je colle le bon mot de passe. (fastlane va utiliser ce compte pour les déploiements TestFlight, tout comme notre CI !)

Et là, si comme moi vous avez un compte “Individual” fastlane râle en disant que ce compte n’est pas un compte de développeur. On s’en fiche et on fallback sur un “manual Fastfile”.

Je modifie le gitignore en ajoutant des **/ devant les lignes concernant fastlane (ex : fastlane/report.xml devient **/fastlane/report.xml).

Pour l’instant mon fichier Fastfile ne contient rien d’intéressant, donc modifions-le un peu !

Mais avant ça un petit commit car nous sommes dans un état stable. 😁

Automatiser l’incrémentation du numéro de version

Primo, je suis les instructions d’Apple pour activer l’outil d’incrémentation automatique des versions. (Je m’arrête avant la partie “Commande Line”, fastlane va faire le boulot pour moi donc pas besoin de tester agvtool.)

Enfin je modifie le Fastfile pour ajouter l’incrémentation de version.

Voici le contenu de mon fichier :

default_platform(:ios)platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
build_number = increment_build_number(xcodeproj: "TutoCI.xcodeproj")
end
end

Un petit commit avant de tester notre nouvelle lane !

Je lance la commande fastlane beta.

Normalement votre projet est passé en version 2 (si vous avez démarré de zéro comme moi).

Je nettoie à l’aide de git checkout -- . && git clean -f car ça fonctionne (et que je ne veux pas incrémenter le numéro de version pour l’instant). 👍

Aller, on passe à la suite !

fastlane match : partager certificats et provisioning profiles en toute sécurité

Je reste dans le dossier contenant mon .xcodeproj et je lance la commande fastlane match init.

L’outil me demande l’URL du dépôt qui va contenir les certificats et provisioning profiles, ça tombe bien je l’ai créé plus haut, il s’agit de tuto-ci-app-ios-codesigning. Je colle l’URL ssh du dépôt (dans mon cas git@bitbucket.org/ryfacto/tuto-ci-app-ios-codesigning.git).

Un fichier fastlane/Matchfile est créé. Je modifie son contenu pour permettre à fastlane de créer les certificats et provisioning pour moi, j’utilise donc mon vrai compte de développeur (car j’ai un compte “Individual”, si vous avez un compte “Company” ou “Enterprise” et que votre compte de CI en fait partie, vous pouvez utiliser le compte de CI à la place) :

git_url "git@bitbucket.org:ryfacto/tuto-ci-app-ios-codesigning.git"type "appstore"app_identifier ["fr.ryfacto.internal.TutoCI"]
username "nicolas.verinaud@ryfacto.fr"

Je vais ensuite créer les certificats et provisioning profiles pour le développement et la publication Testflight.

J’ajoute mon device sur le portail développeur.

Pour le dev je lance la commande fastlane match development.

Match me demande une passphrase qui servira à chiffrer les données présentes sur le dépôt git. Je génère donc un nouveau mot de passe, je le note bien quelque part et je le colle à Match (et bim). 💪

Et voilà Match a créé le bon certificat de développement ainsi que le bon provisioning profile.

Je fais de même pour l’App Store en tapant fastlane match appstore.

And voilà !

Je devrais normalement pouvoir compiler mon app sur mon device (match ajoute automatiquement les devices enregistrés).

J’ouvre Xcode et je désactive le signing automatique. Je sélectionne les bons profiles en fonction de Debug et Release :

  • pour Debug je sélectionne match Development fr.ryfacto.internal.TutoCI,
  • pour Release je sélectionne match AppStore fr.ryfacto.internal.TutoCI.

Je branche mon iPhone et je teste un lancement. It works ! 🎉

Automatiser la récupération du certificat et des provisioning profiles (et les tests !)

Je modifie le fichier Fastfile afin d’automatiser match ainsi que le lancement des tests.

Voici son contenu maintenant :

default_platform(:ios)platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
match(app_identifier: "fr.ryfacto.internal.TutoCI", type: "appstore", readonly: true, verbose: true)
run_tests(scheme: "TutoCI")

build_number = increment_build_number(xcodeproj: "TutoCI.xcodeproj")
end
end

Un petit commit avant de tester (comme ça je peux clean facilement si ça se ramasse). 😎

Et c’est parti pour fastlane beta ! It works (again) !

Un petit clean et on passe à la suite.

À partir de maintenant, tous les développeurs ayant connaissance de la passphrase sont capables de récupérer le projet et de le builder sur les devices en clonant le projet et en utilisant la commande fastlane match --readonly.

Première publication TestFlight (enfin!)

Il est maintenant l’heure de publier sur TestFlight (youhou) !

On ne peut pas automatiser la première publication, donc il faut la faire à la main.

On rajoute une icône à l’app (sinon Apple n’est pas content). Si vous n’en avez pas sous la main vous pouvez télécharger celle-ci (c’est cadeau). 😇

Un petit commit (ça ne fait pas de mal).

Je lance l’archivage (Product > Archive).

Puis j’upload sur l’App Store (Upload to App Store…). Je laisse tout coché (Strip swift, bitcode, etc). Le bon certificat est sélectionné par défaut, je sélectionne juste le bon provisioning profile (il n’y en a qu’un normalement mais si jamais il s’agit de match AppStore fr.ryfacto.internal.TutoCI).

Je clique sur Upload et comme ma connexion internet ne casse pas des briques je vais prendre un autre café (en vrai je vais manger là car il est midi, bon app!). 😋

Yay 😎

Une fois l’app uploadée une première fois sur iTunes Connect je la distribue aux testeurs TestFlight.

Je vais donc dans iTunes Connect > My Apps > TutoCI > Onglet “TestFlight” et d’ici je corrige le warning “⚠️ Missing Compliance” en cliquant sur le numéro de build puis sur “Provide Export Compliance Information” à droite.

J’indique que mon app ne va pas cacher d’information à la NSA en utilisant une méchante cryptographie 😈 (si c’est votre cas vous devez mettre “Yes” et vous soumettre à la législation américaine, je ne l’ai jamais fait pour l’instant donc je ne peux vous en dire plus).

Je clique sur “Start Internal Testing” puis je m’ajoute en testeur en cliquant sur “Add iTunes Connect Users”.

Vous pouvez aussi à ce moment-là ajouter des testeurs externes si besoin, mais Apple va empêcher l’installation via TestFlight tant que ses lutins n’auront pas jeté un œil à votre app. Imaginez si des petits malins commencent à spammer la terre entière avec des versions beta contenant des virus…

Je teste sur mon iPhone si j’arrive à installer l’app via TestFlight, et la réponse est…oui ! Tout fonctionne jusque-là, on est trop fort ! 💪

Maintenant qu’une première version a été livrée, on passe à l’automatisation des livraisons avec fastlane !

Automatiser la livraison avec fastlane

Pour résumer, voici où nous en sommes lorsqu’on lance fastlane beta :

  • ✅ compiler le projet
  • ✅ lancer les tests unitaires
  • ✅ lancer les tests UI et/ou les tests d’intégration
  • ✅ si tous les tests passent, incrémenter le numéro de version
  • ❌ livrer l’app via TestFlight
  • ❌ faire un commit / push / tag dans git
  • ❌ envoyer un mail / un message sur slack / un pigeon voyageur (rayez la mention inutile)
  • ❌ automatiser le tout lors d’un git push origin master grâce à Bitrise

La première chose à faire maintenant est de modifier le fichier Info.plist afin d’automatiquement éviter le “⚠️ Missing Compliance”.

Pour cela je rajoute la clé ITSAppUsesNonExemptEncryption avec la valeur NO (si vous chiffrer, vous devez mettre YES) dans le fichier Info.plist.

Un petit commit et je modifie le Fastfile pour la livraison via TestFlight. J’en profite aussi pour ajouter l’automatisation git.

Voilà mon Fastfile à présent :

default_platform(:ios)platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
ensure_git_status_clean
match(app_identifier: "fr.ryfacto.internal.TutoCI type: "appstore", readonly: true, verbose: true)
run_tests(scheme: "TutoCI")
build_number = increment_build_number(xcodeproj: "TutoCI.xcodeproj")
build_app(workspace: "TutoCI.xcworkspace", scheme: "TutoCI")
upload_to_testflight
commit_version_bump(xcodeproj: "TutoCI.xcodeproj", message: "Bump version #{build_number} [ci skip]")
add_git_tag
push_to_git_remote

end
end

Ça veut dire quoi [ci skip] ?

Je l’ai déjà mis ici en prévention lorsque je mettrais en place la CI sur Bitrise. Je dis à Bitrise qu’il ne doit pas relancer un build lorsqu’il recevra ce commit qui est issu du build automatisé.

Si on l’oublie on est dans le 💩 car on aura créé une boucle infinie de build, chaque build créant un commit sur master demandant ainsi un nouveau build, créant un commit sur master demandant ainsi un nouveau build, etc. 😱

Je commit et je lance la commande fastlane beta pour tester.

Et là le terminal va cracher du log à n’en plus finir mais tout devrait bien se passer.

Lorsque vous voyez cette ligne : File: [blabla].ipa 1864400/92957354, 2% completed c’est que c’est le moment pour vous d’aller vous chercher un autre café bien mérité car cela veut dire que l’upload est en cours. (Rassurez-vous, sur les serveurs de Bitrise, cet étape va beauuuucoup plus vite. 😉)

Et paf ! Je reçois une notification sur mon iPhone de TestFlight qui me dit qu’une nouvelle version de TutoCI est dispo ! 🎉

Passons maintenant à l’ultime étape !

Laisser Bitrise faire le job (comme ça nous on peut continuer à dev tranquilou) !

Je crée un compte sur Bitrise en utilisant mon “vrai” compte, pas celui de la CI.

Puis je crée une nouvelle app.

Je sélectionne Bitbucket, car c’est là que sont mes sources. Je me connecte si nécessaire (toujours en utilisant mon “vrai” compte).

Je sélectionne ensuite le bon projet à builder, à savoir tuto-ci-app-ios.

À la question “Do you need to use an additional private repository?”, je réponds “I need to” (eh oui on a un dépôt avec le certificat et les provisioning profiles vous vous souvenez ?).

Je copie la clé publique SSH de Bitrise et je l’ajoute à mon compte de CI sur Bitbucket : https://bitbucket.org/account/user/[votre-username-ci]/ssh-keys/.

Done ! Je clique sur “I’ve added the SSH key”.

Je dis à Bitrise qu’il peut scanner la branche master pour trouver le projet.

Ils sont sympa, ils nous proposent aussi de prendre une petite pause café. 😂

Il a trouvé le projet iOS et a même détecté fastlane !

Puis il faut choisir entre “iOS” et “fastlane”.

Vous l’aurez deviné, je choisis fastlane évidemment !!

Il a trouvé le bon sous-dossier TutoCI ainsi que la bonne lane ios beta.

Bitrise construit un environnement macOS vierge en préparant la stack de développement from scratch (Xcode, cli, etc).

La stack proposée de base me semble mauvaise, je vérifie dans mon Xcode (“About Xcode”), ma version est la 9.2 alors que la version suggérée par Bitrise est 9.1.x. Ni une ni deux j’édite ça et je mets 9.2.x.

Je laisse Bitrise ajouter un webhook pour moi à chaque fois que je push dans la branche master de Bitbucket.

Alors là, normalement il a lancé un build automatiquement, mais ça ne marchera pas !

Whaaaat ? On a fait tout ça pour rien ?

Pas de panique, on remet un peu de jazz et on se relaxe. On y est presque. Promis ! 🤗

Nous n’avons pas configuré Bitrise pour qu’il utilise le bon compte Apple (tuto-ci@ryfacto.fr) ni lui avons dit quelle passphrase utiliser pour fastlane match !

On va sur le build en cours et on clique sur “Abort” car sinon le pauvre va tourner pour rien.

Il va falloir customiser un peu le workflow et ajouter quelques variables d’environnements.

Je clique en haut sur le nom de mon app puis sur “Workflow”.

Je supprime les deux étapes suivantes : “Do anything with script step” et “Deploy to bitrise.io”.

La première étape affiche simplement un message dans les logs (Bitrise l’ajoute automatiquement pour vous montrer que “Oh c’est cool on peut faire du shell !”).

La deuxième sert à héberger le résultat du build (le .ipa et les symboles de debug) sur Bitrise en vue d’une publication ad hoc. On s’en contrefiche car on publie via TestFlight et c’est notre script fastlane qui s’en occupe !

Je sélectionne l’onglet “Secrets” et je vais ajouter 3 variables :

  • FASTLANE_USER avec comme valeur mon compte CI tuto-ci@ryfacto.fr
  • FASTLANE_PASSWORD avec comme valeur le mot de passe de mon compte de CI (celui créé pour l’Apple ID)
  • MATCH_PASSWORD avec comme valeur la passphrase utilisée par fastlane match (vous l’avez noté n’est-ce pas ?)

Je veille à bien décocher la case “Replace variables in inputs?”.

Et je fais un petit ⌘ + S pour sauvegarder la config.

Je retourne sur le projet dans Bitrise et je clique sur “Start/Schedule a Build”, je laisse tout par défaut et c’est partiiiii ! 🚀

Si on a tout bien fait, ça devrait marcher. *Croise les doigts*

Et voilààààà ! 🎉🎉🎉

Au secours ! Ça ne marche pas ! Qu’est-ce que je dois faire ?

Souffler un grand coup, écouter une musique relaxante et bien lire le message d’erreur. Bien lire le message d’erreur. Bien lire le…ok ok je crois que vous avez compris ! Le mieux c’est aussi de demander à son collègue dev le plus proche de jeter un œil au message d’erreur, car quand on a la tête dans le guidon depuis quelques heures c’est compliqué parfois ! 😅

Job Done (enfin !)

Voyons ce que nous avons accompli.

Je fais un git push origin master et voilà ce qu’il se passe, Bitrise :

  • ✅ compile le projet
  • ✅ lance les tests unitaires
  • ✅ lance les tests UI et/ou les tests d’intégration
  • ✅ si tous les tests passent, incrémente le numéro de version
  • ✅ livre l’app via TestFlight
  • ✅ fait un commit / push / tag dans git
  • ✅ envoi un mail /̶ ̶u̶n̶ ̶m̶e̶s̶s̶a̶g̶e̶ ̶s̶u̶r̶ ̶s̶l̶a̶c̶k̶ ̶/̶ ̶u̶n̶ ̶p̶i̶g̶e̶o̶n̶ ̶v̶o̶y̶a̶g̶e̶u̶r̶ (rayez la mention inutile) quand le build échoue ou que le build change d’état.
  • ✅ Le tout automatisé lors d’un git push origin master grâce à Bitrise !

À nous les joies de l’intégration et du déploiement continue !

N’hésitez pas à poser vos questions ci-dessous. Si vous avez des suggestions ou des idées pour améliorer cette procédure (qui est quand même fastidieuse avouons-le) je suis preneur !

Ah…et bravo si vous êtes arrivé jusqu’ici avec succès ! Vous avez mérité un dernier café (ou thé si vous préférez, ou verre d’eau, soda, bref, ce qui vous fait plaisir !). 👍

--

--

Nicolas VERINAUD
Ryfacto
Editor for

iOS Crafter depuis 2010, maniaque de la livraison continue. Zelda fan, papa d’un p’ti gars, cuisto à mes heures perdues (batch cooking le samedi matin !).