Daniel Alvarez

Automate AppGallery releases with GitHub Actions (2/2)

hmsandroidgithubgithub actions

Our goal is to upload the freshly generated, signed APK to AppGallery automatically from GitHub Actions.

In the first part of this guide we set up the process, handled the sensitive information safely and ended up with a correctly signed APK, ready to publish to AppGallery. You can read it here:

Automate AppGallery releases with GitHub Actions (1/2)

To get there we will use the Publishing API, part of the AppGallery Connect APIs.

Publishing API

Among other things, this API lets us create a new application record in AppGallery, update the application information and upload files: an APK, an RPK (for QuickApps) or an AAB, but also icons and other assets.

We will use the file upload service, with the signed APK from part 1. But for that file to show up in AppGallery ready to be selected for a new release, a few steps have to happen first.

These are the steps:

Each of these steps is a Publishing API endpoint.

Preparations

Before calling these endpoints we need to collect a few pieces of information.

Step 1 needs a Client Id and a Client Secret, which means creating a new API key in the AppGallery console. Sign in to AppGallery Connect, then go to Users and permissions > Api Key > Connect API.

Create an API key, either for every application or for a selected few.

App Gallery Connect API

Once created, you can read the Client Id and the Key, which is our Client Secret. Both are sensitive.

We also need the App Id of the application we are uploading to. It is on the main page of the application in the console, and unlike the previous two, it is public, so it does not need to be kept secret.

App Gallery Connect API

With that, we are ready to call the API.

The Script

We could call the endpoints from the command line with curl, but parsing the responses is much easier in a Python script, and the runner is an Ubuntu machine that already ships Python and pip.

Step 1: Get a token to perform all this process

We will use requests for the HTTP calls, with one function per step. Each one is written as simply as possible, with no validation, to keep the focus on the flow.

import requests
import sys
 
def get_token(client_id, client_secret):
    url = 'https://connect-api.cloud.huawei.com/api/oauth2/v1/token'
    body = {
        'grant_type': 'client_credentials',
        'client_id': client_id,
        'client_secret': client_secret
    }
    response = requests.post(url, json=body)
    if response.status_code == 200:
        json = response.json()
        return json['access_token']
    else:
        print('token: ' + str(response.status_code) + ': ' + response.reason)

That returns the token we will use for the rest of the process, starting with the next step.

Step 2: Get a valid URL to upload the signed APK

This is where the App Id comes in, and the parameters declare the type of file we are about to upload, an APK in this case.

def get_upload_url(access_token, client_id, app_id):
    url = 'https://connect-api.cloud.huawei.com/api/publish/v2/upload-url'
    params = {
        'appId': app_id,
        'suffix': 'apk'
    }
    headers = {
        'client_id': client_id,
        'Authorization': 'Bearer ' + access_token
    }
    response = requests.get(url, params=params, headers=headers)
    if response.status_code == 200:
        json = response.json()
        return json['uploadUrl'], json['authCode']
    else:
        print('upload-url: ' + str(response.status_code) + ': ' + response.reason)

The function returns the URL we upload the file to in the next step, plus the authentication code that action requires.

Step 3: Upload the APK with the URL obtained in the previous step

This function performs the upload itself and only needs the path to the file. The maximum size for an APK is 4 GB; a larger one has to be uploaded in chunks through a different endpoint, described in: Uploading a File by Chunk

def upload_file(upload_url, auth_code, path_file, access_token, client_id, app_id):
    headers = {
        "accept": "application/json"
    }
    body = {
        'authCode': auth_code,
        'fileCount': '1'
    }
    with open(path_file, 'rb') as f:
        response = requests.post(upload_url, files={'file_name': f}, data=body, headers=headers)
        if response.status_code == 200:
            json = response.json()
            fileInfoList = json['result']['UploadFileRsp']['fileInfoList'][0]
            update_app_file_info(file_url=fileInfoList['fileDestUlr'],
                                 file_size=fileInfoList['size'],
                                 client_id=client_id,
                                 access_token=access_token,
                                 app_id=app_id)
        else:
            print('upload-file: ' + str(response.status_code) + ': ' + response.reason)

Step 4: Update the application information to announce that there is a new package uploaded in the previous step

We run this last step inside the previous function, so it fires as soon as the upload finishes. Uploading the file is not enough: we have to tell AppGallery that a new package is available for the app.

def update_app_file_info(file_url, file_size, client_id, access_token, app_id):
    url = 'https://connect-api.cloud.huawei.com/api/publish/v2/app-file-info'
    headers = {
        'client_id': client_id,
        'Authorization': 'Bearer ' + access_token
    }
    body = {
        'fileType': 5,
        'files': [{
            'fileName': 'this_is_a_new.apk',
            'fileDestUrl': file_url,
            'size': file_size
        }]
    }
    params = {
        'appId': app_id
    }
    response = requests.put(url, headers=headers, json=body, params=params)
    if response.status_code == 200:
        json = response.json()
        pkgVersion = json['pkgVersion'][0]
        msg = json['ret']['msg']
        code = json['ret']['code']
        print(str(pkgVersion) + ', ' + msg + ', ' + str(code))
    else:
        print('app-file-info: ' + str(response.status_code) + ': ' + response.reason)

Note that one of the parameters is the name the file is registered under. Here it is a constant, but it could be built dynamically from the version, the flavor, and so on.

Run the script

Running the script means calling those functions and passing in everything we prepared earlier. We read it from the arguments with the sys module:

app_id = str(sys.argv[1])
client_id = str(sys.argv[2])
client_secret = str(sys.argv[3])
path_file = str(sys.argv[4])
print(app_id, client_id, client_secret, path_file)
access_token = get_token(client_id=client_id,
                         client_secret=client_secret)
upload_url, auth_code = get_upload_url(access_token=access_token,
                                       client_id=client_id,
                                       app_id=app_id)
upload_file(upload_url=upload_url,
            auth_code=auth_code,
            path_file=path_file,
            access_token=access_token,
            client_id=client_id,
            app_id=app_id)

We commit the script to the repository so GitHub Actions can use it.

GitHub Actions

Now we only need to run the script from the CI/CD YAML file. Some of the data it takes is sensitive, and the answer is the same as in part 1: the repository Secrets.

Save the Client Id and Client Secret in Secrets, under the repository Settings.

App Gallery Connect API

All that is left is to call the script.

- name: Run Script
  run: |
    python --version
    pip --version
    pip install requests
    python my_script.py "102297755" ${{ secrets.CLIENT_ID }} ${{ secrets.CLIENT_SECRET }} app/build/outputs/apk/release/app-release.apk

A few reminders: the App Id can be a parameter rather than a constant, and it is not private; secrets are read with ${{ }}; and the signed APK ends up at app/build/outputs/apk/release/app-release.apk.

The Result

On every commit to the branch, or on a manual run, the log shows the APK being uploaded. Note that no matter how hard the script tries to print a GitHub secret, the console only ever shows ***.

App Gallery Connect API

When it finishes, the AppGallery console shows the package that was uploaded automatically: a properly signed APK, ready for release. If it is a new version, a new section appears under Version Information.

App Gallery Connect API

Conclusion

Using the Publishing API we reached the goal: a process that is tedious to do by hand now happens on its own.

What’s next

To finish the process, the release itself could be pushed automatically, along with new screenshots, an updated description, a bumped version, or a build sent straight to a group of testers. There is a lot left to automate, and a lot of Connect APIs to do it with.

You can see the yml file here:

.github/workflows/android.yml

And you can see the script here:

my_script.py

Implemented in a real project can be seen in:

facts-android