Build by types and sign Android app from Jenkins

Dec 04, 2017 DevOps Jenkins Android

In previous post we could setup Jenkins instance, Android SDK and build a basic APK. In this blog post we’ll set 2 build types and update gradle files of the app. Later we will create keystores and copy them to VM. After that we’ll finish with setting up the jenkins job to build and sign :key: different build types.

Previous blog post

Setup and build Android app from Jenkins

Step by step guide of how to setup Jenkins and Android SDK on VM and create a basic job that builds Android apps.

Steps

Gradle configurations

File: gradle.properties

Add keystore key value params

KEYSTORE=
STORE_PASSWORD=
KEY_ALIAS=
KEY_PASSWORD=

We are adding these keys and leave them empty as part of the project. The moment the project is built through Jenkins we fill and set appropriate values.

Example: gradle.properties

File: build.gradle - app level

Add 2 build types

buildTypes {
	release {
		minifyEnabled true
		shrinkResources true
		proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-release-rules.pro'
	}

	debug {
		minifyEnabled true
		shrinkResources true
		proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-debug-rules.pro'
	}
}

Having at least 2 build types is very important for every Android app. For both build types we’ll minify and call proguard with appropriate proguard configuration file.

Set the signing options

signingConfigs {
    config
}

Set signing option if gradle.properties has path to keystore:

if (KEYSTORE != "") {

    // initialize the signing
    android.signingConfigs.config.storeFile = file('../keystore/' + KEYSTORE)
    android.signingConfigs.config.storePassword = STORE_PASSWORD
    android.signingConfigs.config.keyAlias = KEY_ALIAS
    android.signingConfigs.config.keyPassword = KEY_PASSWORD

    // set this signing for both build types
    android.buildTypes.release.signingConfig = android.signingConfigs.config
    android.buildTypes.debug.signingConfig = android.signingConfigs.config

}

Explanation: If the KEYSTORE path is empty and signing options are not updated, then local keystore will be used. Usually it’s located under ~/.android/. This is what happens most of the time when we develop and build the app locally. If the path is not empty, then we’ll use it with all other signing params as password, key alias and key password.

Example: build.gradle

Keystores

Make sure to have 2 keystores for each of the build types and later sign each of the build types with different key.

Generate 2 keystores and copy to VM

We’ll generate 2 keystores:

  • debug.keystore
  • release.keystore

One will be used for release builds that will go eventually to the Google Play Store and another is for debug builds that are created from Jenkins. It’s totally not must. You can create 1 keystore for all type of builds, but from my experience it’s very useful to have 2 keystores. One of the reasons fot that - when we run manual QA, I want be sure not updating release builds with debug builds on my device. Different signatures will prevent from upgrading different build types. Another good reason is to keep relese keystore far and secure which is used only for production builds and for day to day work to use the debug one.

From Android Studio
  1. Build -> Generate Signed APK …
  2. Next -> Create New …
  3. Set: password, key alias, key password, validity years: 1000 and fill one of the fields like first and last name.
Security

One of the painful things is keeping release.keystore and credentials secured. We don’t want them be easily reached and visible. To make sure that keystore is safe we:

  1. Will never save the release.keystore part of the Git repo we use on a daily basis.
  2. Admin will manually copy the release.keystore to the Jenkins VM and no one will able to copy it and reach it.
  3. When someone decides to build release build from Jenkins, he/she will have to put credentials every single time and Jenkins will make sure to replace them in logs and everywhere with ****** (stars).

For debug.release it’s less important to secure since it’s is used internally only and can’t go public to Google Play Store anyway or overide production versions.

Copy keys once to Jenkins job folder

From your machine we copy to default dir of the user on remote VM:

scp -i ~/.ssh/google_compute_engine debug.keystore [USERNAME]@[EXTERNAL_IP_ADDRESS]:~/
scp -i ~/.ssh/google_compute_engine release.keystore [USERNAME]@[EXTERNAL_IP_ADDRESS]:~/

Create keystore folder under your jenkins job. For example, if your Jenkins job name is “build-android-jenkins” then the job folder will be: /var/lib/jenkins/workspace/build-android-jenkins

sudo mkdir /var/lib/jenkins/workspace/build-android-jenkins/keystore

Move the files keystore folder.

sudo mv debug.keystore /var/lib/jenkins/workspace/build-android-jenkins/keystore
sudo mv release.keystore /var/lib/jenkins/workspace/build-android-jenkins/keystore

Build and sign by type

We will add more parameters to the job as ‘choices option’ so we can build by types and set credentials for keystore.

We continue updating the same basic Jenkins job we have created in previous blog post.

Configure Jenkins Job

Necessary Plugins

Install these plugins: Manage Jenkins -> Manage Plugins -> Available

Add paratemers

The next thing to do is parameterize our job. We are going to add 4 params:

  1. build - Choice option param with two options: debug, release.
  2. store_password - Password param that will be used for release builds for signing.
  3. key_alias - Password param that will be used for release builds for signing.
  4. key_password - Password param that will be used for release builds for signing.

