CI/CD Flutter avec Github Actions

Actualités > Mobilité > CI/CD Flutter avec Github Actions
Catégories
Piotr FLEURY

Publié le :

Par Piotr FLEURY

Comment construire, tester et déployer une application Flutter de manière industrialisée ?

Notre application Flutter tournant simultanément sur 4 plateformes. De gauche à droite: Web, Android natif, Desktop (mac), IOS natif

Lien vers l’application: https://flutter-ci-cd-e37ce.web.app/#/

Comment construire, tester et déployer une application Flutter de manière industrialisée ?

Tous les développeurs ont commencé par créer des applications en local sur leur poste. Mais voilà, lorsqu’il s’agit de travailler en équipe, il devient nécessaire d’avoir un endroit centralisé où une machine sera en charge de construire, valider et déployer votre application. C’est ce que tout le monde connait sous le nom de CI/CD.

Une CI/CD (continuous integration/continuous delivery) permet l’automatisation d’une série d’actions sur une plateforme centralisée dans le but de vérifier le build, la qualité et le bon fonctionnement d’un livrable. De manière très simplifiée, c’est un script ou une série de scripts qui vont s’exécuter sur un serveur ou une machine tierce.

Il existe beaucoup de CI/CD différentes. Elles peuvent toutes ou presque répondre à nos besoins. Sur sa page officielle, Flutter propose quelques CI/CD. Notre top 3 :

  • Github Actions
  • Codemagic.io
  • Gitlab CI

La liste exhaustive est disponible ici: https://flutter.dev/docs/deployment/cd

Comment créer une CI/CD via Github Actions dans un projet Flutter ?

