Implementing CI/CD for Jetbrains Compose Multiplatform Android and iOS Apps using GitHub Actions

Vova Stelmashchuk
Mobile App Development Publication
7 min readJul 3, 2023

Introduction

Overview schema

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:

  1. 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.
  2. We set the environment variable LAST_TAG by finding the latest tag in the repository using git tag --sort=committerdate | tail -1. This allows us to reference the previous version for version bumping.
  3. The actions-ecosystem/action-bump-semver action is utilized to perform the version bumping. It takes the current_version as input and bumps it to the next patch version (level: patch).
  4. We configure the git user details and create a new tag using the bumped version.
  5. The new tag is then pushed to the repository with git push --tags.
  6. 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.
  7. 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:

  1. The deploy_android job has a dependency on the prepare_deploy job, ensuring that the version information from the previous step is available.
  2. Environment variables MIXDRINKS_MOBILE_APP_VERSION_NAME and MIXDRINKS_MOBILE_APP_VERSION_CODE are set based on the outputs of the prepare_deploy job. These variables hold the version name and version code of the app.
  3. The actions/checkout action is used to fetch the codebase from the repository.
  4. The gradle/gradle-build-action is employed to set up Gradle for the build process.
  5. The app bundle release is built using the gradle android:bundleRelease command.
  6. 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.
  7. The signed AAB file is uploaded as an artifact using the actions/upload-artifact action.
  8. 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:

  1. The deploy_ios job has a dependency on the prepare_deploy job, ensuring that the version information from the previous step is available.
  2. Environment variables MIXDRINKS_MOBILE_APP_VERSION_NAME and MIXDRINKS_MOBILE_APP_VERSION_CODE are set based on the outputs of the prepare_deploy job. These variables hold the version name and version code of the app.
  3. The actions/checkout action is used to fetch the codebase from the repository.
  4. The actions/setup-java action is utilized to set up the Java environment with the desired distribution and version.
  5. The gradle/gradle-build-action is employed to set up Gradle for the build process.
  6. The ./gradlew podInstall command is executed to set up the dependencies and create the workspace for the iOS app.
  7. 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.
  8. 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".
  9. The resulting IPA (iOS App Package) file is uploaded as an artifact using the actions/upload-artifact action.
  10. The necessary private API key is installed by decoding the base64-encoded private key stored in the secrets.
  11. 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.
  12. 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.

--

--