Streamlining eero’s Android build process

How we catch bugs early and improve team communication

A good build process makes everyone’s lives easier. Running tests and static code analysis gives developers confidence and peace-of-mind. Product managers and QA benefit from automatically shared builds that clearly indicate changes. Thus automation also improves team communication. At eero, we’ve built on different tools and ideas to make a build process that we think is pretty great.

Requirements for our build process

To truly make our lives easier, this build process needed to be:

  • Stable — The build process should always work
  • Comprehensive — Pull requests are tested; style checks and static code analysis run before merging
  • Communicative — Repetitive work that facilitates team communication is automated

Services used

We use some of the most popular services in the industry. Readers using different services should be able to find similar configurations that work for them.

  • Source control: GitHub
  • Continuous Integration server: Jenkins
  • Issue tracker: JIRA
  • Static code analysis: SonarQube
  • Build distribution: Beta by Fabric

Steps for an awesome build process

The focus of this article is the automation that happens once a user opens a pull request up to the point where that code is merged into a development branch. However, to catch bugs early, we still need to start with our development environment. The most important parts of our build process can be divided into several steps:

  1. Run checks in local environment
  2. Run tests on pull requests
  3. Run static code analysis on Pull Requests
  4. Update ticket status with build number after successful build
  5. Distribute builds with release notes to team members
The checks and services used as our code goes on its journey to production. The most valuable parts start once a pull request is opened.

1) Do checks in local environment

Our focus is after a pull request has been opened, but that doesn’t mean we can ignore local development. We try to catch issues before they are presented to everyone in a pull request.

One way to avoid conflicts and ensure some style consistency is to share the same Android Studio settings for code formatting and arrangement. Just export the styles, check them into version control, and then everyone on the team can import the same rules.

An example macro called “Format and Arranged Save” with its steps

We created a “Format and Arranged Save” macro that automatically rearranges and reformats code before saving. We then override the default save keybinding, so that the code is properly styled every time ⌘-s is pressed.

These tricks are great for style and formatting, but we also want to catch real issues. That’s where SonarQube comes in.

SonarQube is a standalone app that you should run on a server to keep track of tech debt, monitor changes over time, and configure the rules for your project. It even checks for code duplication and complexity. On top of all that, PMD, Findbugs, and Checkstyle are included in the default Java profile. You will, however, want to install the Android Lint plugin to include Android-specific checks, which I will get to later.

Sample SonarLint output for a Java project from SonarQube’s website

It gets better — although SonarQube runs on a server, there is an IntelliJ plugin that provides instant feedback in Android Studio by running an incremental analysis on the open files or the entire project. The errors are easy to read and have helped us a lot.

2) Run tests on Pull Requests

Tests are useless if they aren’t being used. Including tests in the continuous integration process also encourages a culture of writing tests and testable code.

More testing should be done automatically. It’s important to note that by “automatically” we meant that the test results are interpreted automatically as well. -Andrew Hunt, The Pragmatic Programmer

In addition to running tests for all pull requests, we require that all tests pass before a pull request can be merged. To enforce this requirement, we enabled protected branches and status checks on Github.

To run these tests, we start a Jenkins build whenever a pull request is opened. To automatically run this build and send the results to GitHub, install these Jenkins plugins:

Once those plugins are installed, make a new build that will be run for every new pull request; for example, “Android Pull Request Builder.” Configure this build to run whenever a pull request is opened. The GitHub Pull Request Builder plugin docs do a good job describing how to configure the plugin.

After the Pull Request Builder plugin is configured, your new Jenkins job will run whenever a pull request is opened. To run unit tests for this job include the Gradle test task in the “Invoke Gradle script” build step. Send the test results to GitHub by adding a post-build step to publish JUnit results.

Include the location of the test results XML file in the post-build step

Once this is all done, a nifty little box in the GitHub Pull Request box will show the result of the unit tests. And if these tests are considered a required check, then failed unit tests will prevent pull requests from being merged.

To merge there cannot be build errors, unit tests must pass, and there can’t be any major static code analysis issues

This is great to make sure that the build isn’t broken, and that code changes haven’t regressed a unit test, but we can do more!

3) Run static code analysis on Pull Requests

On a small team, asking for a pull request review is like asking a friend to proofread your blog post. That friend is happy to give feedback, but she might not want to search for all the spelling and grammar errors too. To maintain friendships, and take some of the tedium and pressure out of code reviews, we run static code analysis on all pull requests.

Example GitHub PR feedback from SonarQube