The steps
  1. Select job -> Configure

  2. Make sure that This project is parameterized is checked.

  3. Add Parameter ▼ -> Choice Parameter

    Name build
    Choices debug
    release
  4. Add Parameter ▼ -> Parameter Separator

  5. Add Parameter ▼ -> Password Parameter

    Name store_password
  6. Add Parameter ▼ -> Password Parameter

    Name key_alias
  7. Add Parameter ▼ -> Password Parameter

    Name key_password
  8. Under Build Environment -> Check: Mask passwords and regexes (and enable global passwords)

  9. Under Build we will call our build.sh script file that will do the all build job. Explained later.

    chmod +x scripts/build.sh
    ./scripts/build.sh "$branch" "$build" "$store_password" "$key_alias" "$key_password"
    
  10. Under Post-build Actions -> Archive the artifacts

    Files to archive artifacts/*.apk

    Note: You can get red warning which is fine. Don’t worry about this: “‘artifacts/*.apk’ doesn’t match anything: even ‘artifacts’ doesn’t exist”

The configuration looks like this - Click here for full screen

Execution script - build.sh

After setting the right params, we can update the execution script to build debug and release versions.

First of all create new folder in your root project and call it scripts. Then add new file and call it build.sh

The script will do next steps:

  1. Update key store values in gradle.properties file.
  2. Build debug or release APK.
  3. Move ready apk to artifacts folder.

Before I dive into the script details you can check this ready example. You can use this example as is, just make sure to rename the scripts-part-2 folder to scripts. The example here is fully functional. In the next blog posts I will add more steps like tests and lints.

Update key store values in gradle.properties file

#!/bin/bash

# get input params
branchName=$1
buildType=$2
storePass=$3
keyAlias=$4
keyPass=$5

# helper method
setProperty() {
	sed -i.bak -e "s/\($1 *= *\).*/\1$2/" ${propertiesFile}
}

propertiesFile='gradle.properties'
chmod +x ${propertiesFile}

# update key properties based on build type
if [ $buildType = 'debug' ]; then
	(setProperty "KEYSTORE" "debug.keystore")
	(setProperty "STORE_PASSWORD" "123456")
	(setProperty "KEY_ALIAS" "my_alias")
	(setProperty "KEY_PASSWORD" "123456")
elif [ $buildType = 'release' ]; then
	(setProperty "KEYSTORE" "release.keystore")
	(setProperty "STORE_PASSWORD" "$storePass")
	(setProperty "KEY_ALIAS" "$keyAlias")
	(setProperty "KEY_PASSWORD" "$keyPass")
fi
  1. We get input params that are passed from Jenkins by calling this script.
  2. We use helper method that set key=value in properties file.
  3. Based on build type we set keystore values. You can see that I choose to set values of debug.keystore hard coded. This is totally fine since I don’t really care for securing this values and I don’t want developer that builds the debug builds enter the keystore credentials every time. But, for release.keystore we use the values that are passed from filling the parameters in Jenkins job. It’s important not to put them in any way hard coded in the script. It’s super sensitive information you wanna keep secured and hidden.

Build debug or release APK

# clean project
chmod +x gradlew
./gradlew clean --stacktrace

# build
if [ $buildType = 'debug' ]; then
	./gradlew assembleDebug --stacktrace
elif [ $buildType = 'release' ]; then
	./gradlew assembleRelease --stacktrace
fi

# ready expected final APK 
apkFileName="app-$buildType.apk"

# check if exists, if not exit with error
if [ ! -e "app/build/outputs/apk/$buildType/$apkFileName" ]; then
    echo "ERROR: File not exists: (app/build/outputs/apk/$buildType/$apkFileName)"
    exit 1
fi
  1. We clean the project.
  2. Build compile and build APK based on build type.
  3. Check if APK was created and if not we exit with error.

Move ready apk to artifacts folder

rm -r artifacts/
mkdir artifacts
cp app/build/outputs/apk/$buildType/$apkFileName artifacts/
  1. Recreate the artifacts folder.
  2. Copy the final APK to artifacts folder.
The final script - Check here.

:tada: That’s it for now. You can run the job and build debug or release APKs!!! Follow and check the next blog posts so you can build even better builds ;)

BTW, Let’s run

Build debug.apk:

1. Job -> Build with Parameters
2. Enter branch name and choose 'debug' build
3. Press 'Build'

Build release.apk:

1. Job -> Build with Parameters
2. Enter branch name and choose 'release' build
3. Input store password, key alias and key password
4. Press 'Build'	

Next blog post

Run lint and unit tests of Android builds from Jenkins

Setup and configure lint checks, build and run tests across all modules, collect and present reports on Jenkins.

Questions

If you have any comments, please open an issue at https://github.com/sromku/build-android-jenkins