Dans cet article nous avons fait le choix de nous intéresser à Github Actions pour illustrer nos exemples (https://github.com/features/actions).

A la fin de cet article, vous saurez:

  • construire une app Flutter pour différentes plateformes
  • vérifier le formatage de votre code
  • exécuter les tests automatisés
  • déployer

Vous pourrez retrouver le workflow Github Actions sur le repository spécialement créé pour l’occasion

https://github.com/XPEHO/flutter-ci-cd/tree/main/.github/workflows

Github actions en général

Pour créer une CI/CD Github actions vous aurez besoin d’initialiser un fichier workflow dans le dossier .github/workflows/ de votre projet. Ce fichier contiendra toutes les instructions permettant d’arriver à vos fins. Dans cet article nous considérons comme acquises les connaissances de base en Github actions.

Pour plus de détails sur Github actions rendez-vous aux pages suivantes :

https://docs.github.com/en/actions

https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions

Vérifier l’indentation de son code

Première étape, nous allons valider que l’indentation du code de l’application grâce à la commande suivante:

flutter format

Cette commande permet de contrôler la syntaxe ou même de formater les fichiers dart.

Syntax:

flutter format <DART_FILE | DIRECTORY>

Les différentes possibilités

Pour obtenir la notice il suffit de lancer la commande

flutter format -h
Usage: flutter format <one or more paths>
-h, — help Print this usage information.
-n, — dry-run Show which files would be modified but make no
changes.
— set-exit-if-changed Return exit code 1 if there are any formatting
changes.
-m, — machine Produce machine-readable JSON output.
-l, — line-length Wrap lines longer than this length. Defaults to 80
characters.
(defaults to “80”)

-h ou — help : Cette option permet de voir toutes les possibilités de la commande en question.

-n ou — dry-run : Cette option permet d’indiquer les fichiers qui seraient modifiés mais n’applique aucun changement.

— set-exit-if-changed : Cette option permet d’indiquer s’il y a besoin d’un formatage ou non avec comme “code de sortie” 1.

-m ou — machine : Cette option permet d’exporter dans un fichier JSON

-l ou — line-length : Cette option permet de voir les lignes plus longues que la longueur défini par défaut (80). (A confirmer également)

Exemple

Pour détecter les fichiers mal formatés nous utiliserons :

flutter format --set-exit-if-changed

Le résultat :

44 Running “flutter pub get” in flutter_tools… 2,653ms
45 Running “flutter pub get” in flutter-ci-cd… 750ms
46 Formatting directory .:
47 Unchanged lib/main.dart
48 Unchanged test/widget_test.dart
49 Analyzing flutter-ci-cd…
50 No issues found! (ran in 11.5s)

On peut donc apercevoir que les fichiers sont correctement formatés.

Dans le cas inverse, une erreur vous indiquera de formater le fichier.

Could not format because the source could not be parsed:
line 209, column 19 of file example.dart Expected to find ‘,’.

209 │ repository: getIt(),
│ ^^^^^^^^^^^^^^^^^^^^^^

line 208, column 19 of file example.dart Expected to find ‘,’.

208 │ repository: getIt()
│ ^^^^^^^^^^^^^^^^

Bon à savoir

Afin de ne plus reproduire cette erreur de format de fichier lors de la CI, plusieurs actions sont possibles.

Les IDE VsCode, Android Studio et IntelliJ, vous offrent la possibilité d’installer des plugins permettant de formater vos fichiers. Suivant l’IDE, vous pouvez le configurer pour lui dire d’organiser les imports, sauvegarder votre fichier et le formater.

Petit lien d’explication : https://flutter.dev/docs/development/tools/formatting

Détecter les warnings

flutter analyze

Cette commande permet d’analyser le projet dans sa globalité afin de voir les erreurs et de les corriger.

Syntax:

Syntax: flutter analyze --option

Les différentes possibilités

Usage: flutter analyze [arguments]
-h, --help Print this usage information.
--[no-]current-package Analyze the current project, if applicable.
(defaults to on)
--watch Run analysis continuously, watching the filesystem for changes.
--write=<file> Also output the results to a file. This is useful with "--watch" if you want a file to always contain the latest results.
--[no-]pub Whether to run "flutter pub get" before executing this command.
(defaults to on)
--[no-]congratulate Show output even when there are no errors, warnings, hints, or lints. Ignored if "--watch" is specified.
(defaults to on)
--[no-]preamble When analyzing the flutter repository, display the number of files that will be analyzed.
Ignored if "--watch" is specified.
(defaults to on)
--[no-]fatal-infos Treat info level issues as fatal.
(defaults to on)
--[no-]fatal-warnings Treat warning level issues as fatal.
(defaults to on)

-h ou — help : Cette option permet de voir toutes les possibilités de la commande en question.

— watch : Cette option permet d’exécuter une analyse en continu.

— preamble : Cette option permet d’indiquer le nombre de fichiers qui seront analysés.

— fatal-infos : Traite les problèmes de niveau d’informations comme fatal

— fatal-warnings : Traite les problèmes de niveau d’avertissement comme fatal

Exemple

Dans notre projet CI/CD : https://github.com/XPEHO/flutter-ci-cd

❯ flutter analyze
Running “flutter pub get” in flutter-ci-cd… 682ms
Analyzing flutter-ci-cd…
No issues found! (ran in 1.8s)

Dans cet exemple, on peut voir que notre projet ne comporte aucune erreur.

Autre exemple retourné :

❯ flutter analyze
Analyzing flutter-ci-cd…
 error • This requires the ‘non-nullable’ language feature to be enabled • lib/main.dart:33:15 • experiment_not_enabled
error • Expected an identifier • lib/main.dart:45:55 • missing_identifier
2 issues found. (ran in 2.0s)

On peut tout de suite voir les erreurs retournées par la commande flutter analyze, avec la ligne indiquée et le type d’erreur.

Exécuter les tests automatisés

Commençons par créer une simple fonction multiply qui retournera la multiplication de ses arguments. Cette fonction nous servira à créer un test qui sera ensuite lancé automatiquement par la CI.

int multiply(int a, int b) {
return a * b;
}

Créons maintenant un test afin de vérifier si notre fonction multiply fonctionne correctement. Pour ce faire, nous allons créer un fichier contenant nos tests dans le dossier prévu à cet effet.

> Pour plus d’informations sur les tests, je vous redirige vers la documentation officielle Flutter

Testons d’abord une simple multiplication de base puis effectuons une multiplication par zéro dans un second test :

void main() {
group('Multiply function testing', () {
test('Simple multiplication', () {
final result = multiply(6, 7);

expect(result, 42, reason: 'Regular multiplication should work');
});

test('Ensure correct handling of zero', () {
final result = multiply(13, 0);

expect(result, 0, reason: 'Multiple with zero should be zero');
});
});
}

Lancer les tests dans la CI

Il est maintenant possible d’exécuter nos tests à l’aide de la commande

flutter test

à ajouter dans les steps de notre CI :

name: Test CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
...
- name: Run Flutter test
run: flutter test

Notez que tous les tests présents dans le dossier test seront testés par défaut.

Construire l’application

Comme sur votre projet vous aurez besoin de la commande suivante pour construire une application avec Flutter:

flutter build <OPTION>

Pour connaitre toutes les options il suffit de taper

flutter build -h
Build an executable app or install bundle.
Global options:
-h, — help Print this usage information.
-v, — verbose Noisy logging, including all shell commands executed.
If used with — help, shows hidden options.
-d, — device-id Target device id or name (prefixes allowed).
— version Reports the version of this tool.
— suppress-analytics Suppress analytics reporting when this command runs.
Usage: flutter build <subcommand> [arguments]
-h, — help Print this usage information.
Available subcommands:
aar Build a repository containing an AAR and a POM file.
apk Build an Android APK file from your app.
appbundle Build an Android App Bundle file from your app.
bundle Build the Flutter assets directory from your app.
ios Build an iOS application bundle (Mac OS X host only).
ios-framework Produces .xcframeworks for a Flutter project and its plugins for integration into existing, plain Xcode projects.
ipa Build an iOS archive bundle (Mac OS X host only).
web Build a web application bundle.

-h, — help : Permet d’afficher la liste des commandes possibles.

-v, — verbose : Permet d’avoir un détail plus complet des logs du build.

-d , — device-id : Permet de spécifier l’id d’un device afin d’y effectuer le build.

— version : Permet de voir la version et le channel flutter ainsi que la version de dart.

— suppress-analytics : Empêche flutter d’envoyer des rapports de crash à google.

Vous pouvez effectuer un build pour différentes plateformes, vous pouvez les retrouver ici : https://github.com/marketplace/actions/flutter-action

Exemple :

Dans ce tutoriel, nous allons créer 2 jobs, et voir comment effectuer un build apk et un build web.

Retrouvez notre CI/CD ici : https://github.com/XPEHO/flutter-ci-cd

Build d’un apk

Premièrement, nous allons voir comment effectuer le build d’un apk flutter.

Pour cela, nous créons un job build-apk, dans lequel nous allons définir sur quel environnement se déroulera le job et quelles sont les étapes de celui-ci.

build-apk:
runs-on: ubuntu-latest

Nous souhaitons créer un apk flutter, nous aurons donc besoin d’ajouter dans les étapes du build un usage permettant à la CI d’utiliser java, et un second lui permettant d’utiliser flutter.

steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1
with:
flutter-version: '2.5.0'

Une fois nos usages ajoutés, nous pouvons indiquer à la CI les différentes actions à exécuter. Dans notre cas, nous voulons vérifier que tous les paquets de notre application sont compatibles, que nos tests fonctionnent et enfin créer un apk debug.

- name: Build apk
run: |
flutter pub get
...
flutter test
flutter build apk --debug

Build web

Pour effectuer un build web, le processus est semblable à celui du build d’un apk, seulement, java n’est pas nécessaire dans ce build, nous n’avons besoin que de l’usage permettant à la CI d’utiliser flutter.

build-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
flutter-version: '2.5.0'

Une fois l’usage ajouté, comme pour le build apk, nous pouvons indiquer à la CI les différentes actions à exécuter. Dans le cas présent, nous souhaitons vérifier que tous les paquets de notre application sont compatibles, que nos tests fonctionnent et créer un build web.

- name: Build web
run: |
flutter pub get
...
flutter test
flutter build web

Déployer notre application en version Android

Mettre à disposition un APK signé avec Github releases

Le keystore

Si ce n’est pas déjà fait, il faut générer un keystore pour signer notre APK (plus d’infos ici: https://developer.android.com/studio/publish/app-signing#generate-key)

Le build Gradle

Ensuite, nous allons configurer le build de notre app Android pour qu’il signe son APK au cours d’un build de type release.

Dans le dossier android ajoutez un fichier “key.properties” avec le contenu suivant:

storePassword=
keyPassword=
keyAlias=
storeFile=

Prenez le soin d’y mettre vos mots de passe et la localisation de votre keystore.

N’oubliez pas d’ajouter le fichier android/key.properties dans votre .gitignore à la racine du projet pour éviter de commit vos mots de passe sur Github.

Dans le fichier android/app/build.gradle il faut configurer le build release.

...
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
   ...
  signingConfigs {
    release {
      keyAlias keystoreProperties['keyAlias']
      keyPassword keystoreProperties['keyPassword']
      storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
      storePassword keystoreProperties['storePassword']
    }
  }
  buildTypes {
    release {
      signingConfig signingConfigs.release
    }
  }
}
...

Pour tester si votre release fonctionne en local, il suffit de lancer la construction d’un APK sans l’option debug:

flutter build apk

Si tout se passe bien, vous devriez obtenir un joli APK signé.

Ajouter les informations de signature dans les secrets

Github fournit une gestion de secrets bien pratique lorsque vous avez besoin d’utiliser des mots de passe dans votre CI.

Rendez-vous sur le repository de votre projet et allez dans Settings > Secrets. Ajoutez ensuite les secrets KEY_PROPERTIES et RELEASE_KEYSTORE :

La valeur de KEY_PROPERTIES correspond au contenu du fichier key.properties. Remplacez simplement la valeur de storeFile par ./upload-keystore.jks

La valeur du RELEASE_KEYSTORE correspond au contenu de votre keystore.jks encodé en base64.

Sur macOS

#encoder en base64
base64 -i keystore.jks -o keystore.encoded
#afficher le contenu
more keystore.encoded

Coté Github actions

Dans le but de pouvoir lancer une release, nous allons créer un workflow séparé.

Dans le dossier .github/workflows/ ajoutez une fichier nommé release.yml et ajoutez-y le contenu suivant :

name: RELEASE
on:
workflow_dispatch:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Configure signature
run: |
echo "${{secrets.KEY_PROPERTIES}}" > android/key.properties
echo "${{secrets.RELEASE_KEYSTORE}}" > upload-keystore.encoded
base64 -d -i upload-keystore.encoded > ./android/app/upload-keystore.jks
- uses: actions/setup-java@v1
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1
with:
flutter-version: '2.0.5'
- name: Build apk
run: |
flutter pub get
flutter format --set-exit-if-changed .
flutter analyze
flutter test
flutter build apk
- name: Retrieve Release Version
id: versionstep
run: |
VERSION=$(more pubspec.yaml | grep version: | cut -d ' ' -f2)
echo "::set-output name=VERSION::$VERSION"
- name: Upload the APK onto Github
uses: ncipollo/release-action@v1
with:
artifacts: 'build/app/outputs/flutter-apk/*.apk'
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ steps.versionstep.outputs.VERSION }}

