Ivan Garza Ivan is an Android engineer at Mixhalo, a passionate fútbol fan, and an amateur salsa maker.

Android CI/CD using GitHub Actions

8 min read 2370

Android CI/CD Using GitHub Actions

CI/CD is a DevOps practice that allows development teams to push changes that get automatically tested and sent out for delivery and deployment. According to GitLab, a good CI/CD pipeline can maximize development time by improving an organization’s productivity, increasing efficiency, and streamlining workflows through built-in automation, testing, and collaboration.

Modern software development teams have became increasingly reliant on CI/CD practices — or continuous integration and continuous delivery, to be more precise — as they save developers and testers alike precious time by automating much of the manual labor that is needed to get the code from a development state and into production.

With the increased interest and dependence of software companies on CI/CD tools, many new options have been appearing that offer similar products aimed at facilitating and automating much of the development process for programmers. In this article, I will go over one such tool, GitHub Actions, and its application into modern Android development.

Jump ahead:

An introduction to GitHub Actions

GitHub Actions is GitHub’s answer to the increasing demand for good and simple CI/CD tools. Because of the virtual proximity of the GitHub Actions pipeline to your code, this tool also allows for them to go beyond just DevOps and lets you run workflows when other events happen in your repository. For example, you can run a workflow to automatically add the appropriate labels whenever someone creates a new issue in your repository.

Instead of diving into GitHub Actions’ broad functionality, we’ll instead do a quick comparison with some of its competition, and follow that by showing how to get it set up for Android. For more information about what GitHub Actions are and what they do, check out what my fellow LogRocketeer had to say about the topic in this article, or go straight to the source and read the GitHub Actions official documentation.

Why GitHub Actions?

Before continuing our exploration into why GitHub Actions is a better option than other CI/CD tools, let’s first make a couple of disclaimers. First, for the purpose of this exploration, I have briefly researched alternative tools like Bitrise, CircleCI, and TeamCity by JetBrains. Second, while the pricing of these tools varies greatly for many different factors, the considerations that we have taken will be from the perspective of an independent developer with a codebase hosted as a public GitHub repository.

Advantages

There are two main reasons why GitHub Actions is a better option over more traditional tools like Bitrise, CircleCI, and TeamCity.

Price – We’ll start with the most common concern for teams of all sizes. While most competitors do offer free version of their software and only charge clients after a certain threshold, GitHub Actions goes one step further to facilitate this.

GitHub Actions is free for all standard GitHub-hosted runners in public repositories, and for self-hosted runners, according to it’s official documentation. For private repositories, each GitHub account receives a certain amount of free minutes and storage for use with GitHub-hosted runners.

Seamlessness – Did I mention that GitHub Actions lives one server down from your code repositories? The biggest advantage that GitHub Actions has over its competition is the fact that this tool is already integrated with your GitHub project and account(s). While traditional CI/CD tools require you to link your same GitHub account to their servers, GitHub Actions comes almost included with your repository already, and it is readily available with the click of a button.

Disadvantages

Using GitHub Actions is not always perfect. Let’s go over the biggest disadvantage that using GitHub Actions may bring upon in contrast to its competition.

Youth – Differently to all of the tools that we’re comparing it to, GitHub Actions is by far the youngest CI/CD tool that we’ve mentioned today. This is a very clear disadvantage due to the fact that older platforms like TeamCity and CircleCI would have not only gone through some similar problems as GitHub Actions may have, but they’ve had more time to harden their tech, as well as to create more integrations and support for their users.

GitHub Actions for Android

Now that we’re all convinced that GitHub Actions may be the most advantageous choice for a CI/CD tool, let’s grab a ride back with the Droid, and get our pipeline integration started through a simple Actions script.

GitHub Actions uses YAML script files for each workflow, where a workflow is a configurable automated process that will run one or more jobs, according to the GitHub docs, and these may run sequentially or in parallel. Workflows are generally triggered by events, such as a pull request being created, or a new tag being pushed, but workflows may also be triggered manually at any time.

Workflow scripts live in the .github/workflows/ location from the root directory of your repo. Where a push.yaml file’s full path would be .github/workflows/push.yaml. The following snippet of the top part of push.yml will show the basic setup of a simple workflow that runs after every push to master or develop:

name: Push

on:
  push:
    branches: [ "develop", "master" ]
  workflow_dispatch:

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
        - run: echo "The job was automatically triggered by a ${{ github.event_name }} event."

Let’s go over each of these fields before we get into the steps section:

name: The name of the workflow as it will be displayed in the GitHub Actions page

on: This is where you specify the kind of events that may trigger this workflow. In this case, it will be triggered for every push coming into either master or develop

workflow_dispatch: This flag lets the system know that you want to trigger this workflow manually from the interface

jobs: Workflows may have one or more jobs, which run in parallel by default but may be run sequentially instead. In this example, we only have one job called build with a name parameter of value Build

runs-on: Indicates the kind of machine this job is going to run inside of

steps: Each job has a series of steps that may contain either a shell command or a public action from the GitHub marketplace to be run. In this example, the single step shown is a simple echo command with a message containing the name of the event that triggered this workflow

Android-specific Actions

There are many different steps that can be taken for any given GitHub Action. For the purpose of this article, we’ll go over some of the most conventional steps used for the Android development ecosystem.



The next example will show the bottom part of the push.yml file with all the steps needed to build the project after every push event:

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
        - run: echo "The job was automatically triggered by a ${{ github.event_name }} event."
        - run: echo "This job is running on a ${{ runner.os }} server hosted by GitHub!"
    - uses: actions/[email protected]
    - run: echo "The ${{ github.repository }} repository has been cloned."
    - run: echo "Setting up JDK" 
    - name: set up JDK 11
      uses: actions/[email protected]
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: gradle
    - run: echo "The workflow is now ready to test your code."
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - run: echo "Building Debug APK."  
    - name: Build with Gradle
      run: ./gradlew build
    - run: echo "Build status report=${{ job.status }}."

