Continuous integration and deployment had never been topics I gave much thought about. Either the company I was working for had a solution in place that I wasn’t able to modify due to tool restrictions, or the freelance project was too small to spend the time and money on CI/CD. While my knowledge and exposure was limited, when I first started at ActiveCampaign I focused on setting up a continuous integration and deployment plan for the brand new Android app. This article will give a high level overview of what was implemented.
Linters are tools that can help analyze your code for errors, bugs, style issues. While these are checks are usually run locally, they become even more useful when integrated with CI. The 2 linters we use for our project are ktlint and Lint.
Enforce coding style. On a previous project I learned that it’s near impossible to add a code style checker to a legacy project. A week of my time was dedicated to fixing violations across hundreds of files. At the time of joining ActiveCampaign the team was only 2 strong, but there was anticipation of the team growing. And as this project was close to brand new, it made sense to set this up before the project grew. I used CheckStyle for most of the Android projects I worked on in the past but since this new project was going to be written in Kotlin, ktlint was used. The only configuration we added was enforcing a max character per line limit.
The lint tool built into Android Studio can help uncover a wide range of issues/warnings, from unused resources to poorly structured code. This lint check is another way to enforce a clean code base outside of coding style. There’s little to no setup for this tool, so I can’t think of any reason not to add it to your CI flow.
With a fairly new code base, the team is trying our best to stay strict on having tests for all the code that we’re writing. While I won’t get into the details of what our testing strategy looks like, my teammate Cody has written a great article on Approaching Testing on Android. Writing tests is always good but I’d argue that running them, and running them often, is just as important.
It happens. We all forget to run the unit test suite before opening a pull request. If passing unit tests are required for merging a pull request, the tests in the
develop branch should always have passing tests. This ensures that builds that are to be tested or released do not have broken functionality. We have our standard JUnit4 test suite running for each commit pushed to an origin branch. If there are any failures, the Circle Workflow will stop.
This was probably the most tricky step to set up as this was my first time integrating Espresso test with automated builds. I was quickly blocked by CircleCI 2.0 not natively supporting the Android Emulator. The only somewhat documented solution I found was using Firebase Test Lab within Circle build jobs. Gcloud did take some trial and error to get running, but it seems to be running the instrumentation test suite reliably.
Since Firebase Test Lab is free only up to a certain number of tests, I tried to find a way to only run the Espresso tests build job for main branches (
master) which is straight forward using Circle’s workflow
filters. But I also waned to make sure that code changes in a pull request that had failing Espresso tests would not be allowed to merge into
develop. I tried to find a way to only run the Espresso tests build job when a pull request was approved, this would prevent running espresso tests for every commit pushed to the open pull request. I looked into Github webhooks, but wasn’t able to get it working. So for now Espresso tests will run for every build.
Once our linters and tests are all passing, we can build the actual APK for distribution. We initially had our designer and product manager grab the APK from build job’s artifacts tab. While this was working, it wasted a lot of time from downloading builds and having to manually install them.
Google Play Console’s Internal Test Track
Google recently added the Internal Test Track on the Play Console to go along side Closed, Open, and Production tracks. This new track provides a way for internal testers to receive test builds via the Play Store. Similar to the Closed track, testers email addresses are added to the distribution list and will see all new builds as app updates in the Play Store. This removes testers from having to worry about CircleCI and manually staying up to date with the latest build.
There is some setup required for pushing internal builds to the Play Store. Since these APKs would be uploaded to the Play Store, they’ll need to be signed before the Play Store accepts them. The easiest way I found was to add the signing step in Gradle for the build types that were to be deployed via the Play Store. The other thing to figure out was how to push builds from Circle to the Google Play Console. Fastlane seemed to be exactly what we needed and supported uploading to different tracks.
The setup for production builds is almost identical to the internal test builds. The only difference was configuring Fastlane to deploy to the Alpha track. We’re using the Alpha track as a “staged” rollout, which would be promoted to the Production track once it was ready to be released.
For the most part our solution has worked well here at ActiveCampaign (we’re hiring!) and has allowed us to release with minimal to no manual steps. I hope this article provides some guidance for those who are setting up a CI/CD solution for a new project and I plan on writing future articles on implementation details of each step.