Notez que le déclencheur de ce workflow est “workflow_dispatch” ce qui signifie qu’il faudra le lancer manuellement.

Une fois la release faite, vous n’aurez plus qu’a récupérer votre APK sur Github et a l’importer sur le Play Store dans la console développeur.

Déployer notre application en version WEB avec Firebase Hosting

Premièrement, il faut créer un projet firebase, pour ce faire, il faut vous rendre dans la console firebase : https://console.firebase.google.com/

Une fois le projet créé, rendez vous dans le menu hosting du projet et démarrez le hosting

Pour mettre en place le hosting du projet, firebase vous indique les différentes étapes à suivre: L’installation du firebase CLI, l’initialisation de votre projet flutter ainsi que le déploiement via firebase hosting

Pour installer firebase CLI, il faut effectuer dans un terminal la commande :

npm install -g firebase-tools

Une fois firebase CLI installé, il faut initialiser votre projet flutter, pour ce faire, il faut effectuer deux commandes dans le terminal de votre projet

La connection à firebase :

firebase login

L’initialisation du projet flutter dans firebase :

firebase init

Durant l’initialisation de votre projet, plusieurs questions vous seront posées

Quelle feature firebase souhaitez-vous mettre en place dans votre projet ?

Dans notre cas, nous souhaitons mettre en place firebase hosting, nous allons donc cocher la case “hosting : configure files for firebase hosting and (optionnaly) setup Github action deploys”

