Implementing CI/CD for Jetbrains Compose Multiplatform Android and iOS Apps using GitHub Actions
Introduction
In today’s rapidly evolving mobile app development landscape, implementing a robust CI/CD (Continuous Integration and Continuous Deployment) process has become essential. In this article, we will explore how to leverage the power of GitHub Actions to implement CI/CD for cross-platform Android and iOS apps built with Jetpack Compose Multiplatform. By automating tasks such as building, testing, and deploying your apps, GitHub Actions enables faster, more efficient development cycles, ensuring seamless delivery of high-quality applications to end users. We’ll dive into the details of setting up the CI/CD pipeline, examining the workflow, triggers, and necessary steps to prepare your Jetpack Compose Multiplatform app for deployment on both Android and iOS platforms. Let’s harness the power of Jetpack Compose Multiplatform and GitHub Actions to streamline your app development and delivery process!
Pre build step: Creating a New Version Tag for Future Release
The first step in our CI/CD pipeline is to create a new version tag that will be used for the upcoming release. This tag will help us track and identify the specific version of our app as it progresses through the pipeline.
Within the GitHub Actions workflow file, we have a job called prepare_deploy
that handles this task. Let's dive into the details of this job:
jobs:
prepare_deploy:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set env
id: last_tag
run: echo "LAST_TAG=$(git tag --sort=committerdate | tail -1)" >> $GITHUB_OUTPUT
- uses: actions-ecosystem/action-bump-semver@v1
id: bump-semver
with:
current_version: ${{ steps.last_tag.outputs.LAST_TAG }}
level: patch
- run: |
git config user.name github-actions
git config user.email github-actions@github.com
git tag ${{ steps.bump-semver.outputs.new_version }}
git push --tags
- name: "Set output"
id: set-output
run: |
echo "mix_drinks_mobile_version_name=${{ steps.bump-semver.outputs.new_version }}" >> $GITHUB_OUTPUT
IFS='.' read -r major minor patch <<< "${{ steps.bump-semver.outputs.new_version }}"
mix_drinks_mobile_version_code=$((major * 10000 + minor * 100 + patch))
echo "mix_drinks_mobile_version_code=${mix_drinks_mobile_version_code}" >> $GITHUB_OUTPUT
- name: "Print output"
run: |
echo -e "Version name is: \n ${{ steps.set-output.outputs.mix_drinks_mobile_version_name }}"
echo -e "Version code is: \n ${{ steps.set-output.outputs.mix_drinks_mobile_version_code }}
Let’s break down the steps involved in this job:
- The
actions/checkout
action is used to fetch the codebase from the repository. The fetch depth of 0 ensures that the complete commit history is available, which is important for accurate versioning and tagging in the subsequent steps of the CI/CD pipeline. - We set the environment variable
LAST_TAG
by finding the latest tag in the repository usinggit tag --sort=committerdate | tail -1
. This allows us to reference the previous version for version bumping. - The
actions-ecosystem/action-bump-semver
action is utilized to perform the version bumping. It takes thecurrent_version
as input and bumps it to the next patch version (level: patch
). - We configure the git user details and create a new tag using the bumped version.
- The new tag is then pushed to the repository with
git push --tags
. - The version name and version code are set as outputs using the
set-output
action. The version code is calculated by combining the major, minor, and patch components of the version using a mathematical formula. - Finally, the version name and version code are printed as output for visibility.
Deploying the Android App
Once the new version tag has been created, we can proceed with deploying the Android app. In our GitHub Actions workflow, this deployment is handled by the deploy_android
job. Let's examine the details of this job:
jobs:
deploy_android:
needs: [prepare_deploy]
env:
MIXDRINKS_MOBILE_APP_VERSION_NAME: ${{ needs.prepare_deploy.outputs.output_write_mix_drinks_mobile_version_name }}
MIXDRINKS_MOBILE_APP_VERSION_CODE: ${{ needs.prepare_deploy.outputs.output_write_mix_drinks_mobile_version_code }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: "Setup Gradle"
uses: gradle/gradle-build-action@v2
- name: "Build bundle release"
run: gradle android:bundleRelease
- uses: r0adkll/sign-android-release@v1
name: "Sign app aab file"
id: sign_app
with:
releaseDirectory: androidApp/build/outputs/bundle/release
signingKeyBase64: ${{ secrets.MIXDRINKS_ANDROID_SIGNING_KEY }}
alias: ${{ secrets.MIXDRINKS_ANDROID_ALIAS }}
keyStorePassword: ${{ secrets.MIXDRINKS_ANDROID_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.MIXDRINKS_ANDROID_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "30.0.2"
- uses: actions/upload-artifact@v3
with:
name: "Upload AAB file as artifact"
path: ${{steps.sign_app.outputs.signedReleaseFile}}
- name: "Upload to Google Play"
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.MIXDRINKS_ANDROID_DEPLOY_SERVICE_ACCOUNT_JSON }}
packageName: org.mixdrinks.app
releaseFiles: ${{steps.sign_app.outputs.signedReleaseFile}}
track: production
status: completed
Let’s break down the steps involved in deploying the Android app:
- The
deploy_android
job has a dependency on theprepare_deploy
job, ensuring that the version information from the previous step is available. - Environment variables
MIXDRINKS_MOBILE_APP_VERSION_NAME
andMIXDRINKS_MOBILE_APP_VERSION_CODE
are set based on the outputs of theprepare_deploy
job. These variables hold the version name and version code of the app. - The
actions/checkout
action is used to fetch the codebase from the repository. - The
gradle/gradle-build-action
is employed to set up Gradle for the build process. - The app bundle release is built using the
gradle android:bundleRelease
command. - The
r0adkll/sign-android-release
action is used to sign the app AAB (Android App Bundle) file. The action takes the necessary signing configuration provided through secrets. - The signed AAB file is uploaded as an artifact using the
actions/upload-artifact
action. - The
r0adkll/upload-google-play
action is used to upload the signed AAB file to Google Play Store. The action requires the appropriate service account JSON and package name. The track is set to "production", and the status is marked as "completed".
With these steps, the Android app is built, signed, and deployed to the Google Play Store as a release-ready bundle. This enables a streamlined process of delivering new versions of the app to Android users
Deploying the iOS App
In addition to deploying the Android app, we also need to deploy the iOS app as part of our CI/CD process. In our GitHub Actions workflow, the deployment of the iOS app is handled by the deploy_ios
job. Let's take a closer look at the details of this job:
jobs:
deploy_ios:
needs: [prepare_deploy]
env:
MIXDRINKS_MOBILE_APP_VERSION_NAME: ${{ needs.prepare_deploy.outputs.output_write_mix_drinks_mobile_version_name }}
MIXDRINKS_MOBILE_APP_VERSION_CODE: ${{ needs.prepare_deploy.outputs.output_write_mix_drinks_mobile_version_code }}
runs-on: macos-12
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: "Setup Gradle"
uses: gradle/gradle-build-action@v2
- name: "Build xcworkspace"
run: ./gradlew podInstall
- name: "Setup app version"
uses: yanamura/ios-bump-version@v1
with:
version: ${{ env.MIXDRINKS_MOBILE_APP_VERSION_NAME }}
build-number: ${{ env.MIXDRINKS_MOBILE_APP_VERSION_CODE }}
project-path: iosApp
- name: "Build iOS App"
uses: yukiarrr/ios-build-action@v1.9.1
with:
project-path: iosApp/iosApp.xcodeproj
p12-base64: ${{ secrets.MIXDRINKS_IOS_P12_BASE64 }}
mobileprovision-base64: ${{ secrets.PROD_MIXDRINKS_IOS_BUILD_PROVISION_PROFILE_BASE64 }}
code-signing-identity: "iPhone Distribution"
team-id: ${{ secrets.MIXDRINKS_IOS_TEAM_ID }}
certificate-password: ${{ secrets.MIXDRINKS_IOS_CERTIFICATE_PASSWORD }}
export-options: iosApp/exportOptionsRelease.plist
workspace-path: iosApp/iosApp.xcworkspace
export-method: "app-store"
- name: "Upload IPA file as artifact"
uses: actions/upload-artifact@v3
with:
name: IOS IPA
path: "output.ipa"
- name: Install private API key P8
env:
PRIVATE_API_KEY_BASE64: ${{ secrets.MIXDRINKS_IOS_APPSTORE_API_PRIVATE_KEY }}
API_KEY: ${{ secrets.MIXDRINKS_IOS_APPSTORE_API_KEY_ID }}
run: |
mkdir -p ~/private_keys
echo -n "$PRIVATE_API_KEY_BASE64" | base64 --decode --output ~/private_keys/AuthKey_$API_KEY.p8
- name: "Upload IPA file to TestFlight"
env:
API_KEY: ${{ secrets.MIXDRINKS_IOS_APPSTORE_API_KEY_ID }}
API_ISSUER: ${{ secrets.MIXDRINKS_IOS_APPSTORE_ISSUER_ID }}
run: xcrun altool --output-format xml --upload-app -f output.ipa -t ios --apiKey $API_KEY --apiIssuer $API_ISSUER
Let’s break down the steps involved in deploying the iOS app:
- The
deploy_ios
job has a dependency on theprepare_deploy
job, ensuring that the version information from the previous step is available. - Environment variables
MIXDRINKS_MOBILE_APP_VERSION_NAME
andMIXDRINKS_MOBILE_APP_VERSION_CODE
are set based on the outputs of theprepare_deploy
job. These variables hold the version name and version code of the app. - The
actions/checkout
action is used to fetch the codebase from the repository. - The
actions/setup-java
action is utilized to set up the Java environment with the desired distribution and version. - The
gradle/gradle-build-action
is employed to set up Gradle for the build process. - The
./gradlew podInstall
command is executed to set up the dependencies and create the workspace for the iOS app. - The
yanamura/ios-bump-version
action is used to set the app version and build number in the iOS project. The version and build number are retrieved from the environment variables. - The
yukiarrr/ios-build-action
action is used to build the iOS app. It takes various parameters, including the project path, signing details from the secrets, export options, workspace path, and export method set to "app-store". - The resulting IPA (iOS App Package) file is uploaded as an artifact using the
actions/upload-artifact
action. - The necessary private API key is installed by decoding the base64-encoded private key stored in the secrets.
- The IPA file is uploaded to TestFlight using the
xcrun altool
command, which performs the upload to the App Store Connect API. The command requires the API key and API issuer ID from the secrets. - The
yuki0n0/action-appstoreconnect-token
action is used to retrieve the App Store token, which will be utilized in subsequent steps for managing app versions and releases.
With these steps, the iOS app is built, signed, and uploaded to TestFlight for testing and further distribution through the App Store. This enables a seamless and automated deployment process for iOS users.
Additionally, it’s important to note that the deployment of the iOS app is not fully automated. After uploading the IPA file to the App Store using the CI/CD pipeline, you will need to manually create a new version in App Store Connect, select the uploaded build for that version, and submit it for review by Apple. These manual steps are necessary to comply with Apple’s guidelines and ensure the quality of your app before it becomes available to users. While the CI/CD pipeline streamlines the build and upload process, the final steps require manual intervention in App Store Connect.
Furthermore, the complete CI/CD configuration file for the MixDrinks Mobile project, including the detailed workflows and steps mentioned in this article, can be found in the project’s repository. The MixDrinks project is fully open source, allowing you to explore the CI/CD implementation in its entirety. You can access the repository to gain deeper insights into the CI/CD pipeline and adapt it to your own project’s requirements. Feel free to refer to the repository for a comprehensive understanding of how the CI/CD process is set up for the MixDrinks app.
Finally, I invite you to stay connected with the MixDrinks Mobile project for future updates. As I continue to enhance the CI/CD pipeline, my aim is to fully automate the iOS release process. This includes automating the creation of new versions, as well as automatically updating screenshots in both the App Store and Google Play. By subscribing to the project, you can stay informed about the latest developments and witness the evolution of the CI/CD pipeline. Your support and feedback are greatly appreciated as I strive to improve the deployment process and deliver a seamless experience to end users.