Our story with Flutter and Gitlab-Ci

Fabian Finke
15 min readJul 26, 2019

--

As a developer at XETICS I am also responsible for our CI/CD stack which is done using Gitlab-CI. As I haven’t found any guide about using Gitlab-CI for creating a CI/CD stack which also includes building and deploying an iOS application I will simply publish our approach.

I am assuming that you already have a certain knowledge about Gitlab-CI. If not check https://docs.gitlab.com/ee/ci/

In the end, our pipeline looks like this:

Flutter CI/CD with Gitlab

You can find the complete code in my Github repository:

Prerequisites

To be able to build an iOS App you definitely need a Mac! We’re using the new Mac mini which should be powerful enough

Before we started automating this process, our App was already present in both app stores. Hence, the approach might differ if your app is not published until now.

Android Build

Let’s start with a simple CI/CD which runs all tests with coverage and builds the Android APK. As we are are using Gitlab Runner in a Docker container for the Android build, we need to create the used docker image first.

Checkout https://docs.gitlab.com/runner/install/docker.html for more information about running Gitlab Runner in a Docker container.

To increase our build time, we’ll create an image which already contains all needed stuff like Android SDK, Flutter SDK and the Java run time. Feel free to adjust it according to your needs. The image also already contains Fastlane. I’ll come to that in a later point 😉.

The image will do the following:

  1. Set Android and Flutter SDK version
  2. Install needed dependencies
  3. Download Android SDK and apply licenses
  4. Download Flutter SDK
  5. Install Fastlane
  6. Remind you to not forget to update the Flutter version on the Mac Mini :-)
FROM fedora:latestENV ANDROID_COMPILE_SDK=29
ENV ANDROID_BUILD_TOOLS=29.0.0
ENV ANDROID_SDK_TOOLS=3859397
ENV FLUTTER_CHANNEL=stable
ENV FLUTTER_VERSION=1.5.4-hotfix.2-${FLUTTER_CHANNEL}
# install some needed dependencies
# -y will auto confirm the command
# to keep the container as small as possible, clean all will delete
# the downloaded dependencies
RUN dnf update -y \
&& dnf install -y wget tar unzip ruby ruby-devel make autoconf automake redhat-rpm-config lcov\
gcc gcc-c++ libstdc++.i686 java-1.8.0-openjdk-devel xz git mesa-libGL mesa-libGLU rubygems\
&& dnf clean all
ENV JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk
ENV PATH=$PATH:$JAVA_HOME
# Download Android SDK
# echo "y" will auto confirm license agreement
RUN wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip \
&& unzip android-sdk.zip -d /opt/android-sdk-linux/ \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "platform-tools" \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "extras;android;m2repository" \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "extras;google;google_play_services" \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "extras;google;m2repository" \
&& yes | /opt/android-sdk-linux/tools/bin/sdkmanager --licenses || echo "Failed" \
&& rm android-sdk.zip

# make SDK tools available for CI
ENV ANDROID_HOME=/opt/android-sdk-linux
ENV PATH=$PATH:/opt/android-sdk-linux/platform-tools/
# Download Flutter SDKRUN wget --quiet --output-document=flutter.tar.xz https://storage.googleapis.com/flutter_infra/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_v${FLUTTER_VERSION}.tar.xz \
&& tar xf flutter.tar.xz -C /opt \
&& rm flutter.tar.xz
# make Flutter available for CI
ENV PATH=$PATH:/opt/flutter/bin
# if you want to autoincrease Flutter version number the next three lines are helpful
ENV PATH=$PATH:/opt/flutter/bin/cache/dart-sdk/bin
RUN flutter pub global activate pubspec_version
ENV PATH="$PATH":"/opt/flutter/.pub-cache/bin"
RUN echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "emulator" \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "system-images;android-18;google_apis;x86" \
&& echo "y" | /opt/android-sdk-linux/tools/bin/sdkmanager "system-images;android-${ANDROID_COMPILE_SDK};google_apis_playstore;x86"
# install Fastlane

