Simon Boisset
Retour au blog

Workflows Expo CI/CD (EAS) : builds intelligents, fingerprint et OTA

12 janvier 2026

Workflows Expo CI/CD (EAS) : builds intelligents, fingerprint et OTA

Workflows Expo CI/CD (EAS) : builds intelligents, fingerprint et OTA

Dans un projet Expo/React Native, la vraie question n'est pas "comment builder ?" mais "quand faut-il vraiment builder ?". Les builds natifs sont lents (et coûteux en CI), alors que les updates OTA sont rapides... mais ne couvrent pas tous les changements.

L'objectif du workflow : automatiser toute la chaîne (builds natifs, OTA, soumissions) tout en évitant les builds inutiles, grâce au fingerprint Expo et à un versioning explicite.


Structure type : staging + production

Deux environnements, un seul flux :

  • dev/staging -> environnement preview
  • main -> environnement production

Même logique dans les deux cas :

  1. calculer un fingerprint
  2. décider entre build natif ou OTA
  3. publier automatiquement

En production, on ajoute la soumission store lorsqu'un build natif a eu lieu.


Fingerprint : décider automatiquement

Le fingerprint est un hash basé sur ce qui impacte vraiment le binaire natif :

  • dépendances et modules natifs
  • config Expo + plugins
  • fichiers de config
  • version marketing (si injectée dans app.config)

Règle simple :

  • Fingerprint identique à un build existant -> OTA
  • Fingerprint différent -> build natif

Exemple de workflow EAS (générique)

Voici un exemple inspiré d'un workflow réel, mais volontairement générique. Adapte les paths, profile et channel à ton projet.

name: Production builds

on:
  push:
    branches:
      - main
    tags:
      - v*.*.*
      - "!v*.*.*-**"
    paths:
      - apps/mobile/**
      - packages/**
      - "!**/*.md"

jobs:
  fingerprint:
    name: Fingerprint
    type: fingerprint
    environment: production

  get_android_build:
    name: Check existing Android build
    needs: [fingerprint]
    type: get-build
    environment: production
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
      profile: production

  get_ios_build:
    name: Check existing iOS build
    needs: [fingerprint]
    type: get-build
    environment: production
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
      profile: production

  build_android:
    name: Build Android
    needs: [get_android_build]
    if: ${{ !needs.get_android_build.outputs.build_id }}
    type: build
    environment: production
    params:
      platform: android
      profile: production

  build_ios:
    name: Build iOS
    needs: [get_ios_build]
    if: ${{ !needs.get_ios_build.outputs.build_id }}
    type: build
    environment: production
    params:
      platform: ios
      profile: production

  submit_android:
    name: Submit Android
    needs: [build_android]
    if: ${{ needs.build_android.outputs.build_id }}
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_android.outputs.build_id }}
      profile: production

  submit_ios:
    name: Submit iOS
    needs: [build_ios]
    if: ${{ needs.build_ios.outputs.build_id }}
    type: submit
    environment: production
    params:
      build_id: ${{ needs.build_ios.outputs.build_id }}
      profile: production

  update_android:
    name: Update Android
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_android_build.outputs.build_id && !needs.get_ios_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: android

  update_ios:
    name: Update iOS
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_ios_build.outputs.build_id && !needs.get_android_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: ios

  update_all:
    name: Update All
    needs: [get_android_build, get_ios_build]
    if: ${{ needs.get_android_build.outputs.build_id && needs.get_ios_build.outputs.build_id }}
    type: update
    environment: production
    params:
      channel: production
      platform: all

Versioning : version marketing explicite, builds auto-incrémentés

Je sépare volontairement :

  • version marketing (lisible, intentionnelle) : gérée par nous
  • numéros de build (buildNumber iOS / versionCode Android) : auto-incrémentés par EAS

La version marketing est stockée dans package.json :

{
  "name": "my-app",
  "version": "1.4.0"
}

Et injectée dans app.config.ts :

import type { ConfigContext, ExpoConfig } from "expo/config";
import pkg from "./package.json";

export default ({ config }: ConfigContext): ExpoConfig => ({
  ...config,
  version: pkg.version,
});

Conclusion & accompagnement

Ce type de pipeline réduit fortement les coûts de build, accélère les cycles de livraison et sécurise les mises en production grâce à des règles simples et automatisées.

👉 Vous souhaitez mettre en place ou optimiser un workflow Expo CI/CD (fingerprint, OTA, versioning, soumissions automatiques) ?

Je vous accompagne sur l'architecture, la configuration EAS, la CI et les bonnes pratiques Expo/React Native.

➡️ Prenez rendez-vous avec moi pour construire un pipeline adapté à votre application.

Simon Boisset

Je suis Simon Boisset, développeur mobile/full-stack. J'aide les équipes à livrer des apps React Native/Expo, remettre à plat des stacks legacy et sécuriser les releases.

Prendre rendez-vous