Building APKs from Slack using Circle CI and Firebase cloud functions

For small teams or small projects, it might not be easy to have full self hosted CI/CD pipelines for building apks. Sharing apks within the team for testing purposes might also be difficult. With a simple Slack app and CircleCI integration in the project, we can have automated apk building and anyone can trigger the build from Slack for a branch and build variant.


/build-apk [branch] or [buildVariant]|[branch]

alt text


The whole process will look like this:


/build-apk slash command.

Create a new Slack app and setup a slack command as below -

alt text

Slack will send a HTTP POST request to the Request URL mentioned when creating the Slash command. We can enter the url for the firebase cloud function here.


Firebase cloud function that handles the Slack command request and triggers Circle CI job

If your project uses Firebase, you can create the cloud function in your Android project repo itself. Run firebase login and firebase init functions in your project directory.

Let’s setup the function to recieve POST request and trigger build on Circle CI using Circle CI API trigger.

Note that there are some issues in firebase cloud function working with unescaped characters due to which we are avoiding using space in the slash command. We can use pipe | as the separator for build variant and branch name as branch names can often have / or -.

One thing to note here is that Slack expects a response within 300ms after executing slash command or else it will show operation_tiemout error. Initialising the function might take more than 300ms and then waiting for Circle CI api trigger response will certaintly cross 300ms time limit. To work around this, we write the response instantly using response.write() and then trigger the Circle CI API in response of which we finally end the response. Note that if you instantly end the response with res.end() the cloud function will terminate soon after so we can’t do response.send() or response.end() before the circle ci api trigger is complete.

Also see Circle CI- Using the API to trigger jobs

src/index.ts

import * as functions from 'firebase-functions';
import https = require('https');

const CIRCLECI_API_TOKEN = "your_circleci_api_token"

export const buildApk = functions.https.onRequest((request, response) => {

    const user = request.body.user_name

    let variant = 'debugRelease' // default buil variant
    let branch = 'master' // default branch

    if (request.body.text.includes('|')) {
        // both variant and branch are present
        variant = request.body.text.split("|")[0]
        branch = request.body.text.split("|")[1]
    } else {
        // only branch is present and we use default variant
        branch = request.body.text
    }

    // response to be sent back to Slack after executing command
    const responseString = `Build started by *${user}*-\nBranch: ${branch}\nVariant: ${variant}`
    
    response.writeHead(200, {'Content-type': 'application/json'})
    response.write(`{"response_type": "in_channel","text": "${responseString}"}`);

    let job = 'debug-build' // default circle ci job

    // set jobs for different variants
    if (variant == 'debugRelease') job = 'debug-release-build'
    if (variant == 'release') job = 'release-build'

    const data = JSON.stringify({
        "build_parameters": {
            "CIRCLE_JOB": job
        }
    })

    const options = {
        hostname: 'circleci.com',
        path: `/api/v1.1/project/bitbucket/org/project/tree/${branch}`,
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Content-Length': data.length,
          'Authorization': 'Basic ' + new Buffer(CIRCLECI_API_TOKEN + ':').toString('base64')
        }
    }
    const req = https.request(options, (res) => {
        console.log(`statusCode: ${res.statusCode}`)
      
        res.on('data', (d) => {
            response.end()
        })
    })
      
    req.on('error', (error) => {
          response.end()
    })
      
    req.write(data)
    req.end()
});

Circle CI config for building APKs and uploading to Slack

.circleci/config.yml

version: 2.1

jobs:

  build: # this can be your default job for running tests
    working_directory: ~/code
    docker:
      - image: circleci/android:api-28-alpha
        
    steps:
      - checkout
      - restore_cache:
          key: jars--
      - run: # separate step to be able to cache the dependencies depending on build.gradle
          name: Downloading Dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars--
      - run: 
          name: Run unit tests
          command: ./gradlew :app:testDebugUnitTest :data:testDebugUnitTest :domain:testDebugUnitTest
  

  debug-build: # job to create debug build variant
    working_directory: ~/code
    docker:
      - image: circleci/android:api-28-alpha
    steps:
      - checkout
      - restore_cache:
          key: jars--
      - run: # separate step to be able to cache the dependencies depending on build.gradle
          name: Downloading Dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars--
      - run: 
          name: Building APK
          command: ./gradlew :app:assembleDebug
      - run:
            name: Upload to Slack
            command: curl -F file=@$(find app/build/outputs/apk/debug -name 'app-debug*') -F channels=qa-android -F token=$SLACK_TOKEN -F filename=$(find app/build/outputs/apk/debug -name 'app-debug*' -exec basename {} \;)  https://slack.com/api/files.upload

           
  debug-release-build: # job to create debugRelease build variant
    working_directory: ~/code
    docker:
      - image: circleci/android:api-28-alpha
    steps:
      - checkout
      - restore_cache:
          key: jars--
      - run: # separate step to be able to cache the dependencies depending on build.gradle
          name: Downloading Dependencies
          command: ./gradlew androidDependencies
      - save_cache:
          paths:
            - ~/.gradle
          key: jars--
      - run:
          name: Building APK
          command: ./gradlew :app:assembleDebugRelease
      - run:
            name: Upload to Slack
            command: curl -F file=@$(find app/build/outputs/apk/debugRelease -name 'app-debugRelease*') -F channels=qa-android -F token=$SLACK_TOKEN -F filename=$(find app/build/outputs/apk/debugRelease -name 'app-debugRelease*' -exec basename {} \;)  https://slack.com/api/files.upload

Circle CI config will be your existing config with an extra step for uploading generated artifact to Slack. We can run following CURL to upload file to Slack -

curl -F file=@app/build/outputs/apk/debug/app-debug.apk -F channels=qa-android -F token=$SLACK_TOKEN -F filename=app-debug.apk https://slack.com/api/files.upload

The Slack token can be found in the OAuth & Permissions section of your app homepage. Note that you will have to give the bot the files:write and commands permissions.

If your gradle configuration updates the output apk name according to current branch and version like app-debug-v7.8.50-master.apk, then you will need to slightly change the final step to find the newly built apk.

filePath = $(find app/build/outputs/apk/debug -name 'app-debug*')

fileName = $(find app/build/outputs/apk/debug -name 'app-debug*' -exec basename {} \;)


That’s it! Now you can get the APKs by just running the /build-apk command from Slack.

Notes

signingConfigs {
    debug {
        keyAlias 'androiddebugkey'
        keyPassword DEBUG_KEY_PASSWORD
        storeFile file(project.rootDir.path + '/debug.keystore')
        storePassword DEBUG_KEYSTORE_PASSWORD
    }
}

buildTypes {
    debug {
        signingConfig signingConfigs.debug
    }
}

Posted by Naman Dwivedi on 13 Apr 2020

Tags- Android ,Circle CI, Slack, APK