Modern DevOps and continuous delivery practices require mobile apps to be built on a continuous basis. Mobile application development won’t be perfect without an effective build and continuous integration (CI) process. The build process checks the internal quality of the code and tests the business logic of the product before releasing it to the customer.
Solid and trustworthy build automation plays an important role in the quality of an app. On another note, flaky and untrustworthy builds can cause more damage than good in the continuous integration process. Therefore, building a rock-solid build automation setup and defining a concrete build process for your app is required for successful CI/CD practices. In this post, we will cover the top 12 configuration mistakes that will fail your mobile app build with insights from real user cases on Codemagic, a CI/CD for Flutter apps.
Mobile continuous integration process
The continuous integration process of mobile apps vs web continuous integration is a bit different as it involves additional tasks, like code signing and publishing of binaries to App Store or Play Store. The typical mobile CI process consist of the following steps.
- Downloading the source code
- Linting source code for syntax errors (static analysis)
- Building and testing of source code
- Code signing and publishing of apps
- Reporting build status to the team
These are the most common CI steps, but some projects have a more complex and customized CI process. It’s very important to understand the build process of your project and what is running apart of continuous integration. There might be different workflows for different branches, e.g Git workflow, and everyone in the team needs to understand them.
Top 12 configuration mistakes for mobile build failures
1. Inconsistency in build tools
Mobile apps use build tools to automate all the build process tasks. Examples of the build tools are Gradle and Fastlane.
The most common build errors come from the wrong configuration of the tools or plugins.
The majority of the Flutter Android builds recently failed due to the misconfiguration of the Gradle. The issues like unable to lint, issues using Flutter with other SDKs and wrong class paths are the most common issues in the Flutter Github issue list. At the moment, there are still 103 open issues related to Gradle on the Flutter Github repo. App developers are recommended to keep an eye on the issues and update their projects accordingly.
2. Difference in programming language versions
While developing apps, we have to make sure that we are using the correct version of the programming language. Usually, we need to explicitly specify the programming language version in our code or CI configuration.
Flutter has just released the stable version and is still undergoing changes. The Dart programming language is also evolving with Flutter. In order to avoid Dart version issues, we have to specify the versions of both Flutter and Dart which are compatible with our existing projects. The Flutter SDK version can be specified in the `pubspec.yaml` file.
3. Poor dependency management
Most mobile applications use third-party frameworks to develop the apps faster by reusing existing source code. Swift uses CocoaPods or Carthage or Swift Package Manager, Kotlin uses Gradle, Flutter (Dart) uses Pub Package Manager, etc.
It’s essential to lock the dependencies used for the project while developing so that changes in the dependency framework won’t break the current development workflow.
The best way to lock dependencies is by using the .lock file or a similar file provided by a dependency management tool. It’s also a great practice to check in the .lock file to the source control so that the entire development team and the CI server use the same version of the dependency framework.
Flutter uses Pub package manager to manage all the dependencies in the pubspec.yaml file. We have to make sure that all the versions of the SDK and dependency framework are locked during development. Flutter’s test package is a good example of this scenario as there were recently breaking changes in it which broke all the Flutter builds with unit and widget tests.
4. Missing platform checks (iOS or Android or both)
While developing cross-platform mobile apps for both iOS and Android, it’s important to check that the build runs successfully for both platforms in the local machine.
Some common build failures are that the app builds fine for iOS, but not for Android, or vice versa.
With Flutter apps, it’s a good practice to develop against both platforms, that is iOS and Android, and fix platform-specific errors before CI builds are run. This approach will save a lot of time and gives fast feedback on development.
5. Missing environment variables
It’s a common practice for continuous integration tools to use environment variables to access dynamic or sensitive data. These variables are usually present in the local machine, however developers might not realize that environment variables need to be set on the CI server as well. Another common mistake would be changing the value of the environment variable.
Whenever we change an environment variable locally, it needs to be updated on the CI server as well to avoid build failures.
6. Missing pre-build and post-build actions
Some mobile apps have a complex build process which involves custom scripting, downloading project-specific tools or publishing reports to specific reporting tools. Continuous integration services not necessarily have all the features required for the custom build process, so they provide options to execute the scripts before or after the specific build steps.
Build failures in that regard are most commonly caused by missing build steps before performing a specific action. If the build scripts are not coded, it’s important to add those scripts in the CI configuration.
Flakiness is a major cause of build failures during the CI process. Flakiness can come from anywhere in mobile app development. Commonly tests are flaky, but there are some other factors that can be flaky as well and cause tests to become flaky. Sometimes it’s the case that the test environments are not reliable, sometimes it’s the bad coding that causes inconsistent behaviour in the app. This makes the entire app become flaky.
Flaky builds are very time consuming and hard to debug. It’s important to remove all the flakiness during app development.
8. Cached vs clean builds
Caching project dependencies is a great way to speed up mobile CI builds. Most of the CI solutions provide support for caching the dependencies. However, sometimes cached dependencies can cause build failures, especially when we have upgraded dependencies to a newer version locally, but the CI server still uses cached dependencies.
It’s important to update the cached dependencies by performing clean builds whenever a dependency version is changed.
It’s good to have separate workflows to perform clean builds in addition to a cached workflow so that we can detect the breaking build steps or code changes early.
9. Code signing
Code signing for both iOS and Android apps are very tedious and complex steps in the build process. Especially for iOS apps, as there are so many moving parts in the code signing process. Code signing errors are very common because of the complexity of the process. There are various build failures due to code signing, the most common ones are as follows.
- Providing wrong credential to CI server
This error occurs when the CI server can’t login into the App Store Connect account. A typical error looks like this:
Unable to validate your application. Your Apple ID or password was entered incorrectly.
Or if your account uses 2FA, then the error looks like this:
Unable to validate your application. Please sign in with an app-specific password. You can create one at appleid.apple.com.
It is essential to provide the right credential to the CI server with correct rights to avoid this kind of build failures.
- Wrong bundle identifier or provisioning profile
It happens quite often that App Store Connect has many provisioning profiles and the CI server gets confused about which one to use, or there is no matching bundle identifier or provisioning profile available. Some of the common errors are:
Provisioning profile “For Debug Build” doesn’t include the aps-environment entitlement.
Provisioning profile “xxxx” has app ID “com.example.xxx”, which does not match the bundle ID “com.example.zzz”.
You may have encountered various other errors related to this topic, but the idea is to sort out the code signing assets to avoid this kind of build failures.
- Android keystore error
Android code signing uses the keystore from the project build setting. When the keystore is not detected at the right path, the CI server might throw this error:
Failed to read key keyname from store “/tpath_to/keystore.private”: Cannot recover key
Make sure you have entered the correct credential and keystore in the build settings to avoid these errors.
10. Missing version bump
Continuous delivery practices make it possible to use release trains which contain multiple builds for a specific version. However, one of the requirements for uploading a new build is to increment the build number so that the new build number is always greater than the previous one. The most common mistake while uploading a new build is forgetting to bump the build number. This causes the following error in iOS deployment:
The value for key CFBundleShortVersionString [0.0.1] in the Info.plist file must contain a higher version than that of the previously approved version [1.0.0]
There might be a similar error for Android builds. You should have in place an automated way for incrementing the build number for each new build to avoid this kind of mistakes.
11. Invalid App Store or Play Store assets
Build failures caused by invalid App Store Icon or Play Store Assets are very expensive as such failures are detected in a later phase of the build process. The error will look like this:
“Invalid App Store Icon. The App Store Icon in the asset catalog in ‘your.app’ can’t be transparent nor contain an alpha channel."
In order to fix those errors, we have to get the right assets and perform the build again, especially while deploying the app to Apple App Store or Google Play Store. It would be worth checking that all the assets are compatible with App Store and Play Store guidelines.
12. Cocoapods installation
Cocoapods is the dependency manager required for iOS applications. Some iOS apps require the newer version of Cocoapods 0.32.1 or above. There are a couple of methods generally used to install Cocoapods, the two most common methods being Rubygems and Homebrew. The Ruby gem method sometimes fails on the default Ruby (2.0.0) installed on the macOS, we would then need to use RVM or similar tools to switch between Ruby versions. By using the Rubygem method of installation, we can change the versions of Cocoapods while the Homebrew method always installs the latest version. Build failures often come from the different installation methods of the Cocoapods. Some build failures occur because Cocoapods crash with segmentation fault. It’s a good practice to choose the mechanism of installing Cocoapods with a compatible Ruby version to avoid Cocoapods related build failures. The best and recommended way to install Cocoapods is by using Rubygem, as mentioned on the official Cocoapods site.
The continuous integration process can detect errors early in the project’s life cycle, but as a team, we have to make sure that the project’s build settings are correct to avoid these common, time-wasting build failures.
Making our apps CI friendly is a great way of utilising continuous integration for better quality. What are your experiences with common build failures? How do you deal with it? Is there anything we have missed in this article? Please let us know on social media or in our Slack community for Codemagic CI/CD and Flutter.