Flutter, a wonderful journey from 0 to app stores - CI featuring Codemagic and Fastlane
Tags: flutter, codemagic, fastlane, ci
Published: 2020-11-03 17:54:33

So you just started using [Flutter](https://flutter.dev/) and already love its [wonderful and numerous widgets](https://flutter.dev/docs/development/ui/widgets), but as your project is being serious you're wondering how to sign your Android & iOS apps and how to deploy them to both stores ? Oh, and as a ~~lazy~~ good developer, you're obvisouly wondering how to automate your builds and deployments to reduce manual actions (and errors) and free up some of your precious time to code your beautiful app ?

Well, me too. At first I thought it would be very simple and I'd just have to configure the awesome [Codemagic](https://codemagic.io/), use its 500 free minutes per month... and relax 🛁 <br/>
After all, it's supposed to be magic 🧙 !

In this article I will explain step-by-step how to easily automate app building and beta deployment to Android & iOS stores.

❗️📝
* I am not claiming to have the best approach of all, but I needed a reminder of all the steps needed and I thought it could benefit others... tell me if you have better ways to do it or if I can improve my solution !
* In this article we will only explore how to deploy to testing tracks (beta track for iOS and internal tests for Android), but the process should be pretty much the same for other tracks.

The various chapters we will detail are the following:
1. 👥 [Prerequisites](#prerequisites): all the accounts and things you need before setting up the CI
1. 🧙‍♂️ [Codemagic setup](#codemagic-setup): setup of the CI workflow(s)
1. 🤖 [Android setup](#android-setup)
1. 🍏📱 [iOS setup](#ios-setup)

📁 You can find my [demo repository on Github](https://github.com/orevial/flutter-codemagic-demo).

# Prerequisites <a name="prerequisites"></a>

Hopefully you are reading this article before you actually **need** the CI, because asking all people in the company to create the various accounts may take some time.

## 👥 Accounts <a name="prerequisites-accounts"></a>

In order to connect to different platforms through Codemagic you will need a few accounts and keys to be setup before you start implementing actual CI pipeline.

In particular you need the following accounts and keys:

* [Apple enrolled account](https://developer.apple.com/programs/enroll/). You basically need a paid Apple Developer account, either enrolling as an individual or as an organization.
    * Example: _support@your-company.com_
* [Apple Developer account](https://appstoreconnect.apple.com/apps/1515747150/testflight/ios): Used to handle [iOS profiles & certificates](https://developer.apple.com/account/resources/certificates/list) and to [deploy iOS builds to TestFlight](https://appstoreconnect.apple.com/apps/1537567267/testflight).
    * This account must have App Manager role (not juste Developer role).
    * This account will be the same as the enrolled account if you're an individual, otherwise you should probably keep things separate and create this account as a "technical account".
    * Example: _apple-technical@your-company.com_
* [Google Play Developer enrolled account](https://support.google.com/googleplay/android-developer/answer/6112435?hl=en). Again, you need a paid Google Play Developer account if you want to publish your apps.
    * Example: _support@your-company.com_
* [Google Play Console service account key](https://docs.fastlane.tools/actions/supply/#setup). A JSON key used to deploy [Android builds to Google Play beta store](https://play.google.com/console).
    * This account must have Release Manager role.
    * This key will later be used in [Google Play service account key](#google-play-service-account-key) chapter.
* [Google Play Console account](https://play.google.com/console/developers/): your personal account
    * This account must have at least Release Manager role.
    * Strictly speaking this account won't be used by Codemagic (it will only use service account JSON key mentionned above) but you will need it to manually upload your first build or to to fill app information in Google Play Console, promote the build to production and so on.
    * Example: Google developer account like _your.name@your-company.com_
* **Gitlab/Github/Bitbucket** account. Used by Codemagic to checkout Flutter app's repository.
    * Make sure this account has access to your repository by checking repository Members (e.g. in [Gitlab members](https://docs.gitlab.com/ee/user/project/members/))
    * You need to generate SSH key pair and upload the public key to your profile (e.g. [Gitlab SSH keys](https://gitlab.com/-/profile/keys)). You can name the public key anything, I like to put something like `Codemagic app XXX`
    * Keep the private key, you will need to encrypt it and pass it to Codemagic later on !
    * Example: _codemagic@your-company.com_
* [Codemagic account](https://codemagic.io/). Used to configure Codemagic pipeline itself, Codemagic account is required to run any build as each account has a limited number of build minutes available (500 by default).
    * For simplicity reasons I use the same Gitlab account mentioned above to connect to Codemagic (e.g. _codemagic@your-company.com_ **Gitlab** account).

## Configuration <a name="prerequisites-config"></a>

Once you have your personal accounts for both Apple Connect and Google Play Console, you should take some time to create the application page for both stores as you won't be able to deploy Android or iOS test apps before you filled a minimum information:

* [Google Play Console](https://play.google.com/console/) : create a new application in the console.
    * Make sure you enter a correct package name, it's definitive !
* [Google Play Console](https://play.google.com/console/) : fill-in basic information. Start at app's dashboard and follow the guide to fill all required steps (there are a lot of steps but it's pretty simple)
    * You will need to upload at least an app icon (make sure you respect appropriate format), a Play Store commercial banner and 2 screenshots of the app before you can upload your first APK (yes, even for internal tests).
* [Apple Developer identifiers](https://developer.apple.com/account/resources/identifiers/list) : create a new identifier of type `App IDs` and follow the steps.
    * Note generated `bundleId` and `App team id`, you will need them later.
    * Make sure you enter a correct bundle id, it's definitive !
* [Appstore Connect](https://appstoreconnect.apple.com/) : create a new application in the console. You will need to link your new app to a `bundleId`... the one you created in previous step 🤘
* [Appstore Connect](https://appstoreconnect.apple.com/) : fill-in basic information.

# 🧙‍♂️ Codemagic setup <a name="codemagic-setup"></a>

Codemagic use multiple files to do its "magic":

* [codemagic.yaml main file](https://github.com/orevial/flutter-codemagic-demo/blob/main/codemagic.yaml): defines various worflow for iOS, Android and both apps deployments.
* [Codemagic post-clone setup file](https://github.com/orevial/flutter-codemagic-demo/blob/main/codemagic/post-clone.sh): various installation steps for both Android and iOS apps
* [Gemfile for Android](https://github.com/orevial/flutter-codemagic-demo/blob/main/android/Gemfile) and [for iOS](https://github.com/orevial/flutter-codemagic-demo/blob/main/ios/Gemfile). Used to automate gem installation on Codemagic using `bundle install` command.
* [Fastlane for Android](https://github.com/orevial/flutter-codemagic-demo/blob/main/android/fastlane/) and [Fastlane for iOS](https://github.com/orevial/flutter-codemagic-demo/blob/main/ios/fastlane/): we use [Fastlane](https://docs.fastlane.tools/) for all build and deploy steps to help us handle certificate management, app signing and similar boring stuff.

Let's start right away with Codemagic main file, [codemagic.yaml](https://github.com/orevial/flutter-codemagic-demo/blob/main/codemagic.yaml):

```yaml
workflows:
  internal-deployment:
    name: Internal deployment
    max_build_duration: 90
    environment:
      vars:
        BUILD_NAME: 1.2.3
        GOOGLE_PLAY_STORE_JSON_BASE64: Encrypted(var)
        ANDROID_KEYSTORE: Encrypted(var)
        ANDROID_KEYSTORE_PASSWORD: Encrypted(var)
        ANDROID_KEY_ALIAS: Encrypted(var)
        ANDROID_KEY_PASSWORD: Encrypted(var)
        IOS_APP_ID: com.company.somename
        APPLE_DEVELOPER_TEAM_ID: 123ABCD456
        FASTLANE_EMAIL: apple-technical@your-company.com
        FASTLANE_PASSWORD: Encrypted(var)
        MATCH_PASSWORD: Encrypted(var)
        SSH_KEY_FOR_FASTLANE_MATCH_BASE64: Encrypted(var)
        FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: Encrypted(var)
      flutter: stable
      xcode: latest
      cocoapods: default
    cache:
      cache_paths:
        - $HOME/Library/Caches/CocoaPods
        - $HOME/.gradle/caches
        - $FLUTTER_ROOT/.pub-cache
    triggering:
      events:
        - tag
    scripts:
      - name: Post clone setup for Android & iOS
        script: |
          #!/usr/bin/env sh
            /bin/sh $FCI_BUILD_DIR/codemagic/post-clone.sh all
      - name: Build & Deploy Android app
        script: |
          cd $FCI_BUILD_DIR/android

          bundle exec fastlane supply init --track internal --package_name com.company.somename
      - name: Build & Deploy iOS app
        script: |
          cd $FCI_BUILD_DIR/ios

          bundle exec fastlane ios beta
    artifacts:
      - build/**/outputs/**/*.apk
      - build/**/outputs/**/*.aab
      - build/ios/ipa/*.ipa
      - /tmp/xcodebuild_logs/*.log
      - flutter_drive.log
    publishing:
      slack:
        channel: '#flutter-codemagic-demo'
        notify_on_build_start: false
```

If you're familiar with [Gitlab CI](https://docs.gitlab.com/ee/ci/yaml/README.html) or YAML in general, you shouldn't be too shocked reading this file. Let's describe the behaviour of your one and only workflow, _internal-deployment_:

* 🔫 `max_build_duration`: a timeout duration, because sometimes Codemagic/Android/iOS builds get stuck.
* ✓ `environment`: all environment variables that will be used by our scripts. More on this in [Android](#android-setup) and [iOS](#ios-setup) setup. Also defines the versions of Flutter, Xcode and Cocoapods that Codemagic should use. It is good practice to use latest stable versions.
* 🏎 `cache`: files that should be cached and reused across multiple builds to save some precious build minutes.
* 🔘 `triggering`: how the build should be triggered. I decided that mine should be triggered every time I release a new tag in my Gitlab project. Note that I had to add a Gitlab webhook in order to automatically trigger Codemagic on tag creation.
* ▶️ `scripts`: the core of our workflow: what we should do when this Codemagic workflow is triggered. As you can see here we only delegate the work to Fastlane for both Android and iOS, as we will see in a minute.
* 📦 `artifacts`: the artifacts we want to publish on Codemagic. Not solely needed as artifacts will be published to iOS and Android consoles but I like to keep them here in case the build fails and I need to debug.
* 📩 `publishing`: I like to be notified when my build fails or succeeds so I configured a Slack notification here.

The first script called by our worflow is a "post-clone" script, let's see this [codemagic/post-clone.sh](https://github.com/orevial/flutter-codemagic-demo/blob/main/codemagic/post-clone.sh) file:

```shell
#!/usr/bin/env sh

set -e # exit on first failed commandset

installGems() {
  echo "Installing gems..."

  bundle install
  bundle update fastlane
  bundle update signet
}

androidSteps() {
  echo "========================================"
  echo "|       Android post-clone steps       |"
  echo "========================================"

  # set up key.properties
  echo $ANDROID_KEYSTORE | base64 --decode > /tmp/keystore.keystore
  cat >> "$FCI_BUILD_DIR/android/key.properties" <<EOF
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEY_PASSWORD
storeFile=/tmp/keystore.keystore
EOF

  # set up local properties
  echo "flutter.sdk=$HOME/programs/flutter" > "$FCI_BUILD_DIR/android/local.properties"

  echo "--- Generate Google Service key for Android"
  GOOGLE_PLAY_STORE_JSON_KEY_PATH="$FCI_BUILD_DIR/android/app/google-play-store.json"
  echo $GOOGLE_PLAY_STORE_JSON_BASE64 | base64 --decode > $GOOGLE_PLAY_STORE_JSON_KEY_PATH

  cd $FCI_BUILD_DIR/android
  installGems
}

iosSteps() {
  echo "========================================"
  echo "|         iOS post-clone steps         |"
  echo "========================================"

  echo "--- Generate SSH key for Gitlab access from Fastlane Match"
  echo $SSH_KEY_FOR_FASTLANE_MATCH_BASE64 | base64 --decode > /tmp/bkey

  # adding custom ssh key to access private repository
  chmod 600 /tmp/bkey
  cp /tmp/bkey ~/.ssh/bkey
  ssh-add ~/.ssh/bkey

  cd $FCI_BUILD_DIR/ios
  installGems
}

androidSteps
iosSteps
```

This script is pretty boring but what it does is install a bunch of gems and setup some files that iOS and Android will need for building, signing and publishing the apps.

As you can see there is a bundle install that will use [Android Gemfile](https://github.com/orevial/flutter-codemagic-demo/blob/main/android/Gemfile) and [iOS Gemfile](https://github.com/orevial/flutter-codemagic-demo/blob/main/ios/Gemfile) to automatically install required gems:

**Android Gemfile**
```shell
source "https://rubygems.org"

gem "fastlane"
```

**iOS Gemfile**
```shell
source "https://rubygems.org"

gem "fastlane"
gem "cocoapods"
```

Let's now see Android build script in [android/fastlane/Fastfile](https://github.com/orevial/flutter-codemagic-demo/blob/main/android/fastlane/Fastfile) file:

```
default_platform(:android)

platform :android do
  desc "Deploy a new internal build to Google Play"
  lane :internal do
    Dir.chdir "../.." do
      sh("flutter", "packages", "get")
      sh("flutter", "clean")
      sh("flutter", "build", "appbundle", "--build-number=#{ENV["PROJECT_BUILD_NUMBER"]}", "--build-name=#{ENV["BUILD_NAME"]}")
    end
    upload_to_play_store(
        track: 'internal',
        aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end
end
```

This script basically uses [Fastlane](https://docs.fastlane.tools/) to build and deploy the app to Google Play Console. First it defines a "lane", which is just Fastlane way of defining a "worflow". Then we use shell to initialize and build our flutter app, by passing it build name and number from environment variables.

Once the signed application is ready, we use Fastlane once again to publish this app to the _internal_ Android track through [upload_to_play_store](https://docs.fastlane.tools/actions/upload_to_play_store/) task.

Pretty simple right ? 🎉

Let's now see the equivalent for iOS:

```
default_platform(:ios)

platform :ios do
  before_all do
      # Create a local keychain that will later store iOS profiles and certificates
      if is_ci?
          puts "This is CI run. Setting custom keychain."
          create_keychain(
              name: 'Temp.codemagic_keychain',
              password: 'Temp.codemagic_keychain_password',
              default_keychain: true,
              unlock: true,
              timeout: 3600,
          )
      end
  end

  desc "Push a new beta build to TestFlight"
  lane :beta do
    # Synchronize profiles & certificates from Git repo using Match
    match(
        type: "appstore",
        readonly: is_ci,
        keychain_name: 'Temp.codemagic_keychain',
        keychain_password: 'Temp.codemagic_keychain_password'
    )
    # Disable automatic code signing as we will use custom signing method later on
    update_code_signing_settings(
      use_automatic_signing: false
    )
    # Update Xcode provisioning profile with the one we got from Git repo using Match
    update_project_provisioning(
      # https://github.com/fastlane/fastlane/issues/15926
      profile: ENV["sigh_#{ENV["IOS_APP_ID"]}_appstore_profile-path"],
      build_configuration: "Release",
      code_signing_identity: "iPhone Distribution",
      xcodeproj: "Runner.xcodeproj",
    )
    # Replace version number with Codemagic build number
    set_info_plist_value(
      path: "Runner/Info.plist",
      key: "CFBundleVersion",
      value: ENV["PROJECT_BUILD_NUMBER"]
    )
    # Replace version name with our semver version
    set_info_plist_value(
      path: "Runner/Info.plist",
      key: "CFBundleShortVersionString",
      value: ENV["BUILD_NAME"]
    )
    # Run a first Flutter build with code signing disabled
    Dir.chdir "../.." do
      sh("flutter", "packages", "get")
      sh("flutter", "clean")
      sh("flutter", "build", "ios", "--release", "--no-codesign")
    end
    # Run a second Flutter build with custom code signing
    build_app(
        workspace: "Runner.xcworkspace",
        scheme: "Runner",
        configuration: "Release",
        xcargs: "-allowProvisioningUpdates",
        export_options: {
            signingStyle: "manual",
            method: "app-store",
            provisioningProfiles: {
                "#{ENV["IOS_APP_ID"]}": "match AppStore #{ENV["IOS_APP_ID"]}"
            }
        },
    )
    # Upload our build to TestFlight (Beta track)
    upload_to_testflight(
        skip_waiting_for_build_processing: true,
        apple_id: "123456789"
    )
  end
end
```

Alright, let's decrypt this file step-by-step:

1. `before_all` : Before all steps run we create a local keychain that will later store iOS profile and certificates for app signting. This keychain will only live the time of the Codemagic build so we don't really care what the password is.
1. `match` : We start the _beta_ lane with profile and certificates synchronization. Codemagic configuration for iOS deployment can be rather complicated, in order to avoid this complexity by handling certificates and provisioning profiles ourselves, we use Fastlane and [match](https://docs.fastlane.tools/actions/match/) for [code signing](https://codesigning.guide/). Here we basically tell [match](https://docs.fastlane.tools/actions/match/) to retrieve all stored profiles and certificates from the repo we stored them previously, and store them into the keychain we created in the previous step. We tell match to get things for `appstore` type, i.e. release profile and certificates. We will see later on in [iOS setup](#ios-setup) how to initialize [match](https://docs.fastlane.tools/actions/match/) to create and store profile and certificates.
1. `update_code_signing_settings` : We disable automatic code signing because we want to handle it manually using match-retrieved profile/certificates from previous step
1. `update_project_provisioning` : This step is essential as it tells Xcode to use our generated profile and certificates instead of its default (automatic) signing files. We tell it to use [match retrieved provisioning profile](https://docs.fastlane.tools/actions/match/) instead.
1. `set_info_plist_value` : We replace Xcode version values with the correct version:
    * `PROJECT_BUILD_NUMBER` is the [Codemagic global build number](https://docs.codemagic.io/building/environment-variables/) (global to your application, no matter which workflow you run). Useful to deploy the same version multiple times and from different workflows.
    * `BUILD_NAME` is the actual version of our project, in semver terminology. Note that it is up to you to handle the bump of this variable on each of your app's releases. In my case I use Gitlab pipeine with a custom script to automatically bump `BUILD_NAME` environment variable inside `codemagic.yaml` and commit this as a release commit.
1. `flutter build ios --release --no-codesign` : Flutter setup with a full packages/clean and build process, but with codesign disabled. As stated in [Flutter official CI/CD documentation](https://flutter.dev/docs/deployment/cd#cloud-build-and-deploy-setup), we need to initialize our project wiht a first Flutter build before we can actually run the signing build step with Fastlane (see next step)
1. `build_app` : We use [Fastlane](https://docs.fastlane.tools/) once again to build a signed version of our iOS application. Of course we tell it to use the provisioning profile we retrieved in previous step with match.
1. `upload_to_testflight` : We can finally use [Fastlane upload_to_testflight task](https://docs.fastlane.tools/actions/upload_to_testflight/) to automatically push our new `.ipa` application to Apple TestFlight (for beta testing). The two parameters are set to avoid waiting for Apple build processing to end Codemagic build. It means that you are not 100% sure that your build has correctly been received (and is valid) by Apple but in the other hand it has 2 great advantages:
    * It consumes less Codemagic free minutes waiting for an external response that is not really part of the build itself
    * It is the only way of only using an [Application Specific Password](https://docs.fastlane.tools/actions/upload_to_testflight/#use-an-application-specific-password-to-upload) to push your app. Otherwise you would need 2FA with a short-lived session token, and you don't want that on your CI. Really, believe me 🙄

# 🤖 Android setup <a name="android-setup"></a>

Codemagic configuration for Android deployment is pretty straightforward and is rather well described in code signing documentations of both [Flutter](https://flutter.dev/docs/deployment/android#signing-the-app) and [Codemagic](https://docs.codemagic.io/code-signing/android-code-signing/)

You will need to create a few things:

1. Create a service account key to interact with [Google Play Console](https://play.google.com/console)
2. Create a keystore for Android app signing
3. Configure Android project to use code signing for release mode, with appropriate keystore
4. Locally generate a signed Android application and upload this first release to Google Play Console

## 1. Google Play service account key  <a name="google-play-service-account-key"></a>

Let's start by creating the service account key.
These steps are very well described in [Codemagic documentation](https://docs.fastlane.tools/actions/supply/#setup) (in Google Play section).

This variable is going to be translated into Android `google-play-store.json` by our `codemagic/post-clone.sh` script:

```shell
GOOGLE_PLAY_STORE_JSON_KEY_PATH#"$FCI_BUILD_DIR/android/app/google-play-store.json"
echo $GOOGLE_PLAY_STORE_JSON_BASE64 | base64 --decode > $GOOGLE_PLAY_STORE_JSON_KEY_PATH
```

...which will used by Fastlane further down the road when deploying our application to Google Play Console.

⚠️ Only the app owner of Google Play Console project is able to create a service account key, make sure you ask this person to create (and send you) the JSON key !

We now need to create a file named `android/fastlane/Appfile` to give Fastslane our Google Play service key:

```shell
json_key_file("app/google-play-store.json")
package_name("com.company.awesome_app")
```

Of course, you should put your Android package name here (lowercase letters and underscores only !)

ℹ️ the actual JSON key will be populated later on by `post-clone.sh` script

## 2. Keystore

Generate a keystore for current project:


```shell
keytool -genkey -v -keystore your_app.jks -alias your_app_key -keyalg RSA -keysize 2048 -validity 10000 -storetype JKS
```

You will prompted for a password.

⚠️ Don't forget to securely store the password and share it with your team if they need to sign the app using the keystore (e.g. for CI builds). 🔐

ℹ️ `-storetype JKS` option is required for Java 9+ otherwise you will not be prompted for alias key/password and the keystore will not be compatible for code signing anyway...

Now that you have an Android keystore, you need to encode your keystore to base64.

```shell
base64 -i your_app.jks
```

## 3. Project configuration

Now that we have created util stuff needed to sign and publish our app to Google Play, we only need to tweak our configuration to reference info.

First open `codemagic.yaml` and define the following variables:

* `GOOGLE_PLAY_STORE_JSON_BASE64` : _\[Codemagic-encrypted\]_ - base64 encoded version of JSON key downloaded from Google Play Console in step 1 above.
* `ANDROID_KEYSTORE` : _\[Codemagic-encrypted\]_ - base64 encoded version of the keystore generated in step 2 above.
* `ANDROID_KEYSTORE_PASSWORD` : _\[Codemagic-encrypted\]_ - password of the generated store
* `ANDROID_KEY_ALIAS` : _\[Codemagic-encrypted\]_ - alias of the generated key
* `ANDROID_KEY_PASSWORD` : _\[Codemagic-encrypted\]_ - password of the generated key alias

⚠️ Variables that are marked as _\[Codemagic-encrypted\]_ must be encrypted using Codemagic user interface.
Go to `any project` > `configuration wheel` > `Encrypt environment variable` and paste the variable you want to encrypt, and copy it back to `codemagic.yaml`.
Keep the `Encrypted(VAR)` around your variable !

These variables are going to be translated into Android `key.properties` by our `codemagic/post-clone.sh` script:

```shell
echo $ANDROID_KEYSTORE | base64 --decode > /tmp/keystore.keystore
cat >> "$FCI_BUILD_DIR/android/key.properties" <<EOF
storePassword#$ANDROID_KEYSTORE_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEY_PASSWORD
storeFile=/tmp/keystore.keystore
EOF
```

Finally, we can configure our `android/app/build.gradle` to sign our app using provided keystore. First add these lines above `android` tag:

```shell
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
```

These lines tell Android to read keystore properties from our specific file. We can now add signing configs to the build (replace existing lines with these new ones):

```shell


signingConfigs {
    release {
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
        storePassword keystoreProperties['storePassword']
    }
}

buildTypes {
    release {
        signingConfig signingConfigs.release
    }
}
```

You're ready to sign your Android app !

## 4. Signed release & first upload

In order to push automated releases of your Android app, you need to create a first release manually on the [Google Play Console](https://play.google.com/console/) interface and upload a first signed bundle/apk.

You can follow these steps:

1. Add a file named `/android/key.properties` with the following content:

```shell
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
keyPassword=$ANDROID_KEY_PASSWORD
storeFile=/tmp/keystore.keystore
```

Obvisouly you need to replace the values of each property with the actual value you generated (same values as you set for corresponding environment variable in _Project Configuration_ section).

2. Make sure `/android/key.properties` is excluded in your `/android/.gitignore`. Flutter does this by default when creating the app, you should see the following line:
```shell
key.properties
```
If you don't have this line, obviously add it, it is crucial that you do not push secrets to your distant Git repository !
3. Build and package the application with signing enabled (release mode):

```shell
flutter build appbundle --build-number 1 --build-name 0.0.1 --release
```

Once the signed application is ready, it's time to upload it to Google Play Console: create a new release under "Internal tests" and drop your appbundle there, you should be able to continue with your brand new release.

Once your first release is accessible for internal tests, you can now upload automated releases through CI ! Yeah 🎉

⚠️ Make sure you filled all necessary information on Google Play Console for internal tests (Dashboard will tell you what steps you need to complete first).

# 🍏📱 iOS setup <a name="ios-setup"></a>

Codemagic configuration for iOS deployment can be rather complicated.
To avoid handling certificates and provisioning profiles ourselves, we'll be using Fastlane and [match](https://docs.fastlane.tools/actions/match/) for [code signing](https://codesigning.guide/), as explained in [Prerequisites](#prerequisites).

First make sure you use latest Fastlane version:

```shell
sudo gem install fastlane
```

Also create an empty Git repository called `certificates` somewhere in your Gitlab/Github/Bitbucket. We can no run a few commands to setup codesigning for `iOS` so make sure you are in `ios` subdirectory:

```shell
cd ios/
```

Time to init match to point to our certificate repository:

```shell
match init
> Select git
> URL of the Git repo:
git@gitlab.com:your-company/your-app-group-folder/certificates.git
```

This should generate a Matchfile in `.fastlane/Matchfile` with the following content:

```shell
git_url("git@gitlab.com:your-company/your-app-group-folder/certificates.git")

storage_mode("git")

type("development") # The default type, can be: appstore, adhoc, enterprise or development

# app_identifier(["tools.fastlane.app", "tools.fastlane.app2"])
# username("user@fastlane.tools") # Your Apple Developer Portal username

# For all available options run `fastlane match --help`
# Remove the # in the beginning of the line to enable the other options

# The docs are available on https://docs.fastlane.tools/actions/match
```

⚠️ Use a SSH url for the repository (rather than the HTTPS url) as we will use an SSH key later to enable Codemagic fetching our repository.

Now we have to create `ios/fatslane/Appfile` to fill out a few needed information for Fastlane (and match too):

```shell
app_identifier("com.company.awesomeApp") # The bundle identifier of your app
apple_id("apple-technical@your-company.com") # Your Apple email address for the technical account

team_id("123ABC1DE") # Developer Portal Team ID
itc_team_id("123456789") # App Store Connect Team ID
```

ℹ️ To easily output these variables you can run `fastlane produce` and you should get all these variables:
* `app_identifier` is your bundle id as created in [Prerequisites - Config](#prerequisites-config) and can be found in your [Apple identifier](https://developer.apple.com/account/resources/identifiers/bundleId
* `apple_id` is the technical account email address (e.g. apple-technical@your-company.com)
* `team_id` is your Developer portal team ID and can be found in your [Apple identifier](https://developer.apple.com/account/resources/identifiers/bundleId)
* `itc_team_id` is an integer team id derived from above `team_id`

Once all variables are set, we can use match to create new certificates and profiles.
Please make sure that the Apple account you set in `apple_id` above has "App manager" permissions and not just "Developer" or you won't have permissions to create certificates and profiles.

Let's run the command to generate everything:

```shell
fastlane match appstore
```

Enter password for `apple-technical@your-company.com`.

If the command executed successfully you should now have new, generated [certificates](https://developer.apple.com/account/resources/certificates/list) and [profiles]https://developer.apple.com/account/resources/profiles/list).
Also these certificates should now be stored encrypted into your Git certificates repository.

⚠️ Don't forget to securely store `ios keychain` and `match` passwords and share them with your team if they need to regenerate certificates ! 🔐

Now that you have generated profile and certificate, it's time to complete [Codemagic workflow](codemagic-setup) by setting all environment variables such as [Fastlane environment variables](https://docs.fastlane.tools/best-practices/continuous-integration/#environment-variables-to-set):

* `BUILD_NAME` : The version that you wish to deploy, in semantic versioning notation (e.g. 1.2.3, or more generally X.Y.Z)
* `IOS_APP_ID` : the app identifier as set in [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list).
  * Example: com.company.awesomeApp
* `APPLE_DEVELOPER_TEAM_ID` : Apple Developer Team Id (e.g. 123ABC4DE)
* `FASTLANE_EMAIL` : Apple Developer account email (e.g. apple-technical@your-company.com)
* `FASTLANE_PASSWORD` : Apple Developer account password
* `MATCH_PASSWORD` : _\[Codemagic-encrypted\]_ - Password used when generating `fastlane match appstore` above using `match`.
* `SSH_KEY_FOR_FASTLANE_MATCH_BASE64` : _\[Codemagic-encrypted\]_ - Base64 encoded private SSH key of Codemagic user for Gitlab. You can do the following to generate SSH keys :
    * Generate a new SSH key pair : `ssh-keygen -t ed25519 -C "SSH key description"`
    * Copy public key and add it under [Gitlab's SSH keys](https://gitlab.com/-/profile/keys) for Codemagic user.
    * Copy private key and encrypt it using Codemagic (see note below)
    * Delete SSH key from your local computer
    * If you ever need to see the key... you can't, just revoke the old one and generate a new pair !
* `FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD` : _\[Codemagic-encrypted\]_ - Apple Application specific password as described in [Fastlane documentation](https://docs.fastlane.tools/best-practices/continuous-integration/#application-specific-passwords). You can follow these steps:
    * Go to [Apple manage account page](http://appleid.apple.com/account/manage) with your Apple account corresponding to `FASTLANE_EMAIL`.
    * Generate a new application specific password
    * Encrypt the password using Codemagic (see note below)

ℹ️ You can run `fastlane produce` to output some of these information (app_identifier, team_id, itc_team_id).

⚠️ Variables that are marked as _\[Codemagic-encrypted\]_ must be encrypted using Codemagic user interface.
Go to `any project` > ⚙️ > `Encrypt environment variable` and paste the variable you want to encrypt, and copy it back to `codemagic.yaml`.
Keep the `Encrypted(VAR)` around your variable !

## iOS Project configuration

Just a few more things we need to setup:

* In `ios/Runner.xcodeproj/project.pbxproj`, set all `DEVELOPMENT_TEAM` variables to your actual Apple Development team (must be the same as `APPLE_DEVELOPER_TEAM_ID` env var above). If development team variables do not appear in `ios/Runner.xcodeproj/project.pbxproj` you can setup manually by doing following steps:
    * Open Xcode
    * Select Runner in left pannel
    * In central panel select Targets > Runner
    * Open "Signing & Capabilities" tab
    * Under "All" sub-tab, disable automatic code siging and select your team from the appropriate field.

If no team is found you can also import a provisioning profile after downloading it from [Apple profiles](https://developer.apple.com/account/resources/profiles)
* In `ios/Runner/Runner.entitlements` (create the file if you don't have it), setup required [Apple entitlements](https://developer.apple.com/documentation/bundleresources/entitlements) for your application.
    * If you don't use any entitlements, you should still declare it by leaving the file with an empty list:
    ```xml
      <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
      <plist version="1.0">
          <dict/>
      </plist>
    ```

Phew, you should be all set for iOS automated deployment 🎉

# Conclusion

As you can see setting up CI/CD automation for Flutter projects to go from the ground up to deployed beta versions of both Android and iOS applications requires a bit of patience, but in the end once you understand the various components interacting with each other, it's almost magic ! 🧙‍♀️

My advice : take the time to set up CI/CD when your Flutter project starts, you will be grateful to have it all automated when comes the mandatory rush of your project 🙂