Chapitre 2 : Environnement, intégration et déploiement

L’environnement de développement, l’intégration continue et le déploiement

Dans l’article précédent, nous avons configuré les outils pour la compilation et le packaging de la librairie. Nous allons maintenant voir comment configurer l’environnement de développement, l’intégration continue et le déploiement.

En effet, une des priorités de cette librairie est de proposer une expérience de développement agréable : un développeur doit pouvoir modifier la librairie et la tester sans délai dans son application mobile..

Enfin, nous verrons comment configurer l’intégration continue pour s’assurer de la qualité des développements, éviter les régressions et automatiser les publications.

Nos besoins

Nous pouvons résumer nos besoins comme suit :

  • Simplicité d’utilisation et bonne expérience de développement (cf. ci-dessus)
  • Intégration continue avec Bitrise, notre plateforme de CI mobile
  • Déploiement de la librairie sur notre registry npm interne

Les problématiques

Pour atteindre ces objectifs, plusieurs points restent à éclaircir :

  • Comment permettre au développeur de développer sans différence notable au sein de la librairie et de son application ?
  • Comment configurer notre plateforme d’intégration continue pour tester la librairie en isolation ?
  • S’assurer que la publication de la librairie sur notre registry npm privé fonctionne depuis Bitrise.

L’environnement de développement

La qualité de l’expérience de développement est un point très important à Lyra : il ne faut donc pas que la mise en place de cette librairie ajoute trop de complexité à l’environnement de développement, ou pénalise la productivité des développeurs.

Du coup, le développement de l’application et de la librairie doit continuer de bénéficier des comportement classiques : refresh de composant à chaud, debugger, inspecteur d’éléments etc.

De même, depuis l’application, on doit pouvoir basculer facilement entre une version publiée de la librairie (version officielle) et une version locale (en cours de modification par le développeur), afin de ne pas être obliger de publier une version juste pour la tester dans l’application finale. Il faut donc pouvoir dire au gestionnaire de paquet d’utiliser la version locale de la librairie, et non pas la version publiée disponible sur un registry.

Linking local de notre librairie

Généralement lorsque l’on veut “lier” une librairie en local, on utilise yarn/npm link :

cd /path/to/lib    # nom du package = @lyra/shared-components
yarn link
cd /path/to/app
yarn link @lyra/shared-components

