10 min de lectureCI/CD
Publier une application React Native sur les deux stores avec EAS, GitHub Actions et Maestro

Un pipeline de release mobile tiré de mon travail sur une application de marketplace : EAS depuis GitHub Actions, flows Maestro sur émulateurs, publication gérée sur les deux stores.
Une application React Native est terminée deux fois. Une première fois quand la dernière fonctionnalité est fusionnée, et une seconde quand un build signé arrive devant un vrai utilisateur sur les deux stores sans que personne ait à se souvenir de quel portable contient le keystore. C'est cette seconde fin qu'on oublie dans la planification. C'est aussi elle qui décide si une équipe livre chaque semaine ou une fois par trimestre.
Sur HIÜRD, une marketplace de petits boulots et de services de proximité publiée sur l'App Store et Google Play, cette seconde fin était ma responsabilité : le CI/CD mobile et le pipeline de release vers les stores. L'application est en React Native avec Expo, le site en Next.js, et les releases passaient par EAS, GitHub Actions et des tests de bout en bout Maestro sur émulateurs, avec la publication gérée (managed publishing) sur les deux stores. Voici comment je monterais un tel pipeline aujourd'hui, avec du code générique que vous pouvez adapter.
Ce qui est vérifié et ce qui est général
L'application HIÜRD est publique sur l'App Store et sur Google Play (éditée par HIURD LLC, package Android com.hiurdapp). Le pipeline lui-même ne l'est pas. Les exemples ci-dessous ont été écrits pour cet article et ne sont pas des extraits de ce dépôt ; les durées, le nombre de flows et le détail des incidents de conformité sont laissés de côté.
Pourquoi automatiser les releases mobiles
Les déploiements web pardonnent. Un mauvais push se règle d'un revert. Le mobile a des files d'attente d'examen, du matériel de signature qui expire, des codes de version qui ne doivent jamais redescendre, et deux consoles avec chacune leur vocabulaire. Chaque étape manuelle de cette chaîne est un endroit où une release attend une personne.
Le coût apparaît sous forme de livraisons groupées. Quand un build demande un après-midi de clics, personne ne livre un correctif d'une ligne isolément. Les correctifs s'accumulent, le diff entre deux versions en store grossit, et quand quelque chose casse, personne ne sait quel changement en est la cause. Automatiser le chemin du commit fusionné jusqu'au build sur un track de test rend les petites releases assez peu coûteuses pour qu'elles aient lieu.
Il y a une seconde raison. Les secrets de signature ne devraient pas vivre sur la machine d'un développeur. Avec EAS qui détient les certificats et GitHub qui détient un seul token, la personne qui quitte le projet n'emporte pas avec elle la capacité de publier.
Anatomie du pipeline : EAS Build et EAS Submit depuis GitHub Actions
EAS Build compile les binaires natifs sur l'infrastructure d'Expo. EAS Submit les pousse vers App Store Connect et la Play Console. GitHub Actions décide quand les deux s'exécutent et avec quel profil.
Les profils dans eas.json
Trois profils de build couvrent la plupart des applications : un development client pour le travail en local, un build preview pour la QA et les tests de bout en bout, et un build de production pour les stores. Sur Android, le profil preview produit un APK, qu'un émulateur installe directement. La production produit un AAB, ce que la Play Console attend.
{
"cli": { "version": ">= 12.0.0", "appVersionSource": "remote" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"channel": "preview",
"android": { "buildType": "apk" }
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": { "ascAppId": "1234567890" },
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "internal",
"releaseStatus": "draft"
}
}
}
}Deux réglages ici évitent de vrais ennuis. autoIncrement avec une source de version distante signifie qu'EAS incrémente le numéro de build, donc deux personnes ne peuvent jamais publier le même code de version. Et releaseStatus: "draft" signifie qu'EAS Submit envoie le bundle Android sans le publier. Le store reçoit le binaire ; un humain décide toujours du moment de la mise en ligne.
Secrets et identifiants
Le workflow a besoin d'un seul secret pour parler à EAS, un EXPO_TOKEN issu du compte Expo. Le certificat de distribution Apple, le provisioning profile et le keystore Android sont stockés par EAS et n'apparaissent jamais dans le dépôt ni dans les journaux. Une clé d'API App Store Connect est enregistrée une fois auprès d'EAS pour la soumission. La clé du compte de service Google Play peut aussi être stockée sur EAS ; l'exemple la garde plutôt en secret GitHub, écrite sur disque à l'exécution et supprimée avec le job.
Canaux et versions de runtime
Le champ channel relie un build à une branche EAS Update, si vous utilisez les mises à jour over-the-air pour des correctifs purement JavaScript. La politique runtimeVersion dans app.json sert de garde-fou : une mise à jour n'atteint que les builds dont la couche native correspond. Garder la politique sur appVersion signifie que chaque release native démarre un nouveau runtime, ce qui est la valeur par défaut la plus sûre.
Le workflow
Un workflow minimal qui construit sur un tag de version et soumet vers les tracks internes :
name: Release
on:
push:
tags: ['v*']
jobs:
build-and-submit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Write Play service account
env:
GOOGLE_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}
run: printf '%s' "$GOOGLE_SERVICE_ACCOUNT_JSON" > google-service-account.json
- name: Build and submit
run: eas build --platform all --profile production --non-interactive --auto-submit
- if: always()
run: rm -f google-service-account.json--auto-submit enchaîne EAS Submit après chaque build réussi avec le profil de soumission correspondant. Sur un projet plus gros, découpez cela en deux jobs pour qu'une soumission Android en échec ne masque pas une soumission iOS réussie, et ajoutez un déclencheur workflow_dispatch pour les lancements manuels.
Tests de bout en bout Maestro sur émulateur en CI
Les tests unitaires n'attrapent pas un écran de connexion cassé. Maestro, si, avec des flows YAML qui se lisent comme un plan de test qu'un product owner pourrait relire. Sur ce projet, ils tournaient sur des émulateurs dans le pipeline CI/CD.
Écrire des flows
Un flow déclare l'identifiant de l'application, puis une séquence d'interactions. Les sélecteurs ciblent le texte visible ou un testID, ce qui garde les flows stables quand le style change.
appId: com.example.app
tags: [smoke]
---
- launchApp:
clearState: true
- tapOn: 'Sign in'
- tapOn:
id: 'email-input'
- inputText: 'qa@example.com'
- tapOn:
id: 'password-input'
- inputText: '${QA_PASSWORD}'
- tapOn: 'Continue'
- assertVisible: 'Your jobs'${QA_PASSWORD} est injecté à l'exécution avec maestro test -e QA_PASSWORD=..., donc aucun identifiant ne traîne dans le fichier.
Choisir quels flows tournent où
Les tags font le routage. Les flows smoke (lancement, connexion, un écran central) tournent sur chaque pull request contre un APK preview. La suite complète, avec les parcours plus lents, tourne sur le tag de release. La commande est la même, seul le filtre change.
maestro test --include-tags smoke flows/
maestro test --exclude-tags slow --format junit --output maestro-report.xml flows/L'émulateur est démarré par une action communautaire qui lance une image x86_64 sans affichage. L'APK preview est récupéré depuis l'URL de build que renvoie eas build:list --json et installé avec adb install. Mettez l'AVD en cache entre deux exécutions ; sinon, les démarrages à froid dominent la durée du job.
Des artefacts vidéo quand un flow échoue
Un test E2E en échec avec une seule ligne de journal ne sert presque à rien. Maestro écrit des captures d'écran de l'étape en échec dans son répertoire de sortie, et maestro record produit une vidéo d'un flow. Envoyez les deux comme artefacts du workflow en cas d'échec, pour que la personne qui reprend l'exécution en échec voie ce que l'écran affichait au lieu de deviner à partir d'un nom de sélecteur.
Publication gérée, TestFlight et tracks Google Play
Amener un binaire jusqu'au store, c'est la moitié d'une release. Décider quand les utilisateurs le voient, c'est l'autre moitié, et chaque store la gère différemment.
Sur iOS, chaque build envoyé par EAS Submit apparaît dans TestFlight. Les testeurs internes le reçoivent dès la fin du traitement ; les testeurs externes passent par une courte Beta App Review la première fois. Quand le build part en examen App Store, la version peut être configurée pour une release manuelle une fois approuvée, l'équivalent App Store de la publication gérée. La release manuelle est le choix sûr par défaut ; avec la release automatique, une approbation à 2 h du matin est en ligne à 2 h du matin.
Sur Android, ce sont les tracks qui font ce travail. Une release en brouillon sur le track interne peut être promue vers les tests fermés, les tests ouverts et la production sans nouvel envoi, et la production permet un déploiement progressif par pourcentage. La publication gérée, activée dans la Play Console, ajoute une barrière par-dessus : les changements approuvés à l'examen restent en attente jusqu'à ce que quelqu'un appuie sur publier. Cette barrière permet d'aligner le même jour une mise à jour de la fiche, une note de version et une annonce.
Gérer un incident de conformité Google Play
Un jour ou l'autre, Google Play envoie un e-mail avec « action requise » dans l'objet. Gérer ces incidents de conformité faisait partie de mon travail sur ce projet, et le schéma est assez général pour être décrit sans citer de notification.
Une notification nomme la règle en cause, parfois avec une capture d'écran, et fixe une échéance. Selon la gravité, l'application peut être rejetée à sa prochaine mise à jour ou retirée jusqu'à correction. Lisez la page de la règle elle-même avant le résumé de l'e-mail, parce que la correction est en général plus étroite que ce que la notification laisse entendre : une déclaration manquante dans le formulaire Sécurité des données, ou un écran de connexion que l'examinateur n'a pas pu franchir.
Ensuite, la réponse est une release comme une autre, avec une trace écrite. Corrigez la cause, incrémentez la version, soumettez d'abord sur le track interne, et vérifiez que les notes destinées à l'examinateur contiennent un compte de test. Si la notification est erronée, la page État des règles (Policy status) propose un recours, et un recours qui cite le texte de la règle obtient une meilleure réponse qu'un recours agacé. L'échéance est la chose à suivre. Elle a sa place dans le document de release et dans un calendrier partagé, parce qu'une application retirée coûte plus par jour que n'importe quelle fonctionnalité que vous construisiez cette semaine-là.
Ce qu'un document de release doit contenir
Sur ce projet, la documentation de release dépassait trente pages. La couverture compte plus que la longueur, et voici ce dont un tel document a besoin. Les règles de versionnage : comment la version et le numéro de build évoluent, et qui incrémente quoi. La convention qui déclenche le workflow, tag ou branche. Où vit chaque identifiant, qui peut le renouveler, et quand le certificat Apple expire. La liste de contrôle des métadonnées de store : captures d'écran par taille d'appareil, le texte des nouveautés dans chaque langue, les notes pour l'examinateur avec un compte de test qui fonctionne. Le chemin de promotion sur chaque store avec la personne qui appuie sur publier. Les options de retour arrière, qui diffèrent : Android peut suspendre un déploiement progressif, iOS demande un nouveau build, et un EAS Update peut être republié depuis un bundle précédent pour les régressions purement JavaScript. Et une procédure écrite pour les notifications de non-respect des règles, parce que la suivante arrive quand la personne qui a géré la précédente est en vacances.
Les erreurs à éviter
Construire à chaque push sur main. Les builds natifs sont lents et consomment des minutes de build ; construisez sur des tags ou à la demande.
Faire confiance à une exécution E2E verte sans vidéo. Un flow peut passer parce qu'un sélecteur a ciblé un autre élément. Gardez les artefacts.
Laisser la version de runtime s'écarter de la couche native. Une seule mise à jour over-the-air qui référence un module natif absent plante au lancement pour tous les utilisateurs de l'ancien binaire.
Publier automatiquement après l'examen. Les deux stores permettent de retenir la release ; servez-vous-en.
Garder l'unique copie du keystore sur un portable. Le stockage des clés et certificats par EAS existe pour cette raison.
Deux choses méritent leur place dès le premier jour : le document de release, et un job planifié qui vérifie l'expiration du certificat Apple. Une fois le pipeline en place, une release est devenue des builds prêts pour les deux stores, passés par les flows Maestro sur émulateurs avant que quiconque appuie sur publier, et l'équipe décidait du moment où les utilisateurs les voyaient.