Signing in the Rain — or Why You Should Let Jenkins Do Android Release Builds
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.
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.