Blog Infos
Author
Published
Topics
,
Published

Since HockeyApp has been retired our Android team has been using Microsoft App Center to distribute builds internally. We needed a way to automate app builds with release notes using our CI pipeline.

In the past with HockeyApp we were using this super simple curl to distribute a build. Isn’t it nice and succinct? 😍

curl \
-F "status=2" \
-F "notify=0" \
-F "notes=$RELEASE_NOTES" \
-F "notes_type=1" \
-F "teams=Internal-Testers" \
-F "ipa=@app/build/outputs/apk/$ENV_NAME/$APK_NAME" \
-H "X-HockeyAppToken: $API_TOKEN" \
https://rink.hockeyapp.net/api/2/apps/$2/app_versions/upload

Now onto App Center! According to the aforementioned article the transition from HockeyApp to App Center would be a “seamless transition process”.

Disbelief from Giphy

It has definitely not been a seamless transition process. Even after following their documentation things did not work as expected. With the help of the App Center support team and a sample script they provided I was able to get our build pipeline working again.

The following is everything you need to know to automate your Android build process.

App Center deploy script

This is the deploy script you’ll call from your CI pipeline. Call this script after the step that creates an APK build. In our project this deploy script is in ProjectRoot/.signing.

As you can see it requires 7 steps and is much longer than the code required to release HockeyApp builds. 😆