Quel projet souhaitez-vous associer au projet firebase ?

Dans notre cas nous utiliserons un projet existant ( notre projet flutter )

Quel répertoire souhaitez-vous utiliser pour le déploiement ( celui-ci contiendra les fichiers de déploiement ) ?

Dans notre cas nous souhaitons déployer notre projet flutter web, nous allons donc choisir le dossier build/web

Souhaitez-vous effectuer la configuration comme une single-page app ?

Dans notre cas, c’est oui

Souhaitez-vous mettre en place le déploiement automatique via Github ?

Nous allons répondre non, nous définirons la méthode de déploiement dans la CI/CD du projet flutter

Souhaitez-vous écraser le fichier build/web/index.html ?

Non, nous souhaitons garder le fichier de notre projet flutter

L’initialisation est désormais terminée

Il vous est alors possible de tester le déploiement en effectuant la commande :

firebase deploy

Le déploiement web dans Github actions :

La partie firebase étant terminée, nous pouvons passer à la mise en place du déploiement dans la CI/CD Github actions

Premièrement, nous allons avoir besoin de créer un secret dans notre projet Github dans lequel nous allons ajouter un token firebase permettant à la CI/CD de se connecter à notre projet firebase et effectuer le déploiement

Pour récupérer ce token, il faut effectuer dans le terminal du projet flutter la commande :