RUN gem install fastlane
RUN dnf update -y \
&& dnf install -y pulseaudio-libs mesa-libGL mesa-libGLES mesa-libEGL \
&& dnf clean all
RUN echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" \
&& echo "Don't forget to change the Flutter version also on the Mac mini if you've changed it here!" \
&& echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

If the Docker image fits your need, you can build it with

docker build -f Dockerfile xetics/flutter:1.5.4-stable .

I decided to tag the image with the Flutter version which is used within the container. Hence, I’ll tag it with 1.5.4-stable

iOS Build

First, unpack your new Mac 😃 . No seriously, as we need to do some preparation for the Android build, we need to do so for the iOS App. First thing is that we have to install Gitlab Runner as Shell Executor on our new Mac.

I haven’t found anything about how to use an Docker image on the Mac instead. If you know how, let me know!

Sign in with an user having admin rights and download the Runner executable:

sudo curl --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64

Next, make it executable for everyone

sudo chmod +x /usr/local/bin/gitlab-runner

Now, create a new user gitlab-ci which doesn’t need to be in the sudoers file. This one will be the user which executes the Gitlab Runner later on. As the executor is running in user mode, set the user to be auto logged in on restart.

  • Log in as gitlab-ci and register the Runner:

gitlab-runner register

  • Register your Gitlab instance:
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com ) https://gitlab.com
  • Enter the token:
Please enter the gitlab-ci token for this runner 
xxx
  • Enter some description:
Please enter the gitlab-ci description for this runner 
[hostname]my-runner
  • Enter the tags which will be associated with this Runner. We’ll tag it as ios
Please enter the gitlab-ci tags for this runner (comma separated): 
ios
  • Enter the executor:
Please enter the executor: ssh, docker+machine, docker-ssh+machine, kubernetes, docker, parallels, virtualbox, docker-ssh, shell: 
shell
  • Afterwards, install the service and start it:
cd ~  
gitlab-runner install
gitlab-runner start

Now, you’ve setup the Gitlab Runner also install:

Code signing

To be honest, this is a mess on both platforms and the most disappointing part. It took me quite some time to get it done. To get quickly over it, let’s do all the preparation in advance. If that’s done, we can just concentrate on the fun part.

Android signing

For Android, I’ve found this awesome stackoverflow post which you can easily follow. Kudos to MatPag and thanhbinh84:

After you have all your key store setup done, we will store it base64 encoded in Gitlab’s CI/CD secure environment variables:

  • Encode the keystore: base64 key.jks
  • Copy the base64 output and create a variable called PLAY_STORE_UPLOAD_KEY. I would recommend to put single quotes '' around the base64 string.
  • Save the store password as STORE_PASSWORD variable
  • Save the key password as KEY_PASSWORDvariable
  • Save the key alias as KEY_ALIAS variable

To make the deployment via Fastlane working, you need to to get API access to Google Play console. To get an api key json file, follow the steps of the Collect your Google credentials section of the Fastlane guide

Only the account owner can grant API access. So be kind to your manager 😃

  • Save the content of the key.json file as JSON_KEY_DATA variable.

iOS signing

Code signing for iOS is pretty straight forward if you use fastlane match. The disadvantage is, that you have to revoke your already existing certificates. As they have a well described guide about setting it up, I will just refer to it. Take your time and read through it completely. This is really important!

To store the certificates, I’ve just created another repository in our Gitlab instance. We’re also using a shared Apple Developer Portal account. Hence, I can recommend that you are using one too.

After you’re done, we have to add some more Gitlab environment variables:

  • Add the password of your newly created Apple Developer Portal account as FASTLANE_PASSWORD
  • Add the https git url of your certificates repo as MATCH_GIT_URL
  • Add the passphrase with which you encrypted the certificates as MATCH_PASSWORD

Summary:

Congratulations! You’re are just done with the most annoying part. Your Gitlab CI/CD environment variable should look like this now:

For higher security set the variables to protected

Fastlane

Fastlane is an open-source tool suite which automates beta deployments and releases for iOS and Android apps. With it, you can easily do all of this tasks directly in your CI pipeline.

To prepare the Flutter project, log in with your personal account on the Mac (do not use the gitlab-ci user because you have to checkout the repository ).

Before we’re going on, we need to install fastlane for our user. Run gem install fastlane or brew cask install fastlane. Please do not run these commands with sudo .

Android setup

Check out your Flutter project from Gitlab. Afterwards, open a terminal window and navigate to the android folder of the Flutter project and run fastlane init .

Enter your unique package name
You can enter the path to your key store. Although we won’t use it like that later on

If you’re stuck at $ bundle update, you probably installed fastlane with sudo. Remove it and reinstall it without admin rights.

Now, your folder structure should look like this:

Appfile

Now, it’s time to do some configuration. First, open the Appfile and check that it just contains a single line, your package name:

package_name("com.flutter.medium") # e.g. com.krausefx.app

Ensure that the package name matches the package tag in your AndroidManifest.xml as well as your applicationId in the app/build.gradle file.

Fastfile

Mostly all of the magic is done in the Fastfile . We will simply follow the tracks already provided by Google Play. Hence, after we build the APK via Flutter, we’re going to upload it once into the internal Play Store track. From there, the APK will just be promoted through the upper tracks.

Because we don’t have the need for Beta users in our company yet, the Fastfile also contains a lane which promotes the APK from Alpha to Production directly.

# This file contains the fastlane.tools configuration
# You can find the documentation at
https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#
https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#
https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
update_fastlane
default_platform(:android)platform :android do desc "Submits the APK to Google Play internal testing track"
lane :upload_to_play_store_internal do
upload_to_play_store(
track: 'internal',
apk: '../build/app/outputs/apk/release/app-release.apk',
json_key_data: ENV['JSON_KEY_DATA']
)
end
desc "Promote Internal to Alpha"
lane :promote_internal_to_alpha do
upload_to_play_store(
track: 'internal',
track_promote_to: 'alpha',
json_key_data: ENV['JSON_KEY_DATA']
)
end
desc "Promote Alpha to Beta"
lane :promote_alpha_to_beta do
upload_to_play_store(
track: 'alpha',
track_promote_to: 'beta',
json_key_data: ENV['JSON_KEY_DATA']
)
end
desc "Promote Alpha to Production"
lane :promote_alpha_to_production do
upload_to_play_store(
track: 'alpha',
track_promote_to: 'production',
json_key_data: ENV['JSON_KEY_DATA']
)
end
desc "Promote Beta to Production"
lane :promote_beta_to_production do
upload_to_play_store(
track: 'beta',
track_promote_to: 'production',
json_key_data: ENV['JSON_KEY_DATA']
)
end
end

Gemfile

The Gemfile will just contain Fastlane as our RubyGem:

source "https://rubygems.org"
gem "fastlane"

Add all, the Appfile , Fastfile and Gemfile to your VCS.

Now, let’s have a look at the iOS setup.

iOS setup

The iOS setup is done the same way we’ve already done it for Android. Hence, navigate to the ios folder inside your Flutter root and run again fastlane init . Simply choose manual setup because we’re going to setup all stuff directly. You have to press two times Enter and you’re done. Of course, I would recommend to read the stuff Fastlane is trying to tell you 😉

If you want to try out one of the other offers of Fastlane, be sure you execute this on the Mac.

The folder structure should look similar to the Android ones:

Appfile

Let’s start again with the Appfile It looks a bit different than its Android counterpart:

app_identifier("flutter.com.medium") # The bundle identifier of your app
apple_id("marty-mcfly@icloud.com") # Your Apple email address

itc_team_id("20151021") # App Store Connect Team ID
team_id("TEAM1985") # Developer Portal Team ID

# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile

Fastfile

