How to Automatically Upload Android App Bundles to Play Store


In this article, I am going to explain how to automatically upload Android app bundle (.aab file) to the beta track of Play Store. We will be using Android Studio and AWS as the cloud infrastructure provider.

Once we’ve uploaded the app bundle, we’ll trigger a Slack notification.

This is a valuable use of your time for many reasons, such as building observational capacity and prioritizing processes.

tech we will use

Here are the resources we’re going to use for this tutorial:

  1. android studio
  2. aws codebuild
  3. AWS Lambda
  4. S3
  5. loose

High level overview of the project

beta_track_upload_flow-1

The image above shows you a general overview of how we’ll structure the whole thing.

Essentially, a code pipeline needs to be set up on AWS for your Android repository. This code pipeline will have code build as one of its steps.

Pushing to the master branch of your Android app repository will trigger a code build. The code build project will sign the Android app from the command line and upload the artifact to the S3 bucket.

Uploading the bundle to S3 will trigger a lambda, which will download the bundle and upload it to the Play Store using the Google Publishing API. Once a 200 response is received, Lambda will trigger a Slack notification.

How to Get Your Google Play Services Account Key

To be able to use the Google Play Publisher API, you will need a Google Play Services account key.

A service account is an account that can act on your behalf when the servers are communicating with each other. You can read more about how Google uses OAuth2.0 for server-to-server communication here.

To see how to create a service account and give it access to the Google Play Publisher API, see here.

Once you have created your service account and given it the appropriate permissions, be sure to download and protect the service account key. You’ll be uploading it to an S3 bucket soon.

How to Sign Android Bundles

The main thing to understand is how to sign Android app bundles. Google has quite a good documentation on this which you can find here.

I will summarize the link below.

generate a private key using keytool in such a way:

keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias

You can call your key whatever you want. here, i called it my-release-key.jks. You can also choose whatever nickname you want. Be sure to use the correct name and alias for your key throughout this tutorial.

Open build.gradle inside you app directory in Android Studio and add the following code block to it:

android {
    ...
    defaultConfig { ... }
    signingConfigs {
        release {
            // You need to specify either an absolute path or include the
            // keystore file in the same directory as the build.gradle file.
            storeFile file("my-release-key.jks")
            storePassword "password"
            keyAlias "my-alias"
            keyPassword "password"
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            ...
        }
    }
}

If you’ve renamed your release key to something other than the default, be sure to specify the new name. Same thing for surnames.

Your Store Password will be the password that you generated when you uploaded your app on Play Store for the first time.

Now, when you run the command ./gradlew :app:bundleRelease From the command line in Android Studio, you will see that it creates a signed app bundle.

How to Scrub Signing Information

Committing code with signature information available as plain text in build.gradle The file is a security risk and can be an attack vector.

Google has documentation around this which you can find here.

First, create a keystore.properties file in the root of your project directory.

The contents of the file should be as below:

storePassword=myStorePassword
keyPassword=myKeyPassword
keyAlias=myKeyAlias
storeFile=myStoreFileLocation

Your Store Password and Key Password will be the password you used when you first uploaded your app bundle to the App Store.

your keyAlias and storeFile The alias you specified when creating your private key and the location of the private key you created will be respectively.

Now, we need to load this file build.gradle. It initially came as a surprise to me, but Gradle actually works as a DSL. Therefore, using Gradle makes it easier to write configurations.

//  Load properties from keystore.properties
def keystorePropertiesFile = rootProject.file("keystore.properties")

//  Creating a new Properties() object
def keystoreProperties = new Properties()

//  If keystorePropertiesFile exists, read from that, else set from build environment
if (keystorePropertiesFile.exists()) {
    //  Loading the keystoreProperties file
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} else {
    //  Read all environment variables from the build environment
    keystoreProperties.setProperty("storeFile", "${System.getenv('STORE_FILE')}")
    keystoreProperties.setProperty("keyAlias", "${System.getenv('KEY_ALIAS')}")
    keystoreProperties.setProperty("keyPassword", "${System.getenv('KEY_PASSWORD')}")
    keystoreProperties.setProperty("storePassword", "${System.getenv('STORE_PASSWORD')}")
}

You’ll see the if condition in there – don’t worry about it right now. This accounts exclusively for later code builds.

Once you do that, your signingConfigs in parts build.gradle To look like this:

signingConfigs {
        release {
            storeFile file(keystoreProperties['storeFile'])
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storePassword keystoreProperties['storePassword']
        }
    }

