Android Versioning Using Docker & Git Like A Pro
Unlike web, android still lacks the ease of version deployments. Specially when you don’t want to use Play Store.
Introduction
There will be five stages:
- Signing application
- Versioning of application. For that we gonna use git revision and Major.Minor.Patch naming convention.
- Building application using a docker. So that running environment doesn’t change.
- Pushing new release to s3, while maintaining the previous versions.
- Pushing new tag to git, with the new version. So, we’ll have tags for each version.
Basically, we gonna use docker, git, and some simple hacks to put things in work. In the end, I’ve shared a sample application.
Stage 1: Signing Our Application
It’s better to start thinking about security right from the big bang. From android studio, you can generate a new keystore, a jks file. Help? Copy the keystore file details in a config.yaml file like below:
key_store:
key: /xyz/xyz.jks
alias: key0
store_password: wuhoo
key_password: nibataunga
Studio will take care of signing, but to generate signed apk from command line, you’ll need to make some changes in your build.gradle. The credentials we have put in above yaml file will be passed as command line args to gradle(Build stage[2]).
android {
...
signingConfigs {
release {
if (project.hasProperty('APP_RELEASE_STORE_FILE')) {
storeFile file("$APP_RELEASE_STORE_FILE")
storePassword "$APP_RELEASE_STORE_PASSWORD"
keyAlias "$APP_RELEASE_KEY_ALIAS"
keyPassword "$APP_RELEASE_KEY_PASSWORD"
}
}
}
buildTypes {
release {
...
if (project.hasProperty('APP_RELEASE_STORE_FILE')) {
signingConfig signingConfigs.release
}
}
}
}
Stage 2: Release Versioning, Digging Git
I’am here using the semantic versioning.
Major.Minor.GitRevision.Patch
Let’s dig into GitRevision It counts the number of commits from git, so you’ll get incremental values everytime you release a new version. GitRevision will make versioning easy and consistent.
We’ll put the below code in build.gradle[app]
def getGitRevision = { ->
try {
def stdout = new ByteArrayOutputStream()
exec {
standardOutput = stdout
commandLine 'git', 'rev-list', '--first-parent', '--count', 'master'
}
logger.info("Building revision #"+stdout)
return stdout.toString("ASCII").trim().toInteger()
}
catch (Exception e) {
e.printStackTrace();
return 0;
}
}
And in build.gradle[app]
defaultConfig {
...
versionCode = 10000000*majorVersion+10000*minorVersion + 10*revision
versionName = 'v' + majorVersion + '.' + minorVersion + '.' + revision + patch
}
Docker Image, Savage
We first need to build a docker image with minimum libraries and dependencies required.
FROM openjdk:8
RUN apt-get update
RUN cd /opt/
RUN wget -nc https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip
ENV ANDROID_HOME /opt/android-sdk-linux
RUN mkdir -p ${ANDROID_HOME}
RUN unzip -n -d ${ANDROID_HOME} sdk-tools-linux-4333796.zip
ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/tools/bin:${ANDROID_HOME}/platform-tools
RUN yes | sdkmanager --licenses
RUN yes | sdkmanager \
"platform-tools" \
"build-tools;27.0.3" \
"platforms;android-27"
RUN apt-get -y install ruby
RUN gem install trollop
Trollop will be helpful in compiling scripts, spicing the boring command line args.
We are using openjdk as base image for java environment and installed our sdk with version 27. You can change that accordingly.
Building the image:
docker build -t ${docker_image} -f ./scripts/Dockerfile .
Or you can directly pull my latest base image.
docker pull mukarramali98/androidbase
Docker container on the way
To automate the process, let’s dig into a small script:
#!/usr/bin/env bash
set -xeuo pipefail
app_name=xyz
container_name=androidcontainer
if [ ! "$(docker ps -q -f name=${container_name})" ]; then
if [ "$(docker ps -aq -f status=exited -f name=${container_name})" ]; then
# cleanup
docker rm $container_name
fi
# run your container
docker run -v ${PWD}:/${app_name}/ --name ${container_name} -w /${app_name} -d -i -t mukarramali98/androidbase
fi
docker exec ${container_name} ruby /${app_name}/scripts/compile.rb -k /${app_name}/config.yaml
Here we first check if the container already exists. Then create accordingly. While creating the container, we mount our current project directory. So next time we run this container, our updated project will already be there in the container.
Stage 3: Running container, Build Stage
We run the container, with our compile script. Pass the signing config file we created earlier.
config = YAML.load_file(key_config_file)
key_store = config['key_store']
output_file = 'app/build/outputs/apk/release/app-release.apk'
`rm #{output_file}` if File.exists?output_file
puts `#{File.dirname(__FILE__)}/../gradlew assembleRelease --stacktrace \
-PAPP_RELEASE_STORE_FILE=#{key_store['key']} \
-PAPP_RELEASE_KEY_ALIAS=#{key_store['alias']} \
-PAPP_RELEASE_STORE_PASSWORD='#{key_store['store_password']}' \
-PAPP_RELEASE_KEY_PASSWORD='#{key_store['key_password']}'`
Stage 4: Pushing to S3
So, now we have build a signed apk from a docker container. It’s time to push them. Connect with your s3 bucket and generate $HOME/.s3cfg file, and pass it to ruby script below:
if File.file?(s3_config)
# Push the generate apk file with the app and version name
`s3cmd put app/build/outputs/apk/release/app-release.apk s3://#{bucket}/#{app_name}-#{version_name}.apk -m application/vnd.android.package-archive -f -P -c #{s3_config}`
# application/vnd.android.package-archive is an apk file format descriptor
# Replace the previous production file
`s3cmd put app/build/outputs/apk/release/app-release.apk s3://#{bucket}/#{app_name}.apk -m application/vnd.android.package-archive -f -P -c #{s3_config}`
# To keep the track of latest release
`echo #{version_code}> latest_version.txt`
`s3cmd put latest_version.txt s3://#{bucket}/latest_version.txt -f -P -c #{s3_config}`
`rm latest_version.txt`
puts "Successfully released new app version."
end
application/vnd.android.package-archive
is the apk file type descriptor.
Stage 5: Finally, Git Tagging The New Release Version, #hashtag
def push_new_tag version_name
`git tag #{version_name}`
`git push origin #{version_name}`
puts "New tag pushed to repo."
end
![Mukarram Ali photo](/blog/images/mukku.jpg)