Automate APK Deploy on Google Play Store using API?

0
107

We recently set out to automate APK deploy to the PlayStore to speed up our distribution process. For many development teams, automating your build, testing and release processes is essential. Multiple developers on one project can lead to chaos, broken builds and failing tests. That’s why many teams run a CI server to run tests on every commit to master for example.

Releasing can be a long manual task for a developer. So we like to use services, write scripts and generally make it one button press away.

Check this too:
Lottie – Convert Adobe After Effects to Kotlin
Most of time animation play a important role to success...

Our team, currently uses CircleCI. It runs our tests and builds our apps. We recently set out to Automate APK Deploy to the PlayStore, to speed up our distribution process.

What is Google Play Developer API?

Google provides an API to make edits to your PlayStore listing. It can be used to upload an apk and publish it. At present, they offer a Java and Python client library. Or you can go directly through HTTP. The way we wanted to use it in combination with CircleCI meant we had to go through HTTP.

How to Automate APK Deploy to the PlayStore?

1Service Account

First you will need to get a service account setup which has the permissions to deploy to the PlayStore.

See More:
Implement Android Rich Path Animator
Animation in Android is a best way to give a...

Click below link, then follow the steps under ‘Using a service account’ to get that setup.

You will need to retain the json key you created during this process.

2Access Token

To make any calls to an API you need to obtain an access token. To do this you call a different Google API passing it a JWT token.

JWT_HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e)
jwt_claims()
{
  cat <<EOF
{
  "iss": "$AUTH_ISS",
  "scope": "https://www.googleapis.com/auth/androidpublisher",
  "aud": "$AUTH_AUD",
  "exp": $(($(date +%s)+300)),
  "iat": $(date +%s)
}
EOF
}
JWT_CLAIMS=$(echo -n "$(jwt_claims)" | openssl base64 -e)
JWT_PART_1=$(echo -n "$JWT_HEADER.$JWT_CLAIMS" | tr -d '\n' | tr -d '=' | tr '/+' '_-')
JWT_SIGNING=$(echo -n "$JWT_PART_1" | openssl dgst -binary -sha256 -sign <(printf '%s\n' "$AUTH_TOKEN") | openssl base64 -e)
JWT_PART_2=$(echo -n "$JWT_SIGNING" | tr -d '\n' | tr -d '=' | tr '/+' '_-')

You will need the following variables from your json key:

  • AUTH_ISS — Field ‘client_email’
  • AUTH_AUD — Field ‘token_uri’
  • AUTH_TOKEN — Field ‘private_key’

In the snippet above, we are forming a JWT from the various components required, including signing the section. Note we have set an expiry date of 300 seconds time.

See More:
Top 20 Daily used Kotlin Code Snippet
In past month i did some work on Kotlin and...

Having an expiry is standard practice for these tokens, and just adds a level of security.

HTTP_RESPONSE_TOKEN=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
  --header "Content-type: application/x-www-form-urlencoded" \
  --request POST \
  --data "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=$JWT_PART_1.$JWT_PART_2" \
  "$AUTH_AUD")
