Jenkins, signing Android builds with an entered password, and the Promoted Build plugin
Good workflow is Jenkins automatically picking up changes from git, rebuilding the app in release mode (including proguard) and running the test suite.
If it passes it becomes a Release Candidate, ready for manual testing then distribution to testers and eventually PROD. It's around this point that the .apk needs to be signed using your official Android Developer key rather than the debug key. I'm not keen on putting the password for that in build.properties.
(If you're not uploading to Google Play you can distribute manually with the auto-generated debug key, but switching keys requires your testers to uninstall then reinstall which makes them sad testers so it's best to stabilise at some point)
Jenkins has 'parameterised build' properties which prompt you for values at runtime, this includes passwords which are masked as much as possible. But this doesn't fit into the automatic build-test cycle that is triggered by git, and you don't need it for most builds during development, just ones you're ready to release to the world.
So create a job which takes the output of the release build - 'myapp-release-unsigned.apk' - prompts for the password, signs and zipaligns the .apk then copies it back to the original build's artifacts directory ready for easy picking. As this doesn't rebuild the apk we can be certain that this is the same build that just passed the tests. Maybe trigger the tests again if you like to be certain.
There's a Jenkins plugin called 'promoted builds' which seems to be for exactly this purpose. When a build has finished you can automatically or manually (I prefer to to some manual testing) mark the build as 'promoted' - this gives it a nice star. It also allows another Jenkins job to be triggered. You'd think this would be a great place to trigger a job to sign the .apk... it would, but I could not get it to present the parameterized build's input screen so there's no way to enter the password. (if you're happy to embed the password somewhere then you don't need this, do it in your normal release build).
Install the 'Promoted Builds' plugin along with the 'Copy Artifact' and 'Workspace Cleanup' plugins.
First edit the configuration of the builds you might want to sign the artifacts of, tick the 'Permission to Copy Artifact' box, enter something like 'sign-release-apk' and save.
Create a new project called 'sign-release-apk'. Tick 'This build is parameterised' and add two 'Password Parameter' fields with names 'keystore_password' and 'keyalias_password'. I tried 'Non-stored password parameter' too but that doesn't mask them in the environment so they might be available to snoopers.
At runtime you need a way of selecting which build produced the .apk you are happy with, this is where the 'Promoted Build' plugin comes in handy as it provides a picklist of just the builds you have promoted. It doesn't set your choice in the property formats we need though so some manipulation is needed - copy & pasted below. If you want the dropdown then add a 'Promoted Build Parameter' and set the Job Name.
The script below for parsing the Promoted Build parameter's value is for linux, OSX or other unixes, if you're using Windows and want to use the Promoted Build parameter you'll need to find a way to extract the job name and build number from the URL property that it sets. Maybe cygwin will be fine if you install rev, cut and sed.
If you'd rather not bother with the Promoted Build plugin and manually enter the project name and build number you'd like then that's fine too, set SIGN_PROMOTED_BUILD_NUMBER and SIGN_PROMOTED_BUILD_JOB and skip the first two build steps below. I don't recommend using 'Build selector for Copy Artifact' because it sets the property with some bespoke XML which will need to be parsed to extract the job name and build number for use by the later script.
Tick 'delete workspace before build starts' - you really don't want your builds getting mixed up.
Tick 'Inject environment variables to the build process' and in the 'Properties Content' box enter:
KEYSTORE_LOCATION=/var/lib/jenkins/my-private-keystore
KEY_ALIAS=myappkey
or wherever your keystore lives. If they change or you'd like some security-via-minor-obscurity you could make them enterable parameters.
Click add build step, choose 'Execute shell' and paste the below:
echo "SETTING signing_for_job.properties:" echo "-----------------------------------" echo "pwd: `pwd`, SIGN_PROMOTED_BUILD_ABS_URL: $SIGN_PROMOTED_BUILD_ABS_URL" echo SIGN_PROMOTED_BUILD_NUMBER=$(echo "$SIGN_PROMOTED_BUILD_ABS_URL" | rev | cut -d / -f2 | rev) >signing_for_job.properties echo SIGN_PROMOTED_BUILD_JOB=$(echo "$SIGN_PROMOTED_BUILD_ABS_URL" | sed -rn 's/^.*\/job\/([^/]*)\/.*/\1/p') >>signing_for_job.properties
This parses the URL provided by the Promoted Build parameter and extracts the project name and build number into separate properties which are written into a temporary file which is loaded into Jenkins' environment in the next step.
Add a step 'Inject environment variables' and enter 'signing_for_job.properties' in the Properties file Path.
This seems a bit clunky, is there a more Jenkins-native way to parse a URL than using a shell script which writes to a property file?
Add a step 'Copy artifacts from another project', for Project Name enter:
${SIGN_PROMOTED_BUILD_JOB}
for 'Which build' enter 'Specific Build' and for 'Build number' enter:
${SIGN_PROMOTED_BUILD_NUMBER}
under 'Artifacts to copy' I've entered:
and ticked the 'Flatten directories' box below. We're not interested in storing any artifacts in this build - they'll be copied back to the original build's artifacts directory so let's keep this one as clean as possible.
Add another 'Execute shell' step and paste in:
#!/bin/bash echo "SIGNING AND ZIPALIGNING" echo "-----------------------" echo "pwd: `pwd` " echo "SIGN_PROMOTED_BUILD_ABS_URL: $SIGN_PROMOTED_BUILD_ABS_URL" echo "SIGN_PROMOTED_BUILD_JOB: $SIGN_PROMOTED_BUILD_JOB" echo "SIGN_PROMOTED_BUILD_NUMBER: $SIGN_PROMOTED_BUILD_NUMBER" echo "PROMOTED_NUMBER: $PROMOTED_NUMBER" export RELEASE_UNSIGNED_APK=$(ls *-release-unsigned.apk) export APK_BASE=$(echo $RELEASE_UNSIGNED_APK | sed -rn 's/^(.*)-release-unsigned.apk$/\1/p') echo "RELEASE_UNSIGNED_APK: $RELEASE_UNSIGNED_APK" echo "APK_BASE: $APK_BASE" jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ${KEYSTORE_LOCATION} $APK_BASE-release-unsigned.apk "${KEY_ALIAS}" -storepass $keystore_password -keypass $keyalias_password -signedjar $APK_BASE-release-signed-unaligned.apk $ANDROID_HOME/build-tools/latest/zipalign -v 4 $APK_BASE-release-signed-unaligned.apk $APK_BASE-release-signed-aligned.apk
Note $ANDROID_HOME/build-tools/latest/ probably doesn't exist for you, 'latest' is a symlink that I update to point to the latest directory as new versions are installed by the SDK downloader. Either do the same or change it to the latest one you have. Unless you'd prefer to add it to jenkins' or your system path, in which case you can leave it out and just run 'zipalign'.
Without the '#!/bin/bash' line at the top Jenkins will echo back each line of the script into the log which will include your passwords.
The above script will look for a file ending in '-release-unsigned.apk' and parse it to find the part of the name before it which it sets as APK_BASE. It uses jarsigner with KEYSTORE_LOCATION and KEY_ALIAS (that were set in Properties Content) and keystore_password and keyalias_password (that were set by masked input fields) to create a -release-signed-unaligned.apk, then zipaligns to -release-signed-aligned.apk.
The latter is the file we want to distribute, let's copy it back to the build it came from:
Add a build step 'Execute shell' and paste in:
#!/bin/bash echo "COPYING SIGNED APK BACK TO BUILD" echo "---------------------------------" echo "pwd: `pwd` " echo "SIGN_PROMOTED_BUILD_JOB: $SIGN_PROMOTED_BUILD_JOB" echo "SIGN_PROMOTED_BUILD_NUMBER: $SIGN_PROMOTED_BUILD_NUMBER" echo "APK_BASE: $APK_BASE" export TARGET_DIR=../../$SIGN_PROMOTED_BUILD_JOB/builds/$SIGN_PROMOTED_BUILD_NUMBER/archive/bin/ echo "TARGET_DIR: $TARGET_DIR" ls -l $TARGET_DIR cp -va *-release-signed-aligned.apk $TARGET_DIR/ ls -l $TARGET_DIR
That's it! I'm archiving the artifacts purely for debugging purposes, there's no need to do so as the only thing you're interested in is the final apk and that will have been copied back to your original build directory.
Run it, enter your build, check the log, then if all looks OK check the artifacts on your original build for the new -release-signed-aligned.apk file.
Fingerprinting
Is fingerprinting useful? If your signed apk is depended on by other projects then it probably is. It won't be covered by fingerprinting in the main job because it has no idea about this -signed .apk. Fingerprinting in the sign-release-apk job will work but I'm not sure if the trail will be intact if it is referenced under the main job. It might be.
Re-testing
You could kick off your test suite again but this time using the signed .apk, just add a post-build action to the signing project.
Versioning
This job could edit the AndroidManifest.xml in the .apk before it is signed and change the version number from an internal-test-build-number to a publicly-facing-release-number. Is this safe? Probably, as long as you're very careful to ONLY edit the version string in the manifest, anything else could result in application-level changes that will mean you've not run the package you're distributing through your test process.
If it manages to mess up the XML then the resulting .apk will won't install so there's an easy test for that. Maybe trigger a re-test and have the test suite check the package version number.
Auto-deploy
Auto-upload to Google Play, Amazon Appstore and anywhere else? Probably doable. I'm not sure I'd be that comfortable about it without a quick manual test inbetween, maybe another job similar to this one could do the deployment when manually triggered?
Tag source code
Add a tag to git to state you've made a release build. Mention the version number, especially if you changed it as suggested above. If the original build was triggered by SCM then it will have the revision it built from, if not then it's a good idea to store that somewhere in the build's artifacts. I include it in the (development) .apk filename.