Just as with the IntelliJ Plugin, SonarQube must be running on some server to run analysis and post results to GitHub. Check out the public instance to play around with it some more, if you aren’t already sold. Once SonarQube is running, install these plugins:

  • GitHub Plugin to post the result (as a bot user) of analysis on the pull request
  • Android Lint Plugin to include Android Lint checks in the static code analysis
  • XML Plugin to analyze XML files (otherwise Lint errors on XML files will be lost)
Set Android Lint as the default profile with Sonar way as its parent

I recommend running the default Java rules (“Sonar way”) and the Android Lint rules on your project. To do this, extend the Android Lint profile from the Java profile; then make Android Lint the default profile.

To run the static code analysis from Jenkins, I recommend using the SonarQube Scanner for Gradle to create a new Gradle task.

The modifications to your build.gradle file will look like this:

This will create a new Gradle task that runs a full static code analysis and submits it to the SonarQube server.

./gradlew sonarqube

The following properties will need to be defined as command line arguments to run an incremental analysis and publish the results to the GitHub Pull Request:

sonar.analysis.mode=preview
sonar.github.pullRequest=${ghprbPullId}

In the above code ${ghprbPullId} is the GitHub Pull Request number as provided the GitHub Pull Request Builder Plugin.

An example of a complete command for a preview build is:

./gradlew sonarqube -Dsonar.analysis.mode=preview -Dsonar.github.pullRequest=${ghprbPullId}

The final step is to add the sonarqube Gradle task and the build properties to the build step in your pull request builder Jenkins Job. Once all that is done, you’ll get in-line code comments, a summary of issues as reported by SonarQube and a nifty line in the Pull Request merge box.

Example GitHub status check success… Good to go!
Example GitHub status check failure… It looks like this code could use improvement :(

4) Update ticket status with build number after successful build

Updating tickets can be a tedious, error-prone part of the development cycle. To improve this process, we automatically progress tickets to their “Ready for QA” state and include a comment with the build number where they are fixed.

The JIRA Plugin for Jenkins provides exactly this functionality. Follow the docs for configuring the plugin, and then include these two build steps:

  • JIRA: Add related environment variables to build
  • Progress JIRA issues by workflow action
The two build steps used to update all JIRA issues and comment with their fix build number

For the plugin to work you must include the ticket number in your commits. This is how the plugin knows what tickets to update.

This is an easy plugin to add that will hopefully improve everyone’s lives by keeping tickets up-to-date and tagging them with build fix numbers.

5) Distribute builds with release notes to team members

At eero, we use Fabric‘s Beta service to share staging builds with the team. One nice feature of the Beta service is that it allows uploading release notes with new builds.

We upload changes for every new build by getting the changes from Jenkins, writing them to a CHANGELOG file and then uploading that with Fabric.

Sample code to write Jenkins changes to CHANGELOG directory

Now that every build writes the changes to a CHANGELOG file, we just upload that file to Fabric. As the documentation says, including a release notes file is as simple as adding this line of code to the Gradle file:

ext.betaDistributionReleaseNotesFilePath = "CHANGELOG"

The result: whenever emails are sent from Fabric Beta, a section called “Release Notes” will be included with the changes since the last build.

Release notes included in an email from Fabric Beta

What’s next?

Pre-commit hooks. This can be a good time to do light-weight automation, like how Yelp optimizes image size by converting them to WebP.

Integration tests. Android Studio 2.2 includes Espresso Test Recorder, so there is no longer an excuse to not write UI tests. We are currently working with Square’s Spoon library to run our app on different devices and versions of Android. We will also get more test coverage with Jacoco.

Custom rules. As we build-out our own style and find specific issues that we want to address, we will likely want to add some special rules. One route is to write custom inspections for Android Studio using structural search.

Tracking UI performance. Eventually we will track UI performance, including Janky frames, across devices and versions of the app.

Tracking real-world performance. Nimbledroid is doing a lot of cool automation with respect with method count, start-up time, and finding slow methods. And Jenkins can be used to track APK size and method count.

Firebase. Pre-launch reports, Test Lab, crash reporting — Firebase has it allfor a price. If you’re in a position to buy (figuratively and literally) into this ecosystem, it may be a good choice.

Is it all worth it?

We think so. Automation has improved team communication, saved time, and allowed us to write better code. Some parts may not work for everyone, and that’s totally fine. If something from this article seems worthwhile, try it out.


For more info on eero, check out our website. The Android app that is produced from this development process is available in the Play Store.

A shorter version of this post appeared on eero’s blog.


If you found this post useful please ❤ ︎it below :)