There’s quite a lot of noise through echo commands on this example, but let’s go over all functions of type run that don’t print to the screen, as well as uses commands.

The first such example is our third step, uses: actions/checkout#v3. This action is simply checking out the repository into the machine where this job is running.

Followed by two more prints commands, the next action has a particular name field that explains what the uses: actions/[email protected] action is doing. This same action is followed by some metadata inside the with tag, which is specifying the Java version and the build tool being used; in this case, it is Gradle.

We have a few more messages printed as well as some permissions refactor, but on one of the last lines, we find our last command run: .gradlew build, which simply builds the debug version of project. This command is followed by a print reporting the status of the build.

Advanced GitHub Actions topics

Before we move on to more advanced scripts, let’s take a step back and look at the topic of secrets. GitHub secrets is a safe place to hide confidential data, like signing or API keys. GitHub Actions may then take these secrets and use them as part of its actions and commands.

For more information about how to create GitHub secrets, their relationship to the Android ecosystem, and how to use them within GitHub Actions, check out this article.

Build and sign a release build

For our next example, we’ll go over a job that builds the release build of our project, and signs it using a keystore found inside GitHub secrets:

jobs:
  build:
    name: Generate App Bundle
    runs-on: ubuntu-latest
    steps:
    - uses: actions/[email protected]
    - name: set up JDK 11
      uses: actions/[email protected]
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: gradle
    - name: run: chmod +x gradlew
    - name: Bundle 'release' with Gradle
      run: ./gradlew bundleRelease
    - name: Sign AAB
      id: sign_aab
      uses: r0adkll/[email protected]
      with:
        releaseDirectory: app/build/outputs/bundle/release
        signingKeyBase64: ${{ secrets.SIGNING_KEYSTORE }}
        alias: ${{ secrets.SIGNING_ALIAS }}
        keyStorePassword: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
        keyPassword: ${{ secrets.SIGNING_ALIAS_PASSWORD }}  
    - run: echo "Build status report=${{ job.status }}."
    - name: Upload App Bundle
      uses: actions/[email protected]
      with:
        name: aab
        path: ${{steps.sign_aab.outputs.signedReleaseFile}}

Skipping right into the new commands only from the example above, we first find run: ./gradlew bundleRelease, which, differently to the build command from our first example, triggers a build with the release build variant, and makes an App Bundle .aab file instead of the regular .apk file. To generate an APK instead, a simple call of ./gradlew buildRelease will get the job done.

The next new command we’ve got is uses: r0adkill/[email protected]. This action was create by the community. It simply takes the freshly generated build file, in this case in the form of an AAB, and signs it using the certificate and variables found inside the nested with: container. As we can see, we’re using our GitHub secrets as part of the command mentioned before.

The last new command we have in this second example is uses: actions/[email protected]. This action simply takes a variable under the given path, and saves it using the given name for later use within the same workflow. More on that on the next and final example.

Deploying into GitHub packages

For our last example, we’ll go over one last job that uses the job from above to get a resulting AAB, and upload that into a GitHub Release for the given repository:

jobs:
  release:
   name: Release App Bundle
   needs: build
   runs-on: ubuntu-latest
   steps:
     - name: Download AAB from build
       uses: actions/[email protected]
       with:
         name: aab
     - name: Create Release
       id: create_release
       uses: actions/[email protected]
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       with:
         tag_name: Tag Name
         release_name: Release Name
     - name: Upload Release AAB
       id: upload_release_asset
       uses: actions/[email protected]
       env:
         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       with:
         upload_url: ${{ steps.create_release.outputs.upload_url }}
         asset_path: aab/app-release.aab
         asset_name: ${{ github.event.repository.name }}.aab
         asset_content_type: application/zip
     - run: echo "Upload returned with status=${{ job.status }}."

There are three main functions here, with a final print indicating the status of final upload job. Before we look at each command one by one, first we should address the new field we added into this workflow. The field needs presupposes that there’s another job with the build name that this job will execute, and potentially utilize any assets the preceding job might upload.

Returning to the commands, we start with uses: actions/[email protected] , which downloads an asset of name aab, the same asset our last example uploaded. This means that this first command assumes that a job like the one from our last example is ran beforehand in order to find the required artifacts.

Moving on to the next action, uses: actions/[email protected] attempts to create a new GitHub release for the given repository, using the GitHub token found in the secrets to identify the environment. Additionally, this function takes both a tag_name and release_name in which we’ve added placeholders in order to keep the example a bit simpler.

The last function that we haven’t seen before in this last example is uses: actions/[email protected]. This action is taking the .aab file inside our asset through the asset_path, defined by the asset_content_type variable and having the name from asset_name. This asset is then attempted to upload into the upload_url path. The action emits a success result status at the end, which we print out in the next and last echo command right after.

Conclusion

Evidently, there are a lot more acts that can be done as part of the CI/CD pipeline using GitHub Actions. These include steps as fundamental as testing your code, to automating complex flows that span multiple jobs and chain multiple workflows.

My hope is that this guide serves as a starting point for the readers to quickly get GitHub Actions up and running, and that they’re able to expand on this basis to cover their particular CI/CD needs. With the help of the GitHub community, finding new and useful actions to everyone’s respective needs shouldn’t be a tall order, and we’ve all got to start somewhere!

LogRocket: Instantly recreate issues in your Android apps.

LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your Android apps — .

Ivan Garza Ivan is an Android engineer at Mixhalo, a passionate fútbol fan, and an amateur salsa maker.

Leave a Reply