Improving CI/CD pipeline for Android via Fastlane and GitHub Actions

Build better & faster 🚀

CI/CD bridges the gaps between development and operation activities and teams by enforcing automation in the building, testing and deployment of applications. — Wikipedia

CI/CD is not new, and it has been at the forefront of adopting good software engineering practices. While it is vital for all, it becomes even more critical for companies that are just getting started, typically startups. They follow the infamous quote of moving fast and breaking things. When the system begins becoming unstable, the need for proper automation(development/testing/deployment) kicks in while still maintaining development and release speeds. It eventually boils down to adopting a proper CI/CD pipeline right from the beginning and improving upon it as the underlying services/apps scale. This article provides an overview of the core concepts for building a CI/CD pipeline for Android via Fastlane and GitHub Actions.

Sit back and onboard the build automation rocket 🚀

Building CI/CD pipeline 🔨

You can skip this section if you do not have any private libraries.

Preface: Migration from JFrog’s Artifactory to GitHub Packages

If your Android app relies on several in-house libraries, you would be probably using JFrog’s Artifactory to manage your repositories served via Maven. While Artifactory is incredible in its own way, you lose out on the principle of a single source of truth. That typically means that if your code repository is on GitHub, you will ideally want all the related packages also available in the same place. This helps in better maintenance and visibility. When you start setting up a CI/CD pipeline on GitHub action, you will face the first blocker as the packages are hosted privately on Artifactory via intranet requiring a VPN connection. Here are some out-of-the-box solutions:

  1. Go Public  -  You can make Artifactory publicly accessible, obviously under the authenticated environment. If the packages are private, why would you want them to be hosted publicly? Also, isn’t it still defeating the idea of a single source of truth?

  2. Use Self-Hosted Runners -  Github also provides the power to use self-hosted runners to customize the environment according to your workflow needs. Since the environment will be hosted and created according to your needs, you can easily pull out the private libraries while building the app on Github action. This sounds good but fails due to the massive efforts it would require to build and stabilize while still investing time in the maintenance of it.

“We can not solve our problems with the same level of thinking that created them”
Albert Einstein

As mentioned earlier, the aim of the pipeline is to have a cleaner separation of concern with clearer visibility having GitHub as the single source of truth (code, wiki, versioning, changelog, actions, releases, etc.) for your repositories. Proper version/release management, faster deployment, and deeper integration with GitHub services will help in running a smoother pipeline and enhance it for future needs.

If you are already publishing your libraries to Artifactory via Maven, it would require little effort in build.gradle file to start publishing .aar files on GitHub. You can maintain agithub.properties that contains github_token and github_username to give the capability to publish locally for fallback. The pom file for the .aar packages will also be published under GitHub packages.

Using ./gradlew publish locally, you can test your integration and find that the libraries are properly getting published on GitHub packages under your targeted repository.

Minimal changes are required in your Android project’s build.gradle file to consume the library.

Github Action and Fastlane working together

Once you have the in-house libraries being pulled in via GitHub, you can start creating the workflow for your Android app on GitHub actions. You can have two different strategies depending upon pushes on the target branches(development & master) These are aimed at not only removing the manual intervention altogether by having continuous integration embedded deep inside the workflow but also focussing on continuous deployment by automating the overall release process right from building the bundle and uploading to the Play Store. Let's discuss the important steps in the workflow:

  • No hardcoding  - Every variable used in the workflow, is made to come via GitHub secrets. For creating a release build, you can first encrypt your .jks and release.properties files containing signing information and then store them on GitHub secrets.

      // Encryption
      gpg --armor --symmetric release.properties
      // Decryption 
      gpg -d --passphrase "<Password used for encryption stored as a GitHub secret>" --batch release.properties.asc
    
  • Similarly, to upload your release on the Play Store, you can encrypt your Google service account file and then save it as a secret.

      # Decrypt release keystore and release properties file from github secrets
      - name: Decrypt files
        id: decrypt_files
        run: |
        # Creating encrypted keystore file from secret saved on GitHub
          echo "${{ secrets.YOUR_APP_RELEASE_STORE }}" > customer.jks.asc
        # Decrypting keystore from passphrase(saved as secret) used earlier for encryption
          gpg -d --passphrase "${{ secrets.YOUR_APP_RELEASE_PASSWORD }}" --batch customer.jks.asc > config/customer.jks
        # Creating encrypted release.properties file from saved secret.
          echo "${{ secrets.YOUR_APP_RELEASE_PROPERTIES}}" > release.properties.asc
        # Decrypting release.properties.asc with passphrase(saved as secret) used earlier for encryption
          gpg -d --passphrase "${{ secrets.YOUR_APP_RELEASE_PASSWORD }}" --batch release.properties.asc > config/release.properties
        # Creating encrypted play service account file from saved secret.
          echo "${{ secrets.YOUR_PLAY_RELEASE_SERVICE_ACCOUNT}}" > <your_play_service_account_filename>.json.asc
        # Decrypting play service account with passphrase(saved as secret) used earlier for encryption
          gpg -d --passphrase "${{ secrets.YOUR_APP_RELEASE_PASSWORD }}" --batch <your_play_service_account_filename>.json.asc > <your_play_service_account_filename>.json
        # Creating tester-groups file used for publishing on firebase app distribution
          echo "android,qa" > tester-groups.txt
        # Creating empty github.properties to avoid the build failure since our local builds depend on it
          echo " " > github.properties
    
  • Setting up Fastlane and Firebase tools — If your app is already using Fastlane, you can run Fastlane commands manually to trigger a build and send it on Slack and Firebase App distribution. It is about auto-triggering those commands from your workflow and the rest will be taken care of by Fastlane itself. Slack is used to communicate the status of our build workflow and send .apkor .aab to keep the concerned folks notified in real-time by using different GitHub Actions described further in the article.

