Ensuring code quality with CI/CD
Hello Android developers,
One of the best ways for us to help you enhance your apps is with samples, and it’s important that our samples are high quality and work as intended.
Building dual-screen libraries and samples
As you may already know, specifically for Android development (through its SDK and using Kotlin/Java), we currently provide:
- surface-duo-sdk: dual-screen and foldable libraries that help developers create and enhance apps targeting dual-screen and foldable devices.
- surface-duo-sdk-samples-kotlin: a set of samples that demonstrate dual-screen patterns which take advantage of the capabilities new form factor devices provide.
- surface-duo-compose-samples: samples that implement dual-screen patterns whose UIs are implemented using Jetpack Compose.
- surface-duo-app-samples: a set of apps that follow specific dual-screen patterns in order to provide the best user experience whether you are using a single-screen or a dual-screen device or foldable.
In the above repos, there is a lot of code that was provided from different contributors, each who have their own style. It’s important to validate the code to ensure that the code does what it says it does, and be careful to validate that new changes don’t break things that were working before your changes.
With every new Pull Request (PR), members of the team have to review the code that has been pushed to catch coding problems, mistakes, etc. Peer review approval is mandatory in order to get PRs merged into the main branch.
But as you can imagine, there are tasks that the reviewer shouldn’t focus on (like code style, nits, etc.), or that are so repetitive and that take time (i.e., running tests) that we should be able to automate and be the machine who does those checks.
Following continuous integration (CI) principles, we run some tasks in an automated way every time we push new code to our different repositories. These tasks will help us to understand if our code is maintaining the quality that we want.
With every new PR and push into the main branch, we will run these basic tasks:
- Build the code: It will build the PR and make sure that there aren’t issues while building it.
- Run unit tests: it will run all unit tests that were already implemented, and new ones added in the new PR, so we are more confident that our code works as expected and we don’t break anything that was already covered by tests.
- Run Android lint: we check that our code doesn’t have structural problems that can impact the reliability and efficiency of our samples and libraries.
- Run ktlint: our samples and code are mostly done using the Kotlin programming language, and ktlint helps to analyze the Kotlin code in order to discover programming errors, bugs, code style errors, etc.
To automate our CI workflow, we could use any of the many and great alternatives we can find currently in the market. But since our code lives in GitHub, we wanted to go for the simplest and all-in-one alternative that is GitHub Actions.
With GitHub Actions we can create different workflows that are triggered whenever we have defined (with every PR open, with every push to main branch, with every new release, etc.) that run the specific tasks that we want (e.g. build, run unit tests, etc.).
Currently, in all our repos mentioned previously, we have a very similar workflow that runs all tasks we previously covered in earlier blog posts too.
How does our simple-yet-useful-enough build workflow look? Figure 1 shows our workflow configuration file (.yml file) and explains what each task does:
Figure 1: Sample workflow configuration file
You might wonder why we are splitting assemble tasks into two instead of running assemble directly? First, we are just interested in debug and release flavors and second, having each task as a separate step makes it easier to discover if the build fails, exactly where it failed.
As you can see in figure 2, having different steps makes the workflow run easier to identify problems:
Figure 2: Results of a complete workflow run
In our surface-duo-app-samples repo we have a “special” scenario. As we have mentioned earlier, in this repo we have different and independent apps that implement a specific dual-screen pattern. These apps don’t share anything, they just share the repo merely as a host, and each app is located in its own folder with its own configuration files, dependency files, etc. But how we can run all different apps that don’t share a common gradlew executable so they don’t share tasks (i.e. we cannot do ./gradlew build and build all apps at the same time)?
One option would be to just have independent workflow files for each different app/folder, but that means that we have basically the same file copied several times with basically the same content (just changing the app/folder name); but is there a better alternative? Yes, build matrix to the rescue!
Build matrix allows you to run your workflow across multiple combinations of operating systems, platforms, and languages, but also for other and simpler scenarios; buthow do we use it? In figure 3 you can see how we define the matrix and how we use it to go inside of each app/folder to execute its specific gradlew file:
As you can see, ensuring code quality while saving your team time is something that we can achieve through implementing continuous integration. We have shown you how we do it and the tools that we use. As with any software product, as we move forward, we’ll also change and improve our workflow.
There are plenty of different alternatives to choose and to follow, you just have to pick what fits for you and your team better.
Resources and feedback
Check out the Microsoft Surface Duo developer documentation and past blog posts for links and details on all our samples. You can also find them summarized in the Microsoft Samples Browser, or explore on GitHub.