firebase login:ci

Une fois le secret créé, nous allons pouvoir ajouter à notre workflow effectuant le build web la partie déploiement

- name: Deploy web
env:
SECRET_NAME: ${{ secrets.SECRET_NAME }}
if: github.base_ref == 'Name of the branch to be deployed'
run: |
curl -sL https://firebase.tools | bash
firebase deploy - token $SECRET_NAME

Et voilà, le déploiement sera effectué sur firebase lorsqu’un push sera réalisé sur la branche choisie

La copie finale

name: CI
on: push
jobs:
build-apk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: '12.x'
- uses: subosito/flutter-action@v1
with:
flutter-version: '2.5.0'
- name: Build apk
run: |
flutter pub get
flutter format --set-exit-if-changed .
flutter analyze
flutter test
flutter build apk --debug
- name: Run Flutter test
run: flutter test
build-web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: subosito/flutter-action@v1
with:
flutter-version: '2.5.0'
- name: Build web
run: |
flutter pub get
flutter format --set-exit-if-changed .
flutter analyze
flutter test
flutter build web
- name: Deploy web
env:
WEB_DEPLOY_TOKEN: ${{ secrets.WEB_DEPLOY_TOKEN }}
if: github.ref == 'refs/heads/main'
run: |
curl -sL https://firebase.tools | bash
firebase deploy - token $WEB_DEPLOY_TOKEN

Disponible ici: https://github.com/XPEHO/flutter-ci-cd/blob/main/.github/workflows/cicd.yml

Et la release ici: https://github.com/XPEHO/flutter-ci-cd/blob/main/.github/workflows/release.yml

Et voilà ce que ça donne dans Github à chaque Pull Request

En résumé

Une CI/CD est un moyen sûr et automatisé pour vérifier et packager votre code. A chaque pull request vous obtenez un feedback rapide avec un verdict positif ou négatif qui évite de laisser passer bon nombre d’erreurs humaines.

Merci

Merci d’avoir lu cet article. N’hésitez pas à le partager s’il vous a plu. Pensez aussi à nous suivre sur Medium pour ne pas manquer les prochaines publications.