Malheureusement, dans notre cas, c’est un échec. C’est un problème connu (issue #1) mais Metro ne supporte pas les liens symboliques (symlinks) dans les dépendances ! Il a donc fallu trouver une autre solution …

Deuxième essai : yalc

Une solution alternative est d’utiliser yalc.

Yalc est un utilitaire qui utilise un dépôt local dans lequel on peut venir pousser des modules. A chaque mise à jour d’un module, il le copie automatiquement dans les répertoires “node_modules” de tous les projets “abonnés”.

Il n’utilise donc pas de lien symbolique mais une copie directe des fichiers au bon emplacement dans les node_modules.

npm install --global yalc
cd /path/to/lib     # nom du package = @lyra/shared-components
yalc push
cd /path/to/app
yalc add @lyra/shared-components

Et ça fonctionne plutôt bien ! L’application démarre et ne se rend compte de rien. A chaque nouvelle commande ‘yalc push’, les sources sont copiées dans les node_modules de l’application cible.

Et cerise sur le gâteau, le watcher de Metro surveille aussi les node_modules, ce qui veut dire qu’à chaque yalc push, Metro déclenche un refresh des composants modifiés. C’est exactement le fonctionnement désiré.

Synchronisation automatique des mises à jour

Nous savons maintenant linker notre librairie en local. Cependant, à chaque modification du code, nous sommes obligés de lancer manuellement un build TypeScript puis la commande ‘yalc push’ pour mettre à jour les sources dans les node_modules de l’application. On peut encore améliorer ça.

Il nous faudrait une sorte de watcher qui nous permette de déclencher automatiquement, à chaque modification, un build TypeScript puis un yalc push.

Or, un utilitaire fait exactement cela : tsc-watch !

Une fois lancé, l’utilitaire déclenche automatiquement un build TypeScript dès qu’il détecte une modification dans le code source. Nouvelle cerise sur le gâteau, on peut lui passer des commandes après chaque build avec l’option –onSuccess → c’est l’endroit idéal pour placer notre yalc push.

On configure donc un script npm côté librairie pour lancer tsc-watch.

package.json


{
  "scripts": {
    ...
    "prewatch": "rimraf dist",
    "watch": "tsc-watch -p tsconfig.json --onSuccess \"yalc push\"",
  }
}

Ainsi, à chaque modification sur un fichier de la librairie, un build TypeScript est déclenché, suivi d’un yalc push. L’application recharge les composants modifiés comme si nous développions directement dans l’application !

La configuration côté application

Côté application, il suffit de deux scripts npm pour lier ou délier la librairie locale :

package.json

{
  "scripts": {
    ...
    "lib:link": "yalc add @lyra-network/shared-components && yarn install",
    "lib:unlink": "yalc remove @lyra-network/shared-components && yarn install",
  }
}

Le yarn install après chaque commande est important dans le cas où les dépendances dans la librairie sont différentes entre la version locale et la version publiée dans nos repositories.

Pour résumer, quand un développeur veut faire une évolution dans la librairie, il est opérationnel en deux commandes : yarn watch depuis la lib et yarn lib:link dans son application.

Ensuite qu’il développe dans son application ou dans la librairie, le comportement est identique ! Il peut continuer à tester ses changements directement dans son application comme avant.

Attention à ne pas publier la configuration locale !

Nous utilisons une plateforme d’intégration continue ainsi que la plateforme d’Expo pour publier nos applications. Nous reviendrons en détail sur ces sujets juste après.

Cependant, dans ce contexte, si la librairie est linkée localement au moment de la publication, ça ne peut pas fonctionner. En effet, quand l’application sera compilée sur les serveurs d’Expo, les références locales ne peuvent pas être résolues.

Il faut donc être vigilant et s’assurer qu’au moment de publier, la librairie ne soit pas configuré en mode “local”.

Pour être sûr de ne jamais se rater (une erreur est si vite arrivée…), nous avons mis en place un hook git pre-push au niveau de chaque application qui vérifie qu’il n’y a pas de package linké localement avant de pousser le code sur la branche ‘main’.

Pour nos hooks git, nous utilisons l’utilitaire husky. La commande yalc check nous permet de vérifier qu’aucun package n’est linké localement dans le projet courant. Exemple de notre fichier de hook pre-push :

.husky/pre-push

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
 
git_branch="$(git rev-parse --abbrev-ref HEAD)"
 
if [ "$git_branch" = "main" ]; then
  npx yalc check
fi

yalc check retourne une erreur si un package est linké localement et que nous sommes sur la branche ‘main’, ce qui empêche ainsi le push sur le serveur git.

C’est terminé pour la partie environnement de développement. Les outils mis en place nous permettent de garder une bonne expérience développeur. Le client Expo fonctionne comme avant et ne se rend même pas compte du changement. Le développeur peut donc travailler sur la librairie en testant les modifications directement depuis son application.


Intégration continue et publication de la librairie

Grâce à la plateforme d’intégration continue Bitrise, tout changement de code dans la librairie est testé pour éviter les régressions. La publication de la librairie est également gérée avec Bitrise. 

Non-régression

La librairie contient ses propres tests avec Jest, ainsi que sa propre configuration ESLint et Prettier.

A chaque nouveau push, quelque soit la branche, un build est lancé dans Bitrise. Notre workflow contient les étapes suivantes :

  1. yarn install → installation des dépendances
  2. yarn lint → vérification de la syntaxe avec ESLint
  3. yarn test → exécute les tests unitaires et d’intégration avec Jest
  4. yarn build → compile la librairie avec TypeScript

Si une étape échoue, nous sommes notifiés directement par messagerie instantanée.

Ces tests nous permettent de nous assurer que les branches restent stables, surtout la branche ‘main’.

Publication de la librairie

Pour la publication, nous souhaitons que la librairie reste privée mais tout de même accessible depuis l’extérieur, justement pour permettre à Bitrise et Expo d’y accéder au moment des tests et builds de nos applications.

A Lyra, nous avons déjà un registry npm privé (accessible depuis l’extérieur via une authentification forte). C’est donc sur celui-ci que nous allons publier notre librairie.

Après avoir configuré l’authentification dans Bitrise, nous avons créé un nouveau job dans Bitrise qui s’occupe de publier notre librairie. Ce job est déclenché à chaque nouveau tag git.

Ainsi, quand nous voulons publier une nouvelle version de la librairie, il suffit de pousser un tag git. Le job se déclenche alors automatiquement, il effectue tous les tests nécessaires puis publie la librairie.

Ci-dessous une impression d’écran des différents builds de notre librairie sur Bitrise :

ci-builds

Conclusion

(coche)  L’expérience développeur reste très bonne malgré l’introduction de la librairie.

(coche)  Les développements en local sont quasi transparents grâce à l’outillage de notre environnement de développement.

(coche)  Le développeur continue de tester son application, qu’il travail sur la librairie ou sur son application.

(coche)  La qualité de la librairie reste maitrisée grâce à l’intégration continue.

(coche)  La librairie reste privée mais tout de même accessible depuis l’extérieur via une authentification.