Publish your flutter app with GitLab CI automatically
Posted on December 12, 2020 • 6 minutes • 1102 words
Publishing your app by hand every time is quite a tedious task. It is also error prone, not fun and this leads to you not doing it as often as you should. Because small, frequent releases are good.
We will change this now by creating a GitLab CI pipeline that lets you deliver your flutter app – or any android app really – automatically to Google Play.
I assume you already have the following:
- A successfully building flutter app you want to publish
- A Google Play developer account
- Your code is hosted on an instance of GitLab
- Gradle 6.7.+
- Android Gradle plugin 4.1.+
Signing config
First, you have to sign your app. There are two ways to sign your app.
With a signing key, or with an upload key. Read more about that here. We will use the upload key and leave the actual signing to Google, as it has some benefits.
Create a new keystore with this command. This is going to be your upload key that is used to verify to Google that the artifact really comes from you. I’m using linux so your windows paths will look differently. The keytool comes with your JDK.
keytool -genkey -v -keystore play-upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key
The play-upload-keystore.jks
goes into the android
folder of your flutter project. But make sure to never commit it! I added this to my .gitignore
file.
After that we have three values we need to remember.
KEYSTORE_PASSWORD – pretty self explanatory
KEY_ALIAS – this is specified in the command above: „key“
KEY_PASSWORD – the password for the key inside the keystore
Now we need these values in our gradle scripts. Open android/app/build.gradle
and add a release signingConfig in the android block.
signingConfigs {
release {
storeFile file("../play-upload-keystore.jks")
storePassword KEYSTORE_PASSWORD
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
}
}
Now make sure that this config is used when you build your app, by setting it in your release buildType.
buildTypes {
release {
signingConfig signingConfigs.release
}
}
The last part to make this work is setting the three variables we remembered earlier.
We don’t want to store them in the workspace and accidentally commit them to our repository, so we create a file gradle.properties
in a directory called .gradle
in our user’s home directory and fill it with our values. Gradle reads this file by default and makes all the values available to our scripts.
KEYSTORE_PASSWORD=<your keystore password>
KEY_ALIAS=key
KEY_PASSWORD=<your key password>
Note: The official docs say, you should place such a file in your project directory, but then you have to add extra code to your gradle script to load this file which I don’t like. Also since we won’t be doing the signing on our own machine from now on anymore, it won’t conflict with your other projects.
The signing is now working. Run the following to create a signed appbundle:
flutter build appbundle
Publish your flutter appbundle
Open android/app/build.gradle again and include the Gradle Play Publisher as a plugin at the top of the file, like so:
plugins {
id 'com.github.triplet.play' version '3.0.0'
}
This plugin needs a bit of configuration. You can read about all of the available options in the documentation of the GPP.
play {
serviceAccountCredentials.set(file("../play-api-private-key.json"))
track.set("beta") // internal/alpha/beta/production
userFraction.set(1d)
artifactDir.set(file("../../build/app/outputs/bundle/release"))
}
How to get the file play-api-private-key.json
that you have to put into your android folder is also described in their documentation, so I will not write this here again. Also make sure to never commit this file!
Now you could publish your appbundle by executing the gradle task publishBundle
. But we’re not here to do this manually, are we?
Automate with a GitLab CI pipeline
Just copy this in a file called .gitlab-ci.yml
in the root of your flutter app and you’re done.
image: cirrusci/flutter:1.22.4
stages:
- build
before_script:
- export GRADLE_USER_HOME=$(pwd)/android/.gradle
- mkdir -p ${GRADLE_USER_HOME}
- echo ${GRADLE_PROPS} | base64 -d > ${GRADLE_USER_HOME}/gradle.properties
- chmod +x android/gradlew
after_script:
- rm -f ${GRADLE_USER_HOME}/gradle.properties
build-debug:
stage: build
only:
- branches
script:
- flutter build appbundle --debug
publish-play:
stage: build
only:
- /^play-\d+$/
script:
- cd android
- export BUILD_NUMBER=`echo ${CI_COMMIT_REF_NAME} | grep -oP "(?<=play-)(\d+)"`
- echo ${PLAY_KEY} > play-api-private-key.json
- echo ${KEYSTORE} | base64 -d > play-upload-keystore.jks
- flutter build appbundle --build-number ${BUILD_NUMBER} --build-name ${CI_COMMIT_SHORT_SHA}
- cp ../build/app/outputs/mapping/release/mapping.txt ../build/app/outputs/bundle/release/
- ./gradlew publishBundle
after_script:
- rm -f android/play-api-private-key.json android/play-upload-keystore.jks
artifacts:
paths:
- build/app/outputs/bundle/release/*
Not quite.
We have to somehow get our secret values (passwords, keystore etc.) into out pipeline.
We do this by adding them as variables in GitLab > Settings > CI / CD > Variables
.
GitLab now has the ability to save variables as files in a temporary directory, but I do it the old fashioned way, it has no disadvantages in this scenario.
So take your play-api-private-key.json
and store it in PLAY_KEY
. For the play-upload-keystore.jks
and gradle.properties
we have to encode them as base64 before storing them in KEYSTORE
and GRADLE_PROPS
so that Gitlab doesn’t mess with them like removing line breaks etc.
Encoding these files in base64 can be done as follows:
cat ~/.gradle/gradle.properties | base64 -w0
The parameter -w0
prevents line wrapping in the base64 output, which is important. Otherwise it can not be decoded correctly in the pipeline.
Lastly you have to commit the file gradlew
in your android directory. This file is on your .gitignore
list as per flutter default. There are mixed opinions as to whether to commit this file to your repository or not. In this case it makes sense and the task at hand easier, because we don’t have to worry about providing an installation of gradle in the dockerimage we use.
You can force to add ignored files to your git stage with the --force
parameter.
git add -f android/gradlew
Executing the pipeline
The way this pipeline works is it runs flutter build appbundle --debug
on every push to every branch to ensure the app can be built successfully.
To create and publish a release to Google Play, you have to push a git tag pointing to the commit you want to be built and published.
The tag has to be in the format play-<versionCode>
(eg. play-123). The versionCode
will be built into the app and has to be an integer that is incremented for each new release and can not get smaller as it will otherwise be rejected by Google Play.
Congratulations, upon running your pipeline you should see the following output:
Starting App Bundle upload: /builds/uberflieger.media/android/vanfinder/build/app/outputs/bundle/release/app-release.aab
Uploading App Bundle: 55% complete
App Bundle upload complete
Starting mapping file upload: /builds/uberflieger.media/android/vanfinder/build/app/outputs/bundle/release/mapping.txt
Mapping file upload complete
Updating [completed] release (de.vanfinder:[2]) in track 'beta'