Automation: ANQ on self-drive mode
Never spend 6 minutes doing something by hand when you can spend 6 hours failing to automate it
In the realm of app development, releasing your creations into the wild can be a tedious task. However, it doesn’t have to be. Enter the world of Continuous Integration and Continuous Deployment (CI/CD), a developer’s knight in shining armour when it comes to automating the build and deployment process. Through CI/CD, developers can automate testing and deployment, ensuring a seamless transition from code to production.
My recent adventure into setting up a CI/CD pipeline for ANQ App led me down a path filled with learning, and a little head-scratching. Below, I’ll share the CI/CD pipeline I established for building and deploying Android and iOS builds to the Google Play Console and Apple App Store, respectively.
Understanding the CI/CD File
Our journey begins with the CI/CD file, the blueprint for automating the entire process. Let’s break down the major components:
1. Setting the Base Image and Variables
image: reactnativecommunity/react-native-android
variables:
LC_ALL: 'en_US.UTF-8'
LANG: 'en_US.UTF-8'
Here, we specify the Docker image and set some global variables. reactnativecommunity/react-native-android
is our base image, and we're setting the locale and language settings which will be used throughout the pipeline.
2. Defining the Stages
stages:
- setup
- format
- lint
- build_android
- build_ios
Stages represent the different phases our code will go through. They will be executed in the order listed, starting with setup, then formatting, linting, and finally, building and deploying for Android and iOS.
3. Preparing the Ground: Setup Stage
.setup_template: &setup_template
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
...
setup:
stage: setup
script:
- yarn install
<<: *setup_template
only:
- merge_requests
In the setup stage, we install the project dependencies using yarn install
. The cache is configured to save node_modules
for later stages, reducing the build time.
4. Format Stage
format_job:
stage: format
script:
- yarn format-check
<<: *setup_template
only:
- merge_requests
In this format_job
stage:
stage: format
: Specifies the stage name asformat
.script: - yarn format-check
: Executes a script that checks code formatting using Yarn. It's vital to ensure consistent code formatting for readability and maintainability.<<: *setup_template
: Inherits the cache setup from thesetup_template
defined earlier to reuse thenode_modules
cache, speeding up the job.only: - merge_requests
: Specifies that this job should only run for merge requests, helping catch formatting issues before code gets merged.
5. Lint Stage
lint_job:
stage: lint
script:
- git fetch --force origin $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME:$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
- git fetch --force origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME:$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- |
CHANGED_FILES=$(git diff --name-only --diff-filter=d $CI_MERGE_REQUEST_TARGET_BRANCH_NAME...$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME | grep -E '\.ts$|\.tsx$|\.js$|\.jsx$') || true
echo "Git diff exit status: $?"
if [[ -n "$CHANGED_FILES" ]]; then
echo "Changed files: $CHANGED_FILES"
echo "$CHANGED_FILES" | xargs yarn lint
else
echo "No files changed"
fi
<<: *setup_template
only:
- merge_requests
In the lint_job
stage:
stage: lint
: Names this stage aslint
.- The
script
block does the following:
- Fetches the source and target branches of the merge request.
- Compares the branches to identify changed files.
- If there are changed JavaScript or TypeScript files, it lints them using Yarn.
3. <<: *setup_template
: Inherits the cache setup to reuse node_modules
.
4. only: - merge_requests
: This job runs only for merge requests, ensuring linting checks are done before code is merged.
6. Build Stages
Before moving to individual Android
and IOS
build stages, we should discuss another important tool called Fastlane
.
Fastlane
Fastlane is a tool that automates beta deployments and releases for iOS and Android apps. It handles tasks like generating screenshots, dealing with code signing, and releasing your application.
Setting Up Fastlane
- Install: using RubyGems by running the following command:
gem install fastlane -NV
- Initialisation: Navigate to you project repository in the termial and run the following command:
# For IOS, it would be inside ios/
# For Android, it would be inside android/
fastlane init
- Configuration
After initializing Fastlane in your project directory, you’d have a fastlane
folder with an Appfile
and a Fastfile
. Configure these files according to your project needs.
Fastfile
defines the lanes or workflows for your build process.Appfile
stores configuration values that are global to your project.
Below is an example of Appfile
for both IOS
and Android
for a better idea.
// Android: Usually would have
json_key_file("path/to/json/key/file.json") # Path to the json secret file
package_name("com.yourcompany.yourapp") # Your app's package name
// IOS: Usually would have
app_identifier("com.yourcompany.yourapp") # Your app's bundle identifier
apple_id("email@example.com") # Your Apple email address
team_id("TEAMID") # Developer Portal Team ID
- Fastlane Match
We all know that code signing is an integral part of iOS and can sometimes leave you scratching your head. To address this issue, Fastlane introduced Match, an iOS code signing utility.
Fastlane Match creates and maintains a single, shared repository of signing certificates and provisioning profiles, which can be used across your development team.
Note: Fastlane Match isn’t necessary for working with Fastlane but simplifies your code signing process significantly.
- Setup Match: Run the following command within
ios/
directory.
fastlane match init
- Configuration: Configure the
Matchfile
with your repository and branching details.
fastlane match development
- Usage: In your
Fastfile
, use thematch
action to fetch the appropriate certificates and profiles for building your app.
// Example of match action
match(
app_identifier: ENV["APP_IDENTIFIER"],
type: "appstore",
readonly: is_ci, // if used in CI/CD
)
Now that we’ve been introduced to Fastlane, let’s delve into the actual build processes for Android and iOS in our CI/CD pipeline.
Android Build Process
- Stage Definition
build_android_job:
stage: build_android
before_script:
- apt-get update -y && apt-get install -y curl git zlib1g-dev autoconf bison build-essential
libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev
- curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash
- export PATH="$HOME/.rbenv/bin:$PATH"
- eval "$(rbenv init -)"
- rbenv install 3.2.2
- rbenv global 3.2.2
script:
- yarn install
- cd android
- gem install bundler
- bundle install
- echo $APKSIGN_KEYSTORE_BASE64 | base64 -d > release.jks
- export APKSIGN_KEYSTORE=`pwd`/release.jks
- echo $APP_PLAY_SERVICE_JSON > app/service.json
- bundle exec fastlane upload_internal
when: manual
In the build_android_job
stage:
stage: build_android
: Specifies this as the Android build stage.before_script
: This block contains commands to set up the environment.
- The first command updates the package list and installs essential packages including
curl
,git
, and others needed for the build process. - The second command installs
rbenv
, a Ruby version manager, which is essential for running Fastlane. - Other setup commands follow to prepare the environment for the Android build.
3. script
: This block contains the script to build and upload the Android app.
bundle exec fastlane upload_internal
: This command triggers Fastlane to run theupload_internal
lane, which handles building and uploading the Android app.
4. when: manual
: Specifies that this job should only run when triggered manually.
- Fastlane Configuration
Fastlane configuration for Android is contained within a Fastfile
. Here's a simplified version of the upload_internal
lane:
desc "Submit a new Internal Build to Play Store"
lane :upload_internal do
gradle(
task: "bundle",
build_type: "release",
properties: {
"android.injected.signing.store.file" => File.expand_path(ENV['APKSIGN_KEYSTORE']),
"android.injected.signing.store.password" => ENV['APKSIGN_KEYSTORE_PASS'],
"android.injected.signing.key.alias" => ENV['APKSIGN_KEY_ALIAS'],
"android.injected.signing.key.password" => ENV['APKSIGN_KEY_PASS'],
}
)
upload_to_play_store(
track: 'internal',
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
In the build_android_job
stage:
gradle
: This command invokes Gradle to build the app.
task: "bundle"
: Specifies to create a bundle.build_type: "release"
: Indicates a release build.properties
: Contains properties for signing the app.android.injected.signing.store.file
: Path to the keystore file.android.injected.signing.store.password
: Keystore password.android.injected.signing.key.alias
: Key alias.android.injected.signing.key.password
: Key password.ENV['APKSIGN_KEYSTORE']
,ENV['APKSIGN_KEYSTORE_PASS']
,ENV['APKSIGN_KEY_ALIAS']
,ENV['APKSIGN_KEY_PASS']
: These environment variables store sensitive data for signing the app. They should be configured in your CI/CD environment securely.
2. upload_to_play_store
: This command uploads the app to the Play Store.
track: 'internal'
: Specifies to upload to the internal track.skip_upload_metadata
,skip_upload_images
,skip_upload_screenshots
: These flags skip uploading metadata, images, and screenshots respectively, as they might not be necessary for internal builds.
IOS Build Process
- Stage Definition
build_ios_job:
stage: build_ios
tags:
- iOS // runner-tag on which this stage would run on
script:
- yarn install
- cd ios
- bundle install
- bundle exec pod install
- bundle exec fastlane release
when: manual
In the build_ios_job
stage:
stage: build_ios
: Specifies this as the iOS build stage.
2. script
: Contains the scripts to build and upload the iOS app.
bundle exec fastlane release
: This command triggers Fastlane to run therelease
lane, which handles building and uploading the iOS app.
3. when: manual
: Specifies that this job should only run when triggered manually.
- Fastlane Configuration
Below are the lanes designed to prepare your environment and ensure that all necessary certificates and profiles are in place before building and releasing the app.
## Setup local environment configuration. If running on a CI machine we need to create a Keychain to store the
## distribution certificates
private_lane :setup_environment do
if is_ci
setup_ci(force: true)
end
end
- A private lane named
setup_environment
. Private lanes are utility lanes that can only be called from other lanes. if is_ci
: Checks if Fastlane is being run on a Continuous Integration (CI) server.setup_ci(force: true)
: Calls thesetup_ci
action to set up the CI environment. Theforce: true
parameter ensures that any existing setup is overridden if necessary.
desc "Setup Development & Distribution profiles and certificates in the local machine"
lane :setup_provisions do
setup_environment
# Install Distribution Provisions and Certificates
match(
app_identifier: ENV["APP_IDENTIFIER"],
type: "appstore",
readonly: is_ci,
)
end
- A public lane named
setup_provisions
. setup_environment
: Calls the previously defined private lane to set up the environment.match(...)
: Calls thematch
action to manage certificates and provisioning profiles.app_identifier: ENV["APP_IDENTIFIER"]
: Specifies the app identifier.type: "appstore"
: Indicates that App Store provisioning profiles and certificates should be used.readonly: is_ci
: In a CI environment, it preventsmatch
from creating new certificates or profiles.
desc "Release app to Testflight"
lane :release do
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_KEY_ID"],
issuer_id: ENV["APP_STORE_ISSUER_ID"],
key_content: ENV["APP_STORE_API_KEY"],
in_house: false,
)
setup_provisions
# Update code signing settings
update_code_signing_settings(
use_automatic_signing: false,
path: "path to .xcodeproj",
team_id: ENV["APP_STORE_DEVELOPER_PORTAL_TEAM_ID"],
code_sign_identity: "Apple Distribution",
profile_name: "match AppStore #{ENV["APP_IDENTIFIER"]}",
bundle_identifier: ENV["APP_IDENTIFIER"]
)
# Increment the version number
increment_version_number(version_number: ENV["VERSION_NUMBER"])
# Increment the build number
increment_build_number(build_number: ENV["BUILD_NUMBER"])
# Build app
build_app(workspace: "your .xcworkspace name", scheme: "your scheme name", clean: true, export_method: "app-store")
# Upload to test flight
upload_to_testflight(api_key: api_key)
end
- A public lane named
release
. app_store_connect_api_key
: This action creates an API Key for App Store Connect, necessary for authenticating requests to upload the app to TestFlight.key_id
,issuer_id
,key_content
: These parameters are credentials obtained from your Apple Developer account. They should be stored securely and passed as environment variables.- Calls the
setup_provisions
lane defined earlier to ensure the necessary provisioning profiles and certificates are set up. update_code_signing_settings
: This action updates the code signing settings of your Xcode project.increment_version_number(...)
andincrement_build_number(...)
: These actions increment the app’s version and build numbers.build_app(...)
: This action builds the app using the specified workspace, scheme, and export method.upload_to_testflight(api_key: api_key)
: This action uploads the built app to TestFlight using the specified API key for authentication.
Conclusion
After this automation-marathon, your back’s earned a diploma in endurance! Time to treat it to a victory stroll.
And voila! After what may have felt like a century in developer years, you’ve mechanized the entire build and release process. What once was a manual marathon every release day is now a breeze thanks to our trusty friend, automation. Sure, it might have taken hours, or was it days, to get this CI/CD pipeline up and running. But hey, who’s counting? Now, every code push is a VIP, escorted smoothly from commit to the app stores without breaking a sweat. So, raise a toast to the countless hours you’ve saved (or will have saved in the future)! Now, you can finally enjoy those leisurely coffee breaks, while your CI/CD pipeline diligently does the heavy lifting. Cheers to modern-day development magic!
Feel free to buy our developer a book to motivate him to write more.