HTTP_BODY_TOKEN=$(echo $HTTP_RESPONSE_TOKEN | sed -e 's/HTTPSTATUS\:.*//g')
HTTP_STATUS_TOKEN=$(echo $HTTP_RESPONSE_TOKEN | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')

if [ $HTTP_STATUS_TOKEN != 200 ]; then
  echo -e "Create access token failed.\nStatus: $HTTP_STATUS_TOKEN\nBody: $HTTP_BODY_TOKEN\nExiting."
  exit 1
fi
ACCESS_TOKEN=$(echo $HTTP_BODY_TOKEN | jq -r '.access_token')

We use the JWT to request an access token from Google. Then checking the response is a success and using jq to retrieve the token.

3Creating an Edit

To begin any PlayStore deployment you need to create an edit. You can think of this as a transaction, which you commit when ready.

EXPIRY=$(($(date +%s)+120))
post_data_create_edit()
{
  cat <<EOF
{
  "id": "circleci-$BUILD_NO",
  "expiryTimeSeconds": "$EXPIRY"
}
EOF
}

HTTP_RESPONSE_CREATE_EDIT=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header "Content-Type: application/json" \
  --request POST \
  --data "$(post_data_create_edit)" \
  https://www.googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits)
HTTP_BODY_CREATE_EDIT=$(echo $HTTP_RESPONSE_CREATE_EDIT | sed -e 's/HTTPSTATUS\:.*//g')
HTTP_STATUS_CREATE_EDIT=$(echo $HTTP_RESPONSE_CREATE_EDIT | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')

if [ $HTTP_STATUS_CREATE_EDIT != 200 ]; then
  echo -e "Create edit failed.\nStatus: $HTTP_STATUS_CREATE_EDIT\nBody: $HTTP_BODY_CREATE_EDIT\nExiting."
  exit 1
fi

EDIT_ID=$(echo $HTTP_BODY_CREATE_EDIT | jq -r '.id')

First we create our post data, consisting of a JSON of an id and an expiry. You can pass any id you want, it should just be unique. We are using our CircleCI build number to ensure uniqueness, whilst also providing traceability. The expiry defines how long you would like the edit to stay open, before automatically deleting. This helps to clear edits out which have failed.

See More:
Top 5 Android Wear Library In 2018
If you’re planning to make your own Wear app then...

Then we make our POST request to googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits, capturing the results. We then check for success and retrieve the id. We use the id received from the API to ensure we have the correct one, although it should be identical to the id we posted.

Note you will need your apps package name. This can be retrieved from your apk, using the aapt tool from the Android SDK build tools.

AAPT=$(find $ANDROID_HOME -name "aapt" | sort -r | head -1)
PACKAGE_NAME=$($AAPT dump badging $APK_PATH | grep package | awk '{print $2}' | sed s/name=//g | sed s/\'//g)

CircleCI comes with multiple versions of build tools, so in this snippet we grab aapt from the latest one.

4Uploading the APK

We now upload our apk against our edit.

HTTP_RESPONSE_UPLOAD_APK=$(curl --write-out "HTTPSTATUS:%{http_code}" \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header "Content-Type: application/vnd.android.package-archive" \
  --progress-bar \
  --request POST \
  --upload-file $APK_PATH \
  https://www.googleapis.com/upload/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID/apks?uploadType=media)
HTTP_BODY_UPLOAD_APK=$(echo $HTTP_RESPONSE_UPLOAD_APK | sed -e 's/HTTPSTATUS\:.*//g')
HTTP_STATUS_UPLOAD_APK=$(echo $HTTP_RESPONSE_UPLOAD_APK | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')

if [ $HTTP_STATUS_UPLOAD_APK != 200 ]; then
  echo -e "Upload apk failed\nStatus: $HTTP_STATUS_UPLOAD_APK\nBody: $HTTP_BODY_UPLOAD_APK\nExiting."
  exit 1
fi

Here we post our apk file to googleapis.com/upload/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID/apks using the id of our edit to associate it.

5Assign Edit to Track

Next, we set our meta information on the edit. We can specify track and version code among other things.

post_data_assign_track()
{
  cat <<EOF
{
  "track": "$PLAYSTORE_TRACK",
  "releases": [
    {
      "versionCodes": [
        $VERSION_CODE
      ],
      "status": "$STATUS"
    }
  ]
}
EOF
}

HTTP_RESPONSE_ASSIGN_TRACK=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --header "Content-Type: application/json" \
  --request PUT \
  --data "$(post_data_assign_track)" \
  https://www.googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID/tracks/$PLAYSTORE_TRACK)
HTTP_BODY_ASSIGN_TRACK=$(echo $HTTP_RESPONSE_ASSIGN_TRACK | sed -e 's/HTTPSTATUS\:.*//g')
HTTP_STATUS_ASSIGN_TRACK=$(echo $HTTP_RESPONSE_ASSIGN_TRACK | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')

if [ $HTTP_STATUS_ASSIGN_TRACK != 200 ]; then
  echo -e "Assign track failed\nStatus: $HTTP_STATUS_ASSIGN_TRACK\nBody: $HTTP_BODY_ASSIGN_TRACK\nExiting."
  exit 1
fi

We create our post JSON and call googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID/tracks/$PLAYSTORE_TRACK. Again checking for success.

You will need the following variables:

  • PLAYSTORE_TRACK – One of “alpha”, “beta”, “production”, “rollout” or “internal”
  • VERSION_CODE – You can retrieve the version code from your apk
AAPT=$(find $ANDROID_HOME -name "aapt" | sort -r | head -1)
VERSION_CODE=$($AAPT dump badging $APK_PATH | grep versionCode | awk '{print $3}' | sed s/versionCode=//g | sed s/\'//g)
  • STATUS – One of “completed”, “draft”, “halted”, “inProgress”. Halted and inProgress are for staged rollouts.

6Commit your Edit

The only step left is to commit your edit, meaning you have finished with this “transaction”.

HTTP_RESPONSE_COMMIT=$(curl --silent --write-out "HTTPSTATUS:%{http_code}" \
  --header "Authorization: Bearer $ACCESS_TOKEN" \
  --request POST \
  https://www.googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID:commit)
HTTP_BODY_COMMIT=$(echo $HTTP_RESPONSE_COMMIT | sed -e 's/HTTPSTATUS\:.*//g')
HTTP_STATUS_COMMIT=$(echo $HTTP_RESPONSE_COMMIT | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')

if [ $HTTP_STATUS_COMMIT != 200 ]; then
  echo -e "Commit edit failed\nStatus: $HTTP_STATUS_COMMIT\nBody: $HTTP_BODY_COMMIT\nExiting."
  exit 1
fi

We simply post to googleapis.com/androidpublisher/v3/applications/$PACKAGE_NAME/edits/$EDIT_ID:commit.

If you now head over to the PlayStore console you should see your deployment in the console.

We have put all the various pieces together into a single script, expecting certain variables to be passed in. To call this from CircleCI:

- run:
    name: Assemble APK for upload
    command: ./gradlew assembleLiveApi
- run:
    name: Upload to PlayStore
    command: |
      APK_PATH=$(find . -path "*release*.apk" -print -quit)
      ./.circleci/scripts/upload-playstore.sh "$PLAYSTORE_SERVICE_KEY" $APK_PATH $CIRCLE_BUILD_NUM internal false
      

We put this script in the .circleci folder of our repository. We have stored our service account key in environment variables, named PLAYSTORE_SERVICE_KEY.

Summary

Using the PlayStore API can be quite a complex process of steps. But hopefully this script will help simplify the process you for.

 

Share your thoughts

Loading Facebook Comments ...
Loading Disqus Comments ...