#!/bin/sh
# Description:
# This script releases a build to App Center using the last commit message for release notes.
# Each new line in the commit will be a separate bullet in the release notes.
# If the release notes contain the word "skip_appcenter_release" then no release will be generated.
# Example script usage:
# deploy.sh [dev|release] {API key}
# Environment variables defined in CircleCi:
# $APP_CENTER_API_TOKEN_STAGING
# $APP_CENTER_API_TOKEN_PROD
# $APP_CENTER_DISTRIBUTION_GROUP
# $APP_CENTER_OWNER_NAME
cd ProjectRoot
APPCENTER_OUTPUT_DIRECTORY="."
VERSION=$(./gradlew -q ":app:printVersion" | tail -n 1)
APP_NAME=ParentAppStaging
CONTENT_TYPE='application/vnd.android.package-archive'
ENV_NAME=staging
APK_NAME="AppName-staging-$VERSION.apk"
if [ "$1" == "release" ]; then
APK_NAME="AppName-production-$VERSION.apk"
ENV_NAME=release
APP_NAME=ParentAppProd
fi
echo "Preparing to build $APK_NAME"
RELEASE_FILE_LOCATION=$(pwd)"/app/build/outputs/apk/$ENV_NAME/$APK_NAME"
FILE_SIZE_BYTES=$(wc -c $RELEASE_FILE_LOCATION | awk '{print $1}')
# -------------- Handle release notes --------------
# Get the last commit message and use it as release notes
RELEASE_NOTES=$(git log -1 --pretty=%B | grep -v -e '^$' | grep -v "^Merge pull request" | sed -e 's/^/\* /' | awk '{printf "%s\\n", $0}')
echo -e "Formatted release notes are below:\n$RELEASE_NOTES\n\n"
if [[ $RELEASE_NOTES == *"skip_appcenter_release"* ]]; then
echo "**** Skipping App Center release. ****"
exit 0
fi
# -------------- Begin upload process --------------
UPLOAD_DOMAIN="https://file.appcenter.ms/upload"
API_URL="https://api.appcenter.ms/v0.1/apps/$APP_CENTER_OWNER_NAME/$APP_NAME"
AUTH="X-API-Token: $2"
ACCEPT_JSON="Accept: application/json"
# Create upload resource - step 1/7 **************************
echo "Creating upload resource (1/7)"
upload_json=$(curl -s -X POST -H "Content-Type: application/json" -H "$ACCEPT_JSON" -H "$AUTH" "$API_URL/uploads/releases")
releases_id=$(echo $upload_json | jq -r '.id')
package_asset_id=$(echo $upload_json | jq -r '.package_asset_id')
url_encoded_token=$(echo $upload_json | jq -r '.url_encoded_token')
# Upload metadata - step 2/7 **************************
echo "Creating metadata (2/7)"
metadata_url="$UPLOAD_DOMAIN/set_metadata/$package_asset_id?file_name=$APK_NAME&file_size=$FILE_SIZE_BYTES&token=$url_encoded_token&content_type=$CONTENT_TYPE"
meta_response=$(curl -s -d POST -H "Content-Type: application/json" -H "$ACCEPT_JSON" -H "$AUTH" "$metadata_url")
chunk_size=$(echo $meta_response | jq -r '.chunk_size')
split_dir=$APPCENTER_OUTPUT_DIRECTORY/split-dir
mkdir -p $split_dir
eval split -b $chunk_size $RELEASE_FILE_LOCATION $split_dir/split
# Upload APK chunks - step 3/7 **************************
echo "Uploading chunked binary (3/7)"
binary_upload_url="$UPLOAD_DOMAIN/upload_chunk/$package_asset_id?token=$url_encoded_token"
block_number=1
for i in $split_dir/*
do
echo "start uploading chunk $i"
url="$binary_upload_url&block_number=$block_number"
size=$(wc -c $i | awk '{print $1}')
curl -X POST $url --data-binary "@$i" -H "Content-Length: $size" -H "Content-Type: $CONTENT_TYPE"
block_number=$(($block_number + 1))
printf "\n"
done
# Finalize upload - step 4/7 **************************
echo "Finalising upload (4/7)"
finish_url="$UPLOAD_DOMAIN/finished/$package_asset_id?token=$url_encoded_token"
curl -d POST -H "Content-Type: application/json" -H "$ACCEPT_JSON" -H "$AUTH" "$finish_url"
# Commit release - step 5/7 **************************
echo "Committing release (5/7)"
commit_url="$API_URL/uploads/releases/$releases_id"
curl -H "Content-Type: application/json" -H "$ACCEPT_JSON" -H "$AUTH" \
--data '{"upload_status": "uploadFinished","id": "$releases_id"}' \
-X PATCH \
$commit_url
# Poll for release ID - step 6/7 **************************
echo "\nPolling for release ID (6/7)"
release_id=null
counter=0
max_poll_attempts=15
while [[ $release_id == null && ($counter -lt $max_poll_attempts)]]
do
poll_result=$(curl -s -H "Content-Type: application/json" -H "$ACCEPT_JSON" -H "$AUTH" $commit_url)
release_id=$(echo $poll_result | jq -r '.release_distinct_id')
echo $counter $release_id
counter=$((counter + 1))
sleep 3
done
if [[ $release_id == null ]];
then
echo "Failed to find release from App Center"
exit 1
fi
# Distribute build - step 7/7 **************************
echo "Distributing build (7/7)"
distribute_url="$API_URL/releases/$release_id"
distribute_response=$(curl -H "Content-Type: application/json" -H "$ACCEPT_JSON" -H "$AUTH" \
--data '{"destinations": [{ "name": "'"$APP_CENTER_DISTRIBUTION_GROUP"'"}], "notify_testers": true, "release_notes": "'"$RELEASE_NOTES"'" }' \
-X PATCH \
$distribute_url)
status_code=$(echo $distribute_response | jq -r '.status')
if [[ $status_code == 500 ]];
then
echo "Failed to distribute app. Response = $distribute_response"
exit 1
fi
# -------------- End upload process --------------
view raw deploy.sh hosted with ❤ by GitHub

That’s a lot of code! It is. Let’s go over the parts that you’ll need to make this work.

App info from App Center

We’ll need the owner name and app name from App Center. We have a staging and production App Center variant so our deploy script takes both into account. The App Center URL format is as follows.

https://appcenter.ms/orgs/{owner_name}/apps/{app_name}

This gives you APP_CENTER_OWNER_NAME and APP_NAME. We also added APP_CENTER_DISTRIBUTION_GROUP as an environment variable for flexibility. If you look at release history in App Center this is under the Destinations column.

API keys

Head over to App Center and navigate to the target app > Settings > App API tokens. Create a full access token. You’ll need to do this for each one of your apps in App Center.

Save the API tokens as environment variables in your CI settings. We used APP_CENTER_API_TOKEN_STAGING and APP_CENTER_API_TOKEN_PROD as we have a staging and production variant in App Center.

Version named APKs

When we generate a build our APK file names have a version. For example AppName-staging-2.3.4–567.apk. There are two parts to this. 1) Generating APK files with versions and 2) reading that version in the deploy script (line 20 from the deploy script above).

  1. Generating version named APKs

This goes in the android.buildTypes block. For each build type that we have, we name it.

buildTypes {
applicationVariants.all { variant ->
def version = "${variant.versionName}-${variant.versionCode}"
variant.outputs.all { output ->
if (variant.buildType.name == "debug") {
outputFileName = "AppName-debug-${version}.apk"
} else if (variant.buildType.name == "staging") {
outputFileName = "AppName-staging-${version}.apk"
} else if (variant.buildType.name == "release") {
outputFileName = "AppName-production-${version}.apk"
}
}
}
...
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

2. Reading the version from the deploy script

In order to access the version in the deploy script we’ll use a gradle task.

task printVersion {
println "${android.defaultConfig.versionName}-${android.defaultConfig.versionCode}"
}

Then as you can see on line 20 from the deploy script we read this version.

VERSION=$(./gradlew -q ":app:printVersion" | tail -n 1)
Automatic build number increments

All this build automation is great but wouldn’t it be nice if the build number could automatically be incremented on each new build? There’s a way! (Assuming your CI system has a build number environment variable.)

We use CircleCi for Continuous Integration. CircleCi has built-in environment variables. One of these variables is CIRCLE_BUILD_NUM.

In order to update the app build number we need to get that number into the app build.gradle file. One way to do that is have our CI system write the build number to a file and then read that file in our gradle file.

Write the CI build number to a file
echo "VERSION_CODE=$CIRCLE_BUILD_NUM" > version.properties

This creates a file called version.properties in ProjectRoot/.signing. This is one of the steps in our CI file that runs before we build the app.

Read this number in our app build.gradle file
android {
//version.properties is only used to get the version number from CircleCi
def versionFile = file('../.signing/version.properties')
def buildNumber = 456
if(versionFile.canRead()) {
Properties props = new Properties()
props.load(new FileInputStream(versionFile))
//Add skip_appcenter_release in a commit message to develop/master/release if you don't want a build to be created by CircleCi
if(props['VERSION_CODE'] != null) {
def circleBuildNumber = props['VERSION_CODE'].toInteger()
buildNumber = circleBuildNumber - 3870 //subtract offset to get close to our real build number
}
}
defaultConfig {
versionCode buildNumber
versionName "1.2.3"
...
}
...
}
Branch level execution

Using a CircleCi configuration file, we are able to specify that we only want to deploy builds when we commit to develop, master, or release branches.

Here’s the step in our CI system where we do this.

- run:
name: Upload build to App Center (if develop, release, or master)
command: |
if [ "$CIRCLE_BRANCH" == "develop" ] || [[ "$CIRCLE_BRANCH" == "release/"* ]]; then
bash ./ProjectRoot/.signing/deploy.sh dev $APP_CENTER_API_TOKEN_STAGING
fi
if [ "$CIRCLE_BRANCH" == "master" ]; then
bash ./ProjectRoot/.signing/deploy.sh release $APP_CENTER_API_TOKEN_PROD
fi
view raw circle.yml hosted with ❤ by GitHub

And there you have it! A slick way to automate your Android app builds.

Success from Giphy

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
This tutorial is the second part of the series. It’ll be focussed on developing…
READ MORE
blog
We recently faced a problem with our application getting updated and reaching slowly to…
READ MORE
blog
A few weeks ago I started with a simple question — how to work…
READ MORE
blog
One of the main functions of a mobile phone was to store contacts information.…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu