Signing in the Rain — or Why You Should Let Jenkins Do Android Release Builds

Johan Jungbeck
BBC Product & Technology
5 min readJul 10, 2017

One of the more hectic time during a development cycle is when you are getting ready for a release. The last minute bugs should be fixed, artwork should be in place, and someone must make sure there’s enough caffeine in the developers. It is easy to forget that you will actually also produce an APK — and it must be signed to be released to Google Play.

For a large company this is a suitable task for the build server. Letting a developer do it manually can quickly escalate. For an example: Bill, who usually does it, is on vacation. The project manager has to call him and disturb him right during his favourite musical. He emails the certificate and the password to Jane, then turns off his phone. Unfortunately Jane does not have same environment setup as Bill, so she is not able to use the build script. She makes hateful remarks about Gene Kelly on slack, and updates the script, which unfortunately breaks all the QA jobs on Jenkins.

Manually Signing

Before we get into how to sign on Jenkins it’s a good idea to have a look at how it’s done manually.

In android studio, go to: build->Generate Signed APK

Create a new keystore if you do not have an old. Select an alias, a password for the certificate, and a password for the keystore.

In this article the keystore will be saved at <keystore>/key.jks. The passwords will be pass123 and the alias key0.

Signing with Jenkins

When adding jobs to Jenkins I like to do so first on a local server, and then when it works I transfer it to the real build server. Normally a build server is busy (slow) and you don’t want to risk to accidentally break an existing job. I do so by installing Lubuntu guest in Virtualbox.

Signing with Gradle

The easiest way is to define the signing properties in app/build.gradle SigningConfigs:

signingConfigs {
debug{
...
}
release {
keyAlias "key0"
keyPassword "pass123"
storeFile file("<keystore>/key.jks")
storePassword "pass123"
}
}

This approach has several downsides:

  • The password will be visible in the source control (git)
  • The keystore must not be moved or it will break the build.

A popular technique not covered here is to save the password and keystore path in a local property file that will not be checked in with the source control.

Injecting through command line

A more flexible approach would be to add the sign properties with gradle’s
-P switch.

$ ./gradlew -Pkeypass=’pass123’ -PstoretPass=’pass123’ -PstoreFilePath=’<keystore>/key.jks’ assembleProductRelease

signingConfigs {
debug{
...
}
release {
...
keyPassword project["keyPass"]
storeFile file(project["storeFilePath"])
storePassword project["storePass"]
}
}

This is better, the passwords will not be checked in, and it’s possible to change the keystore path for each job.

Sample Jenkins job for injecting properties

But there are two security issues with this approach. The password will be visible in the jenkins job configuration. And Jenkins will write all the build commands in the log, including the -P switches. A typical build log will therefor look like this:

[Gradle] — Launching build.
[broadway-app-inject-sign] $ /var/lib/jenkins/workspace/broadway-app-inject-sign/gradlew -PkeyAlias=key0 -PkeyPass=pass123 -PstoreFile=/usr/share/android/sdk/keys/pkey.jks -PstorePass=pass123 clean assembleRelease

Secure Credentials and Jenkins

There are a couple of ways to hide sensitive information in Jenkins. The following are the two ways I feel suit Android signing the best.

Using Secret Text for Password

Start by adding a credential in Jenkins: Jenkins->Credentials->system->Global Credentials

Add Credentials.

  • In kind select secret text
  • Add your password in secret
  • The ID can be blank, but it is helpful to add a name you will remember

You can now use this in your jobs. In bindings, define a variable with your secret text. Then in the build step you can access this variable (don’t forget the $ sign)

Now your password is invisible for any user. In the build log the password is represented as stars:

[Gradle] — Launching build.
[broadway-app-inject-secret] $ /var/lib/jenkins/workspace/broadway-app-inject-secret/gradlew -PkeyAlias=key0 -PkeyPass=**** -PstoreFile=/usr/share/android/sdk/keys/pkey.jks -PstorePass=**** clean assembleRelease

Much more secure (unless you pick four stars as a password).

If you prefer pipeline scripting, the code would look something like:

node {    stage('get source') {
git 'https://yourgitrepourl/broadway.git'
}
stage('build apk'){
sh './gradlew clean assembleRelease'
}
stage('sign apk'){
sh '$ANDROID_BUILD_TOOLS/zipalign -v -p 4 app/build/outputs/apk/app-*-unsigned.apk app/build/outputs/apk/app-unsigned-aligned.apk'
withCredentials([string(credentialsId: 'signingPassSecret', variable: 'JKS_PSS')]) { sh '$ANDROID_BUILD_TOOLS/apksigner sign --ks /usr/share/android/sdk/keys/pkey.jks --ks-pass pass:$JKS_PSS --out signed.apk app/build/outputs/apk/app-unsigned-aligned.apk'
}
}
}

Android Signing Plugin

You can also use the android signing plugin. Unfortunately Jenkins only supports PCKS12 format of keystore. This means you’ll have to convert your android jks file.

$ keytool -importkeystore -srckeystore pkey.jks -destkeystore pkey.p12 -srcstoretype JKS -deststoretype PKCS12

Make sure source and dest passwords are same.

Now go to credentials and select your file and add password. If you don’t fill in an ID you’ll get an auto generated one like in the picture:

To sign the APK you need to have an unsigned version. This can be achieved by leaving the release signing config blank:

signingConfigs {
....
release {

}
}

Now when building release there will be an APK app-unsigned.apk

Add the plugin as a build step (after the assemble step)

That’s simple!

If you want to do it from a groovy script it’d look something like:

node {
stage('sync code'){
git branch: 'master', url: 'https://url.to.repo/zumba.git'
}
stage('build unsigned apk'){
sh './gradlew clean assembleRelease'
}
stage('sign apk'){
signAndroidApks (
keyStoreId: "81c76f5a-8868-4c14-b067-ed36bf497a8e",
keyAlias: "",
apksToSign: "**/*-unsigned.apk"
)
}
}

Conclusion

Now when the job is setup, you can trigger it manually or automatically when it’s time for release. It is an overhead to set it up, but you’ll save time and grey hairs every time you make a release build. You should give it a try, it’s a glorious feeling, you’ll be happy again.

--

--

Johan Jungbeck
BBC Product & Technology

Working in Children’s App Team at BBC Manchester. My bosses know what they want, are not afraid to complain loudly when unhappy, and are on average 6 y/o :)