Streamlining mobile development with CI and CD
Continuous integration (CI) and continuous delivery (CD) have significantly improved both my productivity as a developer and my team’s ability to execute smoothly and efficiently on a variety of projects. In this post, I’ll explain how CI and CD work, talk a bit about the benefits of these practices, and walk through an example that illustrates how to set up your own CI/CD systems.
What is continuous integration (CI)?
Continuous integration (CI) refers to the practice of putting automated checks in place to ensure that new code doesn’t break your current build. A CI system runs a variety of tests that help you detect (and fix) any problems or conflicts as early as possible.
Thoughtworks defines CI as follows:
Continuous Integration (CI) is a development practice that requires developers to integrate code into a shared repository several times a day. Each check-in is then verified by an automated build, allowing teams to detect problems early. By integrating regularly, you can detect errors quickly, and locate them more easily.
What is continuous delivery (CD)?
A continuous delivery (CD) system allows any engineer (or anyone with the correct permissions) to easily distribute a new version or build of a product at the push of a button. CD eliminates the cumbersome process of engineers manually building and uploading from their personal computers, and therefore makes it much easier to release changes to customers on a consistent schedule.
Atlassian defines CD as follows:
Continuous delivery is an extension of continuous integration to make sure that you can release new changes to your customers quickly in a sustainable way. This means that on top of having automated your testing, you also have automated your release process and you can deploy your application at any point of time by clicking on a button. In theory, with continuous delivery, you can decide to release daily, weekly, fortnightly, or whatever suits your business requirements.
“CD” can also refer to the similar concept of continuous deployment. In a continuous deployment system, every change that is merged triggers a new build that is deployed immediately. Again, Atlassian offers a useful formal definition:
Continuous deployment goes one step further than continuous delivery. With this practice, every change that passes all stages of your production pipeline is released to your customers. There’s no human intervention, and only a failed test will prevent a new change to be deployed to production.
Continuous deployment requires a commitment to small pull requests and incremental changes; for larger changes, most teams will still pause deployment and plan a more staged release. Additionally, continuous deployment isn’t possible in the world of mobile development, where app store review schedules dictate the pace and timing of actual deployment. Instead, most mobile teams utilize a “release train” system to support regular weekly, biweekly, or monthly updates.
Getting started with CI/CD
To start using continuous integration and delivery on your team, you’ll need to have the following basic tools in place:
- A version control system, such as Github
- An automation server or service, such as Jenkins, Travis, XCode Server, Buddy, CircleCI, etc.
- A robust test suite, including Lint rules, unit tests, UI tests, integration tests
- Local development management tools (optional, but recommended), such as fastlane. These tools can handle repetitive tasks such as building application executables, code signing, beta distribution, App Store and Google Play distribution, and even automatically taking and uploading screenshots;they also integrate with GitHub, S3, Dropbox, and AppCenter to manage changelogs and versioning.
- A good testing culture. This isn’t a tool, but rather an attribute of your team, and it may well be the most important precursor to using a CI/CD system. Anyone who adds code needs to understand that they must also add proper tests, not only to prevent their new code from breaking the current build, but also to protect teammates against accidentally breaking the new code in the future.
Example CI/CD setup
Here’s a recap of the steps I take to configure a CI/CD system for a new or ongoing project:
Step 1: Fastlane setup
Create “lanes” in fastlane to handle basic testing and release tasks. In the code below, for example, each lane runs the lint and unit test suite, while the others can make a clean build and upload to the Play Store beta track, or tell the Play Store to promote the current beta build to a production rollout of 50 percent; the last lane can be used to manage version numbers:
# fastlane/Fastfile
lane :app_test do
gradle(task: "app:lint app:test")
end
lane :app_beta do
gradle(task: "clean app:bundleRelease")
upload_to_play_store(track: "beta")
end
lane :app_beta_to_rollout do
upload_to_play_store(track: "beta",
track_promote_to: "rollout",
rollout: 0.5)
end
desc "Increment major version number + build bump"
lane :bumpversionmajor do
build = increment_build_number()
version = increment_version_number(
bump_type: "major"
)
puts "Bumped project to v#{version} (#{build}).”
puts “Please remember to commit changes to git."
end
Step 2: Configure CI/CD workflows
Whenever there is a new pull request, GitHub informs the CI system. The configuration below guides the CI system on which workflow jobs should be performed.
# .circleci/config.yml
jobs:
test_project:
...
steps:
- checkout
...
- run:
command: fastlane lib_test
- run:
command: fastlane app_test
...
build_deploy:
...
steps:
...
- run:
command: fastlane libbuild
- run:
command: fastlane app_beta
...
(after jobs section)
workflows:
test-build-deploy:
jobs:
- test_project:
...
- build_deploy:
requires:
- test_project
filters:
branch:
ignore: /./
tags:
only: /^v./
You’ll notice that this configuration is actually very simple. I recommend keeping the logic inside your CI system to a minimum, as this empowers engineers to leverage local scripts, and will make it easier to switch between CI providers as needed to minimize cost or maximize functionality.
Step 3: Protect primary branches
Most engineers don’t actually run their local test suite when issuing a pull request — and they probably shouldn’t have to, since (assuming your organization has a strong testing culture) doing so could mean running literally thousands of unit tests. Therefore, it’s important to configure your CI/CD system such that primary branches are forced to undergo appropriate testing before any changes are merged.
In GitHub, you can protect primary branches by enforcing code reviews. You can also integrate CircleCI to require that CI workflows complete and pass before merging:
Step 4: Optimize
Once your basic configuration is in place, look for ways to optimize and accelerate everyday tasks. For example, when creating a new tag for a release, you could trigger the test suite and beta release automatically. Or, if you need a large variety of design assets, you could empower your designers to make pull requests for new assets, and automate the creation of smaller assets (e.g., thumbnails) from larger versions.
Even if full continuous deployment isn’t possible (as with a mobile app), try to keep moving toward faster, more frequent releases — your team and customers will be happier for it.