For iOS we will follow the flow Apple is providing in its App Store Connect. Hence, after we‘ve build the IPA via Flutter, we‘ll upload it to Apple‘s TestFlight.

Afterwards, we will simply promote the binary to the App Store. Because of the restrictions of Apple, we can‘t upload the binary as easy as we can for Google Play. Technically, the IPA will be submitted for review. After we passed the review, our update is published in the App Store.

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#

# Uncomment the line if you want fastlane to automatically update itself
update_fastlane

default_platform(:ios)

platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
# This var is needed to work around proxy problems on our Gitlab instance. See https://github.com/fastlane/fastlane/issues/13572
ENV["DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS"] = "-t Signiant"
# build the binary
gym(
scheme: 'Release',
export_method: 'app-store',
export_options: { compileBitcode: false},
output_directory: './build',
output_name: 'app-beta'
)
# submit to TestFlight
pilot(
ipa: './build/app-beta.ipa',
app_identifier: ENV['APP_IDENTIFIER'],
skip_submission: false,
changelog: ENV['CI_COMMIT_MESSAGE'],
apple_id: ENV['APPLE_ID']
)
end
desc "Promote the beta build to App Store"
lane :submit_review do
deliver(
build_number: ENV['CI_PIPELINE_ID'],
app_identifier: ENV['APP_IDENTIFIER'],
app_version: ENV['FLUTTER_APP_VERSION'],
submit_for_review: true,
automatic_release: true,
force: true, # This skips the HTML report verification
skip_metadata: true,
skip_screenshots: true,
skip_binary_upload: true
)
end

desc "Post updates on Twitter"
lane :notify_twitter_followers do
twitter(
access_token: ENV['TWITTER_ACCESS_TOKEN'],
access_token_secret: ENV['TWITTER_ACCESS_TOKEN_SECRET'],
consumer_key: ENV['TWITTER_CONSUMER_API_KEY'],
consumer_secret: ENV['TWITTER_CONSUMER_API_SECRET_KEY'],
message: "A new version of our Value Stream Tracking App is available in the App Store: https://apps.apple.com/de/app/xetics-wertstromanalyse/id1463187782"
)
end
endGemfile

The Gemfile is exactly the same like it is for Android:

source "https://rubygems.org"

gem "fastlane"
gem "twitter"

Gitlab

Test stage

Our first stage will be pretty simple. It will run all tests with coverage and upload the generated LCOV report to Gitlab. From there, you might want to publish it to Gitlab Pages. I haven’t done it until now. Sorry for that. If you already have an approach for that, feel free to post it into the comments 😃.

stages:
- test

variables:
LC_ALL: "en_US.UTF-8"
LANG: "en_US.UTF-8"
.android_docker_image:
image: xetics/flutter:1.5.4-stable
tags:
- flutter-android
test:
extends: .android_docker_image
stage: test
script:
- flutter test --coverage
- genhtml coverage/lcov.info --output=coverage
artifacts:
paths:
- coverage/
expire_in: 5 days

The testing stage will use our previously created docker image to run all unit tests.

Package stage

From now on, our stages will run parallel. The iOS related one on the Mac, the Android one on our Docker image.

stages:
- test
- package
....android_key_store:
extends: .android_docker_image
before_script:
# this will fetch the base64 encoded key store from our CI variables, decode it and place it underneath the path specified in build.gradle
- echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > android/key.jks
only:
- master
build_android:
stage: package
extends: .android_key_store
script: flutter build apk --release --build-number=$CI_PIPELINE_ID
artifacts:
paths:
- build/app/outputs/apk/release/app-release.apk
expire_in: 1 day

build_ios:
stage: package
script:
# although fastlane also is capable of building the ipa, we are doing it with flutter to ensure it has the same versioning like the Android app
- flutter build ios --release --build-number=$CI_PIPELINE_ID
artifacts:
untracked: true
expire_in: 1 day
tags:
- ios
only:
- master

We’re using flutter build for creating both binaries to ensure that they’ll use the same versioning.

For Android, we need to grab the already prepared key store to sign the APK for release.

For iOS, the signing stuff will at first play a role in the next stage. But here, I’m simply uploading everything which is not under VCS control to Gitlab to reuse it in the next stage. This is more a dirty workaround as I wasn’t able to figure out what stuff I exactly need and what not. If you know, please share your knowledge 😃

Test deployment

As the binaries are build now, it’s time to upload them to the stores. This will be done in our test_deployment stage

stages:
- test
- package
- test_deployment
....setup_fastlane_android:
extends: .android_key_store
before_script:
- cd android/
# because the Docker container runs as root currently, we won't do any user-install. Otherwise, it will fail with
# $ gem install --user-install bundler
# ERROR: Use --install-dir or --user-install but not both
- gem install bundler
- bundle install
.setup_fastlane_ios:
before_script:
- cd ios/
- gem install --user-install bundler
- bundle install --path vendor/bundle
- bundle exec fastlane match
- export FLUTTER_APP_VERSION=$(pubver -d ../. get)
tags:
- ios
only:
- master
...ios_testflight_beta_deployment:
stage: test_deployment
extends: .setup_fastlane_ios
dependencies:
- build_ios
script: bundle exec fastlane beta

android_play_store_internal_and_alpha_deployment:
stage: test_deployment
extends: .setup_fastlane_android
dependencies:
- build_android
script:
- bundle exec fastlane upload_to_play_store_internal
- bundle exec fastlane promote_internal_to_alpha

For iOS we need to sign the code now. Because we already setup Fastlane match before, running bundle exec fastlane match is enough. In addition, we need to set displayed app version string (like 1.0.0). Therefore, we will simply use pubver command from pubspec_version you should have installed before. The environment variable will be read by Fastlane afterwards.

Productive deployment

The last missing stage is of course making your App available to the outside.

stages:
- test
- package
- test_deployment
- productive_deployment
...app_store_submit_to_review:
stage: productive_deployment
extends: .setup_fastlane_ios
dependencies:
- ios_testflight_beta_deployment
script: bundle exec fastlane submit_review
when: manual
allow_failure: false

android_play_store_productive_deployment:
stage: productive_deployment
extends: .setup_fastlane_android
dependencies:
- android_play_store_internal_and_alpha_deployment
script: bundle exec fastlane promote_alpha_to_production
when: manual
allow_failure: false

Because we don’t want to auto push the update to the stores, the stages are marked as manual . Hence, after our QA give the final go, someone can simply start the release via a single click in Gitlab.

Additional stuff

Twitter

You may have already seen in the picture in the beginning, that we have an additional pipeline after the release was published. There, we’re informing our users via Twitter that a new release is available. To achieve that the following adjustments have to be done:

  • Add gem "twitter" to ios/Gemfile and android/Gemfile
  • Add new lane to both ios/fastlane/Fastfile and android/fastlane/Fastfile :
desc "Post updates on Twitter"
lane :notify_twitter_followers do
twitter(
access_token: ENV['TWITTER_ACCESS_TOKEN'],
access_token_secret: ENV['TWITTER_ACCESS_TOKEN_SECRET'],
consumer_key: ENV['TWITTER_CONSUMER_API_KEY'],
consumer_secret: ENV['TWITTER_CONSUMER_API_SECRET_KEY'],
message: "<Your tweet>"
)
end
  • Add new stage in .gitlab-ci.yml :
tweet_about_android_updates:
stage: notification
extends: .setup_fastlane_android
dependencies:
- android_play_store_productive_deployment
script: bundle exec fastlane notify_twitter_followers
when: on_success

tweet_about_ios_updates:
stage: notification
extends: .setup_fastlane_ios
dependencies:
- app_store_submit_to_review
script: bundle exec fastlane notify_twitter_followers
when: on_success

Change logs

To fill up the stores What’s new section, we are also providing change logs for each possible release. The way how you have to provide the change logs differs from Android and iOS. Because we don’t want to maintain those things twice, we’ve created our own approach.

  • Create changelogs directly inside the root of your project
  • Create a folder for each language code you’re supporting
  • Create localizedrelease_notes.txt for each language code

