visit
You can do it manually.
It’s not hard or time-consuming to create a signed APK or Bundle in Android Studio, upload it to Google Play and promote it through the different testing tracks up to production. I did it for many years. I thought the time automation saves won’t compensate for the effort to set up a working pipeline. I was wrong. Automation doesn’t just save time, it also makes the process more reliable, less error-prone (to human error) and encourages to deploy/publish more often. In general, the development cycle is sped up, not just when it comes to bug fixes but also feature releases. Why wait to bundle a big release when features can be pushed out to the customer by a simple pr/merge?You can use <insert your SAAS CI/CD solution here>.
There are many cloud based CI/CD solutions like , or to name just a few. I’m sure they are all great and integrate with your preferred Git provider but it’s another tool to integrate (while Bitbucket Pipeline is obviously tightly integrated already) and they seem to be sledge hammers for the requirements I had which are:Requirements
The Gradle build needs to be configured to include a that reads the secrets from environment variables (or the gradle.properties file in your ~/.gradle folder).
If you already have one then you can skip this chapter.
For a local build the location of the keystore, the keystore password, the key alias and the key password will be configured in your ~/.gradle/gradle.properties file.
If you don’t have a ~/.gradle/gradle.properties file, please create one and add these four parameters (the bold part needs to be configured to fit your setup):
KEYSTORE_FILE=/path to the keystore file/playstore.keystore
KEYSTORE_PASSWORD=keystore password
KEYSTORE_KEY_ALIAS=key alias
KEYSTORE_KEY_PASSWORD=key password
Note: don’t use ~ for your home directory but use absolute paths. ~ works in a shell context but not with Gradle, Gradle Play Publisher and Bitbucket Pipeline.
Create a signing config in your app’s gradle.build file:signingConfigs {
release {
storeFile file(KEYSTORE_FILE)
storePassword KEYSTORE_PASSWORD
keyAlias KEYSTORE_KEY_ALIAS
keyPassword KEYSTORE_KEY_PASSWORD
}
}
buildTypes {
debug {
// debug build type configuration ...
}
release {
// release build type configuration ...
signingConfig signingConfigs.release
}
}
If the signing configuration is correct then the following command should run and create one or more aab files in your build/outputs/bundle folder:
./gradlew bundleRelease
One of the requirements is the auto-increment of build numbers / versionCode. We will use Bitbucket’s $ to set an environment variable that defines the versionCode. In order to process this environment variable, change your build.gradle file from:
versionCode 124
versionCode project.hasProperty('BUILD_NUMBER') ? project['BUILD_NUMBER'].toInteger() : 124
Last but not least we need to set the initial value for $ as it needs to be higher than the last used versionCode. Please follow this article to do so: .
plugins {
id 'com.android.application'
id 'com.github.triplet.play' version '3.3.0'
// other plugins...
}
android { ... }
play {
serviceAccountCredentials = file(GOOGLE_PLAY_API_KEY)
}
GOOGLE_PLAY_API_KEY=/path to the api key file/google-play-api-key.json
./gradlew bootstrap
After going through that article you should now have a bitbucket-pipelines.yml file in your app’s root directory.
What we want now is to create this specific pipeline:It’s easy to define the three values for KEYSTORE_PASSWORD, KEYSTORE_KEY_ALIAS and KEYSTORE_KEY_PASSWORD since they are just text values. To do so go to the “Repository settings” and scroll down to “Repository variables”. Enter all three variables with the correct values:
To store the KEYSTORE_FILE and the GOOGLE_PLAY_API_KEY in a repository variable we encode the files with base64. The build pipeline will decode the text and recreate the original files.
Run the following commands to encode the two files:base64 google-play-api-key.json
base64 playstore.keystore
Copy the base64 strings and create repository variables in Bitbucket. The strings should look somewhat like this (much longer though): YmFzZTY0IGdvb2dsZS1wbGF5LWFwaS1rZXkuanNvbg==
I also created two variables KEYSTORE_FILE and GOOGLE_PLAY_API_KEY to define the files names used for the decoded secrets:
Now we’re ready to define the first step of the actual pipeline in the bitbucket-pipelines.yml file.
image: androidsdk/android-30
pipelines:
branches:
master:
- step:
name: Create keystore and API key
script:
# create the keystore file and the google play api key file
- mkdir keys
- echo $KEYSTORE_FILE_BASE64 | base64 --decode > keys/$KEYSTORE_FILE
- echo $GOOGLE_PLAY_API_KEY_BASE64 | base64 --decode > keys/$GOOGLE_PLAY_API_KEY
artifacts:
- keys/**
We use androidsdk/android-30 as the Docker image. That image has all the tools to build apps up to API 30 so no “manual” installation of build tools and writing code to accept the licenses.
In our case we want to build the master branch upon commit, hence the:branches:
master:
mkdir keys
echo $KEYSTORE_FILE_BASE64 | base64 --decode > keys/$KEYSTORE_FILE
echo $GOOGLE_PLAY_API_KEY_BASE64 | base64 --decode > keys/$GOOGLE_PLAY_API_KEY
artifacts:
- keys/**
When run locally, Gradle reads the build arguments from the ~/.gradle/gradle.properties file. When run in the build pipeline we need to pass in the parameters as environment variables like so:
./gradlew -PKEYSTORE_FILE=../keys/$KEYSTORE_FILE
-PKEYSTORE_PASSWORD=$KEYSTORE_PASSWORD
-PKEY_ALIAS=$KEY_ALIAS
-PKEY_PASSWORD=$KEY_PASSWORD
-PGOOGLE_PLAY_API_KEY=../keys/$GOOGLE_PLAY_API_KEY
test
PKEYSTORE_FILE creates an argument for Gradle with the name KEYSTORE_FILE and the value ../keys/$KEYSTORE_FILE with $KEYSTORE_FILE referencing the repository variable we defined earlier (translates to ../keys/playstore.keystore).
Putting everything together we get this step:step:
name: Run unit tests
caches:
- gradle
script:
- export GRADLE_OPTS='-XX:+UseG1GC -XX:MaxGCPauseMillis=1000 -Dorg.gradle.jvmargs="-Xmx2048m -XX:MaxPermSize=1024m -XX:ReservedCodeCacheSize=440m -XX:+UseCompressedOops -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" -Dorg.gradle.parallel=false -Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true'
- "./gradlew -PKEYSTORE_FILE=../keys/$KEYSTORE_FILE
-PKEYSTORE_PASSWORD=$KEYSTORE_PASSWORD
-PKEY_ALIAS=$KEY_ALIAS
-PKEY_PASSWORD=$KEY_PASSWORD
-PGOOGLE_PLAY_API_KEY=../keys/$GOOGLE_PLAY_API_KEY
test"
artifacts:
- app/build/outputs/**
- app/build/reports/**
Gradle build daemon disappeared unexpectedly (it may have been killed or may have crashed)I’m sure there’s a better solution for this but for now the org.gradle.daemon is good enough for me.
Building the app and deploying it to Google Play is simple with the Gradle Play Publisher plugin properly configured. The tasks publishFreeReleaseBundle and publishProReleaseBundle (with a Free and a Pro flavor of the app) will do all the heavy lifting. The pipeline step is:
- step:
name: Build & deploy
caches:
- gradle
script:
- export GRADLE_OPTS='-XX:+UseG1GC -XX:MaxGCPauseMillis=1000 -Dorg.gradle.jvmargs="-Xmx2048m -XX:MaxPermSize=1024m -XX:ReservedCodeCacheSize=440m -XX:+UseCompressedOops -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" -Dorg.gradle.parallel=false -Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true'
- "./gradlew -PKEYSTORE_FILE=../keys/$KEYSTORE_FILE
-PKEYSTORE_PASSWORD=$KEYSTORE_PASSWORD
-PKEY_ALIAS=$KEY_ALIAS
-PKEY_PASSWORD=$KEY_PASSWORD
-PGOOGLE_PLAY_API_KEY=../keys/$GOOGLE_PLAY_API_KEY
-PBUILD_NUMBER=$BITBUCKET_BUILD_NUMBER
clean :app:publishFreeReleaseBundle :app:publishProReleaseBundle"
artifacts:
- app/build/outputs/
- parallel:
- step:
name: Promote free version
caches:
- gradle
trigger: manual
script:
- export GRADLE_OPTS='-XX:+UseG1GC -XX:MaxGCPauseMillis=1000 -Dorg.gradle.jvmargs="-Xmx2048m -XX:MaxPermSize=1024m -XX:ReservedCodeCacheSize=440m -XX:+UseCompressedOops -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" -Dorg.gradle.parallel=false -Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true'
- "./gradlew -PKEYSTORE_FILE=../keys/$KEYSTORE_FILE
-PKEYSTORE_PASSWORD=$KEYSTORE_PASSWORD
-PKEY_ALIAS=$KEY_ALIAS
-PKEY_PASSWORD=$KEY_PASSWORD
-PGOOGLE_PLAY_API_KEY=../keys/$GOOGLE_PLAY_API_KEY
-PBUILD_NUMBER=$BITBUCKET_BUILD_NUMBER
promoteFreeReleaseArtifact --from-track internal --promote-track production --release-status completed"
- step:
name: Promote pro version
caches:
- gradle
trigger: manual
script:
- export GRADLE_OPTS='-XX:+UseG1GC -XX:MaxGCPauseMillis=1000 -Dorg.gradle.jvmargs="-Xmx2048m -XX:MaxPermSize=1024m -XX:ReservedCodeCacheSize=440m -XX:+UseCompressedOops -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8" -Dorg.gradle.parallel=false -Dorg.gradle.daemon=false -Dorg.gradle.configureondemand=true'
- "./gradlew -PKEYSTORE_FILE=../keys/$KEYSTORE_FILE
-PKEYSTORE_PASSWORD=$KEYSTORE_PASSWORD
-PKEY_ALIAS=$KEY_ALIAS
-PKEY_PASSWORD=$KEY_PASSWORD
-PGOOGLE_PLAY_API_KEY=../keys/$GOOGLE_PLAY_API_KEY
-PBUILD_NUMBER=$BITBUCKET_BUILD_NUMBER
promoteProReleaseArtifact --from-track internal --promote-track production --release-status inProgress --user-fraction .5"
promoteFreeReleaseArtifact --from-track internal --promote-track production --release-status completed
promoteProReleaseArtifact --from-track internal --promote-track production --release-status inProgress --user-fraction .5
trigger: manual
For reference here’s the complete bitbucket-pipelines.yml file: .
Happy coding!Previously published behind a paywall: //medium.com/nerd-for-tech/ci-cd-for-android-using-bitbucket-pipelines-and-gradle-play-publisher-f00d6047ecb5