Firebase App Distribution enables you to instantly manage and deliver pre-release versions of your app with trusted testers in order to get feedback and find issues ahead of releasing to production.
You can now distribute android app bundle too from Firebase App Distribution 🎉

  • Faster workflow  - Set up Gradle, Download the dependencies, Set up Ruby, Install Firebase Tools, Set up Fastlane, Install Bundler, etc are the steps that get executed for every workflow trigger. This will affect your GitHub Actions in billing minutes leading to higher payments. You can use [actions/cache@v2](https://github.com/actions/cache) to have a faster workflow execution time.

    %[gist.github.com/reactivedroid/c7e3bf75da50a..

You can use Gradle cache and Gems cache to avoid downloading the jars and gems if there is no change in the dependencies. It is important to create a cache key in such a way that it can be invalidated when required thus avoiding stale builds. For Gradle cache, checksum acts as the cache key which is calculated by running the checksum script( [checksum.sh](https://github.com/chrisbanes/tivi/blob/main/checksum.sh))[Thanks to Chris Banes]. For gems, the hashFiles(Gemfile.lock) acts as the cache key. Once there is a Gradle/Gems cache hit, the setup time will drastically reduce giving faster workflow runs.

  • Running UI/Unit tests — UI/Unit tests play a vital role in making the app robust. It helps to track any broken features upfront when a huge refactoring/re-architecture task takes place. If the Unit/UI tests are not working as expected, you can fail the CI/CD pipeline to keep the tests always up-to-date with the newer changes. Once the tests have been executed successfully, you can upload the test reports and results on the workflow for future analysis.

    %[gist.github.com/reactivedroid/abf7f60c34c5f..

  • Creating release builds— Once you have made the necessary setup to run Fastlane via bundle, you can execute lane based on the branch push type development& master to create debug and release builds respectively. You can club all the steps under one workflow separated if: github.ref == ‘refs/heads/master’to have your development and release builds go seamlessly. Let’s break this down even further.

  1. Distributing app bundle via Fastlane - You can write a lane which will bundleProdRelease and then push the release tag on themaster branch. This will then publish the build on the Slack channel and Firebase App Distribution. This is how the lane looks like in fastfile

    %[gist.github.com/reactivedroid/0dbbe11ebec06..

  2. Lane Options on Fastlane - Fastlane needs some values to run the distribute_bundle lane. You can store the required values as secrets on GitHub and then inject them into Fastlane as lane options to be used later.

    %[gist.github.com/reactivedroid/c8c513a5643a2..

  3. Creating GitHub release from Workflow  - Once the release tag is pushed on Git via build_prod_release step above, you can save it as tag_name and use it to create our GitHub release. To know what actually went in the release, you can check-in changelog.mdor release_notes.txt into version control. This file will be used to push releases both on Firebase app distribution and GitHub. You can also use the checked-in release notes to create your GitHub releases right from workflow while uploading other assets like bundle/apk.

    %[gist.github.com/reactivedroid/cc1c5e7554c61..

  4. Uploading to the Play Store  - The final piece is automating the deployment process from the workflow. You can create a service account file from Google Cloud Console, associate it with Play Store by giving necessary permissions and use it in your workflow to upload on internal track. Once the build is approved by the Play folks, you can promote it to production.

    %[gist.github.com/reactivedroid/0b238fd16e5da..

  5. Uploading build outputs and send status- Hope you are still with us :) The final step includes uploading all the build outputs on workflow and then notifying about its status on Slack via action.

    %[gist.github.com/reactivedroid/6f7f04470bd67..

Phew! That was a long read, right 😌. I understand that creating a seamless CI/CD workflow for Android builds can be tricky and it becomes even more complicated when you want to automate each and every step from continuous development to production.

For that reason, I have tried my best to provide you with as many details as possible so that setting up CI/CD pipeline should be a fairly easy task going forward and you can reap its benefits in the longer run. Obviously, this smooth setup would not have been viable without the ever-growing open-source contributions ❤️. You can use slack action to get the status of your workflow right into your Slack channel. Sweet, isn’t it!

Conclusion

This is not the only way to create a CI/CD workflow on GitHub action and I am all ears to listen to your creative ideas. I will feel more happy and content if this article helps you in any way possible. Till then, keep learning and sharing. #BetterTogether