For iOS we can simply take these change logs. Therefore, adjust the submit_review lane in ios/fastlane/Fastfile to also fetch the release notes:

...desc "Promote the beta build to App Store"
lane :submit_review do
deliver(
build_number: ENV['CI_PIPELINE_ID'],
app_identifier: ENV['APP_IDENTIFIER'],
app_version: ENV['FLUTTER_APP_VERSION'],
release_notes: {
'default' => File.read('./../../changelogs/en-US/release_notes.txt'),
'en-US' => File.read('./../../changelogs/en-US/release_notes.txt'),
'de-DE' => File.read('./../../changelogs/de-DE/release_notes.txt')
},
submit_for_review: true,
automatic_release: true,
force: true, # This skips the HTML report verification
skip_metadata: false,
skip_screenshots: true,
skip_binary_upload: true
)
end
...

Android is a bit more complex as it expects the release notes file to be named with the version code the app is going to be released. To achieve that a bit of ruby scripting is needed:

  • Add gem "fileutils" to android/Gemfile
  • Add dependencies to fileutils and create a language code array in android/fastlane/Fastfile
...default_platform(:android)

platform :android do

require ‘fileutils’
languages = ["de-DE", "en-US"]
...
  • Add two helper function to your Fastfile:
...def prepareProductionChangelogs(languageCode)
puts "--- Preparing productive changelogs for language code '#{languageCode}' ---"
sharedChangelogs = "./../../changelogs/#{languageCode}"
specificChangelogs = getSpecificChangelogsDirectory(languageCode)
createChangelogsFolder(specificChangelogs)
FileUtils.cp("#{sharedChangelogs}/release_notes.txt", "#{specificChangelogs}/#{ENV['CI_PIPELINE_ID']}.txt")
end
def createChangelogsFolder(changelogsDirectory)
FileUtils.mkdir_p changelogsDirectory
end
def getSpecificChangelogsDirectory(languageCode)
"./metadata/android/#{languageCode}/changelogs"
end
...
  • Adjust the lane where you are going public (beta or production)
...desc "Promote Alpha to Production"
lane :promote_alpha_to_production do
languages.each { |languageCode| prepareProductionChangelogs(languageCode)}
upload_to_play_store(
track: 'alpha',
track_promote_to: 'production',
json_key_data: ENV['JSON_KEY_DATA']
)
end
...

Internal change logs

To make the life of our QA easier, the change logs for the internal releases differs. There, we are using the last commit message which always points to the merge request of the changes.

Testflight app shows merge request as notes

For iOS add changelog: command to pilot in ios/fastlane/Fastfile:

...pilot(
ipa: './build/app-beta.ipa',
app_identifier: ENV['APP_IDENTIFIER'],
skip_submission: false,
changelog: ENV['CI_COMMIT_MESSAGE'],
apple_id: ENV['APPLE_ID']
)
...

Again, for Android a bit more work has to be done in the android/fastlane/Fastfile :

  • add another helper function:
...def prepareTestingChangelogs(languageCode)
puts "--- Preparing test changelogs for language code '#{languageCode}' ---"
specificChangelogs = getSpecificChangelogsDirectory(languageCode)
createChangelogsFolder(specificChangelogs)
File.open("#{specificChangelogs}/#{ENV['CI_PIPELINE_ID']}.txt", "w") {|file| file.write(ENV['CI_COMMIT_MESSAGE']) }
end
...
  • adjust the upload_to_play_store_intenal lane:
...desc "Submits the APK to Google Play alpha testing track"
lane :upload_to_play_store_internal do
languages.each { |languageCode| prepareTestingChangelogs(languageCode)}
upload_to_play_store(
track: 'internal',
apk: '../build/app/outputs/apk/release/app-release.apk',
json_key_data: ENV['JSON_KEY_DATA']
)
end
...

--

--