How to set up AWS Code Pipeline

I am not going to go into too much detail on this as it is relatively simple.

Set up an AWS Code Pipeline with the following three steps:

  1. The source stage is linked to your GitHub repository master branch
  2. Build platform linked to AWS Code Build
  3. Deployment phase that will be deployed in S3 bucket.

You can find more documentation on setting up the code pipeline here.

How to set up AWS S3?

First, make sure you have a code pipeline set up with code build as a step. Next, set up two S3 buckets:

  1. A bucket to store your release key. i’m calling this bucket release-key.jks
  2. A bucket in which you will store your Google Play Services account private key. (You should have downloaded this key when you created your service account.)

You must allow access to these buckets from your Code Build Service role. Your Code Build Service role should have been created when you set up your code pipeline.

Go to IAM console and find your Code Build Service role and get ARN.

Next, use the console to go to the Permissions tab for Bucket release-key.jks and add the following policy there:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::123456789:role/service-role/codebuild-service-role-dummy",
                ]
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::release-key-bucket/*"
        }
    ]
}

This policy will allow access to the S3 bucket from the machine where your codebuild project will be executed.

You need to replace the ARN mentioned above with the ARN of your account. When you are updating the policy, be sure to specify the correct ARN for the Code Build Service role.

You do not need to change the permission policy for the second bucket. We will add the relevant permissions for the AWS Lambda role to allow it to access the bucket.

How to set up AWS CodeBuild

Next, create a buildspec.yml file in your project root folder.

version: 0.2

phases:
  build:
    commands:
      - aws s3api get-object --bucket release-key.jks --key release-key.jks ./releaseKey.jks
      - cp ./releaseKey.jks ${CODEBUILD_SRC_DIR}/app/releaseKey.jks
      - export STORE_FILE=releaseKey.jks
      - export KEY_ALIAS=$keyAlias
      - export KEY_PASSWORD=$keyPassword
      - export STORE_PASSWORD=$storePassword
      - ./gradlew :app:bundleRelease

artifacts:
  files:
    - app/build/outputs/bundle/release/app-release.aab

This file is quite simple. It receives the release key from the specified bucket and saves it to the specified location in a local file on the code build server.

Next, export all the variables needed for build.gradle for the configuration to work correctly. Finally, run Gradle’s release command from the command line.

Before you can run this script in a code build, you need to add variables to the code build environment. To do this, first go to the AWS Code Build Console and select your build project for your Android app.

Next, choose Edit > Environment like in the screenshot below:

After doing this select the Additional Configuration dropdown on the screen that pops up. There you will see the option to add environment variables via key value pairs.

Now when the code build runs buildspec.yml file, it will be able to export the specified variable.

As things stand now, when your pipeline runs, the code build will be able to download the private key and upload the signed bundle to an S3 bucket to sign and build your Android app.

how to set up slack app

Observability is the hallmark of automation. You want to know when your automation runs, whether it succeeds or fails, and if it fails, the reason for the failure.

AWS typically handles the observation capability through CloudWatch. But I think a Slack integration serves the purpose as well.

The easiest way to integrate Slack into your automation workflows is to set up a Slack app and send a notification to that app from your automation workflow.

To learn how to set up the Slack app, check out the documentation here. The process is very easy and you should have an app in just a few minutes.

Once you have created the app, you will get a webhook URL which you can use to call the app to post in the respective channel. Keep an eye on this WebHook URL as we will be using it with the AWS Lambda function.

How to set up AWS Lambda

So far, we have an Android app bundle being signed, built and uploaded to an S3 bucket. Next, we need to figure out how to upload the bundle to the beta track on the Play Store.

The way to do this is to set up an AWS Lambda which will start when the bundle is uploaded to the S3 bucket. When this is triggered, Lambda will run, download the bundle, hold the service account key, and upload the bundle to the Play Store beta track.

Once you have created the lambda and added a trigger to run it when a file is uploaded to Bucket, see the code below:

"""This Python3 script is used to upload a new .aab bundle to the play store. The execution of this Python script
    occurs through an AWS Lambda which is invoked when a new file is uploaded to the relevant S3 buckets"""

import json
import boto3
import os
from urllib import request, parse
from google.oauth2 import service_account
import googleapiclient.discovery

#   Defining the scope of the authorization request
SCOPES = ['https://www.googleapis.com/auth/androidpublisher']

#   Package name for app
package_name="com.app.name"

#   Define the slack webhook url
slack_webhook_url = os.environ['SLACK_WEBHOOK_URL']

def send_slack_message(message):
    data = json.dumps({ 'text': message })
    post_data = data.encode('utf-8')
    req = request.Request(slack_webhook_url, data=post_data, headers={ 'Content-Type': 'application/json' })
    request.urlopen(req)

#   This is the main handler function
def lambda_handler(event, context):
    #   Create a new client S3 client and download the correct file from the bucket
    s3 = boto3.client('s3')
    s3.download_file('service-account-bucket-key', 'service-account-bucket-key.json', '/tmp/service-account-key.json')
    SERVICE_ACCOUNT_FILE = '/tmp/service-account-key.json'

    #   Download the app-release.aab file that triggered the Lambda
    bucket_name = event['Records'][0]['s3']['bucket']['name']
    file_key = event['Records'][0]['s3']['object']['key']
    s3.download_file(bucket_name, file_key, '/tmp/app-release.aab')
    APP_BUNDLE = '/tmp/app-release.aab'

    print(f"A bundle uploaded to {bucket_name} has triggered the Lambda")

    #   Create a credentials object and create a service object using the credentials object
    credentials = service_account.Credentials.from_service_account_file(
        SERVICE_ACCOUNT_FILE, scopes=SCOPES
    )
    service = googleapiclient.discovery.build('androidpublisher', 'v3', credentials=credentials, cache_discovery=False)
    
    #   Create an edit request using the service object and get the editId
    edit_request = service.edits().insert(body={}, packageName=package_name)
    result = edit_request.execute()
    edit_id = result['id']

    #   Create a request to upload the app bundle
    try:
        bundle_response = service.edits().bundles().upload(
            editId=edit_id,
            packageName=package_name,
            media_body=APP_BUNDLE,
            media_mime_type="application/octet-stream"
        ).execute()
    except Exception as err:
        message = f"There was an error while uploading a new version of {package_name}"
        send_slack_message(message)
        raise err

    print(f"Version code {bundle_response['versionCode']} has been uploaded")

    #   Create a track request to upload the bundle to the beta track
    track_response = service.edits().tracks().update(
        editId=edit_id,
        track='beta',
        packageName=package_name,
        body={u'releases': [{
            u'versionCodes': [str(bundle_response['versionCode'])],
            u'status': u'completed',
        }]}
    ).execute()

    print("The bundle has been committed to the beta track")

    #   Create a commit request to commit the edit to BETA track
    commit_request = service.edits().commit(
        editId=edit_id,
        packageName=package_name
    ).execute()

    print(f"Edit {commit_request['id']} has been committed")

    message = f"Version code {bundle_response['versionCode']} has been uploaded from the bucket {bucket_name}.nEdit {commit_request['id']} has been committed"
    send_slack_message(message)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Successfully executed the app bundle release to beta')
    }

The lambda above will use googleapiclient Library and its Discovery module for generating URLs for Google Play’s Publishing API.

Next, Lambda will download the service account key from the bucket you set up earlier. You must ensure that you have specified the correct bucket names.

Depending on whether the upload succeeds or fails, we want a Slack message to go out. Add the Slack webhook URL from the previous section to the environment variables for the lambda. The above function uses Python os To get access to the module environment variables and to post the message to Slack.

If your Lambda fails, it may be because your Lambda does not have permission to access the S3 bucket where the key for your Google Play Services account is stored. In that case, you will see an error message indicating this.

To fix this, all you need to do is add the relevant permissions to your Lambda role.

Here is the policy you need to add:

{
    "Version": "2012-10-07",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObjectVersion",
                "s3:GetBucketVersioning",
                "s3:GetBucketAcl",
                "s3:GetObject",
                "s3:GetBucketTagging",
                "s3:GetBucketLocation",
                "s3:GetObjectVersionAcl"
            ],
            "Resource": [
                "arn:aws:s3:::arn:aws:s3:::your-bucket-name-with-service-account-key"
            ]
        }
    ]
}

Replace the ARN for the bucket with the one relevant to your account and you should be good to go.

Conclusion

So there you have it. It certainly wasn’t easy and there are a lot of moving parts, but it is an automation that will save you a lot of time and effort.

If you’re part of a team that is rolling out new app updates frequently, you don’t want to be hindered by the absence of one person whose job it is to release updates.

Building this kind of automation makes your CI/CD workflow much easier and more robust.

If you are interested in blogs like this, you can read more at https://redixhumayun.github.io or follow me on Twitter.



Source link

Leave a Reply

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