Skip to content

Building the mobile app for dev #145

Building the mobile app for dev

Building the mobile app for dev #145

# SPDX-FileCopyrightText: Copyright (C) 2025 Opal Health Informatics Group at the Research Institute of the McGill University Health Centre <john.kildea@mcgill.ca>
#
# SPDX-License-Identifier: Apache-2.0
# This workflow is explained in `docs/deployment/ci-cd.md`; please keep that documentation file up to date when making changes here.
name: Build and Deploy App
# Default to dev when running automatically (see also "env" below)
run-name: Building ${{ (inputs.DEPLOY_FIREBASE || inputs.DEPLOY_STORES) && 'and deploying ' || '' }}the mobile app for ${{ inputs.ENVIRONMENT || 'dev' }}
on:
# When pushing to main, automatically build for dev
push:
branches:
- main
# Offer a manual interface to build for all other environments as needed
workflow_dispatch:
inputs:
ENVIRONMENT:
description: 'Environment in which to build'
type: choice
required: true
default: 'dev'
options:
- dev
- prod
DEPLOY_FIREBASE:
description: 'Deploy the app via Firebase app distribution'
required: true
default: false
type: boolean
DEPLOY_STORES:
description: '[Prod only] Deploy the app via the app stores'
required: true
default: false
type: boolean
BUILD_NUMBER_STORES:
description: "[Required if deploying to the stores; otherwise leave blank] A specific build number to use. CAUTION: choose carefully based on previous uploads; stores don't allow downgrading once a number has been chosen."
required: false
type: number
env:
# Read the target environment from workflow_dispatch inputs, or default to dev
ENVIRONMENT: ${{ inputs.ENVIRONMENT || 'dev' }}
# The name of the group to which the app is deployed (via Firebase App Distribution)
FIREBASE_GROUP: "general"
permissions:
contents: read
jobs:
build-android:
runs-on: macos-latest
steps:
# Setup
- name: Print environment
run: echo "Environment = $ENVIRONMENT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Fetch part of the commit history and its tags, in order to calculate the build number in `build-setup`
fetch-depth: ${{ vars.FETCH_DEPTH }}
fetch-tags: true
- name: Set up build
uses: ./.github/actions/build-setup
with:
# Should only have a value when building for prod and the stores; otherwise, will be undefined and the build number will be calculated
BUILD_NUMBER_OVERRIDE: ${{ inputs.BUILD_NUMBER_STORES }}
ENV_CONFIG_JS: ${{ vars[format('{0}_CONFIG_JS', env.ENVIRONMENT)] }}
ENV_GOOGLE_SERVICES: ${{ vars[format('{0}_GOOGLE_SERVICES', env.ENVIRONMENT)] }}
# Build the app
- name: Build the app (.apk)
run: npm run build:app:android --env="$ENVIRONMENT"
- name: Rename build output
id: rename-android
run: |
mv "./platforms/android/app/build/outputs/apk/debug/app-debug.apk" "opal-${ENVIRONMENT}.apk"
echo "ARTIFACT_NAME=opal-${ENVIRONMENT}.apk" >> "$GITHUB_OUTPUT"
- name: Archive build output
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: android-app
path: ${{ steps.rename-android.outputs.ARTIFACT_NAME }}
outputs:
ARTIFACT_NAME: ${{ steps.rename-android.outputs.ARTIFACT_NAME }}
build-android-release:
runs-on: macos-latest
# Build in release mode for prod
if: ${{ inputs.ENVIRONMENT == 'prod' }}
steps:
# Setup
- name: Print environment
run: echo "Environment = $ENVIRONMENT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Fetch part of the commit history and its tags, in order to calculate the build number in `build-setup`
fetch-depth: ${{ vars.FETCH_DEPTH }}
fetch-tags: true
- name: Set up build
uses: ./.github/actions/build-setup
with:
BUILD_NUMBER_OVERRIDE: ${{ inputs.BUILD_NUMBER_STORES }}
ENV_CONFIG_JS: ${{ vars[format('{0}_CONFIG_JS', env.ENVIRONMENT)] }}
ENV_GOOGLE_SERVICES: ${{ vars[format('{0}_GOOGLE_SERVICES', env.ENVIRONMENT)] }}
# Build an aab in release mode
- name: Build the app (.aab, release mode)
run: |
npm run build:app:android:release --env="$ENVIRONMENT"
mv "./platforms/android/app/build/outputs/bundle/release/app-release.aab" "app-release-unsigned.aab"
# Sign the aab
- name: Sign the app
run: |
echo "$ANDROID_UPLOAD_KEY_BASE64" > android_upload_key_base64.txt
base64 --decode --input android_upload_key_base64.txt --output android_upload_key.jks
jarsigner -verbose -sigalg SHA512withRSA -digestalg SHA512 -keystore android_upload_key.jks -storepass "$ANDROID_UPLOAD_KEY_PASSWORD" -signedjar app-release-signed.aab app-release-unsigned.aab "$ANDROID_UPLOAD_KEY_ALIAS"
env:
ANDROID_UPLOAD_KEY_BASE64: ${{ secrets.ANDROID_UPLOAD_KEY_BASE64 }}
ANDROID_UPLOAD_KEY_PASSWORD: ${{ secrets.ANDROID_UPLOAD_KEY_PASSWORD }}
ANDROID_UPLOAD_KEY_ALIAS: ${{ vars.ANDROID_UPLOAD_KEY_ALIAS }}
- name: Rename build output
id: rename-android
run: |
mv "app-release-signed.aab" "opal-${ENVIRONMENT}-release-signed.aab"
echo "ARTIFACT_NAME=opal-${ENVIRONMENT}-release-signed.aab" >> "$GITHUB_OUTPUT"
- name: Archive build output
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: android-app-release
path: ${{ steps.rename-android.outputs.ARTIFACT_NAME }}
outputs:
ARTIFACT_NAME: ${{ steps.rename-android.outputs.ARTIFACT_NAME }}
build-ios:
runs-on: macos-latest
steps:
# Setup
- name: Print environment
run: echo "Environment = $ENVIRONMENT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Fetch part of the commit history and its tags, in order to calculate the build number in `build-setup`
fetch-depth: ${{ vars.FETCH_DEPTH }}
fetch-tags: true
- name: Set up build
uses: ./.github/actions/build-setup
with:
# Should only have a value when building for prod and the stores; otherwise, will be undefined and the build number will be calculated
BUILD_NUMBER_OVERRIDE: ${{ inputs.BUILD_NUMBER_STORES }}
ENV_CONFIG_JS: ${{ vars[format('{0}_CONFIG_JS', env.ENVIRONMENT)] }}
ENV_GOOGLE_SERVICES: ${{ vars[format('{0}_GOOGLE_SERVICES', env.ENVIRONMENT)] }}
# Install an Apple certificate and provisioning profile used to build the app for iOS
# See: https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64_FILE }}
P12_PASSWORD: ${{ secrets.BUILD_CERTIFICATE_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets[format('{0}_PROVISIONING_PROFILE_BASE64_FILE', env.ENVIRONMENT)] }} # zizmor: ignore[overprovisioned-secrets]
KEYCHAIN_PASSWORD: ${{ secrets.TEMPORARY_KEYCHAIN_PASSWORD }}
# Use a YAML anchor for reuse: https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations#yaml-anchors-and-aliases
run: &ios-setup |
# Create variables
CERTIFICATE_PATH="$RUNNER_TEMP"/build_certificate.p12
PP_PATH="$RUNNER_TEMP"/build_pp.mobileprovision
KEYCHAIN_PATH="$RUNNER_TEMP"/app-signing.keychain-db
# Import certificate and provisioning profile from secrets
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o "$CERTIFICATE_PATH"
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o "$PP_PATH"
# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Options: -lut: lock keychain when the system sleeps, lock keychain after timeout interval, specify timeout interval in seconds
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Import certificate to keychain
# Options: -P: specify wrapping passphrase immediately; -A: allow any application to access the imported key without warning; -t: type; -f: format; -k: target keychain to import into
security import "$CERTIFICATE_PATH" -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
# Options: -S: comma-separated list of of allowed partition IDs; -k: password for keychain (required)
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
# Options: -d: use the specified preference domain; -s: set the search list to the specified keychains
security list-keychain -d user -s "$KEYCHAIN_PATH"
# Apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp "$PP_PATH" ~/Library/MobileDevice/Provisioning\ Profiles
# Build the app
- name: Build the app (.ipa)
run: npm run build:app:ios:ci --env="$ENVIRONMENT" --devteam="$IOS_DEVELOPMENT_TEAM" --provisioningprofile="$PROVISIONING_PROFILE_UUID"
env:
IOS_DEVELOPMENT_TEAM: ${{ secrets.IOS_DEVELOPMENT_TEAM }}
PROVISIONING_PROFILE_UUID: ${{ secrets[format('{0}_PROVISIONING_PROFILE_UUID', env.ENVIRONMENT)] }} # zizmor: ignore[overprovisioned-secrets]
- name: Rename build output
id: rename-ios
run: |
mv ./platforms/ios/build/Debug-iphoneos/*.ipa "opal-${ENVIRONMENT}.ipa"
echo "ARTIFACT_NAME=opal-${ENVIRONMENT}.ipa" >> "$GITHUB_OUTPUT"
- name: Archive build output
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ios-app
path: ${{ steps.rename-ios.outputs.ARTIFACT_NAME }}
outputs:
ARTIFACT_NAME: ${{ steps.rename-ios.outputs.ARTIFACT_NAME }}
build-ios-release:
runs-on: macos-latest
# Build in release mode for prod
if: ${{ inputs.ENVIRONMENT == 'prod' }}
steps:
# Setup
- name: Print environment
run: echo "Environment = $ENVIRONMENT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
# Fetch part of the commit history and its tags, in order to calculate the build number in `build-setup`
fetch-depth: ${{ vars.FETCH_DEPTH }}
fetch-tags: true
- name: Set up build
uses: ./.github/actions/build-setup
with:
BUILD_NUMBER_OVERRIDE: ${{ inputs.BUILD_NUMBER_STORES }}
ENV_CONFIG_JS: ${{ vars[format('{0}_CONFIG_JS', env.ENVIRONMENT)] }}
ENV_GOOGLE_SERVICES: ${{ vars[format('{0}_GOOGLE_SERVICES', env.ENVIRONMENT)] }}
# Install an Apple certificate and provisioning profile used to build the app for iOS
# See: https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_DISTRIBUTION_BASE64_FILE }}
P12_PASSWORD: ${{ secrets.BUILD_CERTIFICATE_DISTRIBUTION_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets[format('{0}_PROVISIONING_PROFILE_DISTRIBUTION_BASE64_FILE', env.ENVIRONMENT)] }} # zizmor: ignore[overprovisioned-secrets]
KEYCHAIN_PASSWORD: ${{ secrets.TEMPORARY_KEYCHAIN_PASSWORD }}
# Reuse the same setup steps from build-ios
run: *ios-setup
# Build the app in release mode
- name: Build the app (.ipa, release mode)
run: npm run build:app:ios:release:ci --env="$ENVIRONMENT" --devteam="$IOS_DEVELOPMENT_TEAM" --provisioningprofile="$PROVISIONING_PROFILE_UUID"
env:
IOS_DEVELOPMENT_TEAM: ${{ secrets.IOS_DEVELOPMENT_TEAM }}
PROVISIONING_PROFILE_UUID: ${{ secrets[format('{0}_PROVISIONING_PROFILE_DISTRIBUTION_UUID', env.ENVIRONMENT)] }} # zizmor: ignore[overprovisioned-secrets]
- name: Rename build output
id: rename-ios
run: |
mv ./platforms/ios/build/Release-iphoneos/*.ipa "opal-${ENVIRONMENT}-release.ipa"
echo "ARTIFACT_NAME=opal-${ENVIRONMENT}-release.ipa" >> "$GITHUB_OUTPUT"
- name: Archive build output
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: ios-app-release
path: ${{ steps.rename-ios.outputs.ARTIFACT_NAME }}
outputs:
ARTIFACT_NAME: ${{ steps.rename-ios.outputs.ARTIFACT_NAME }}
deploy-android-to-firebase:
needs: build-android
runs-on: ubuntu-latest
# Deploy manually via inputs, or automatically (to dev) when building on main
if: ${{ inputs.DEPLOY_FIREBASE || github.ref_name == 'main' }}
steps:
# Setup
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download Android build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: android-app
run-id: ${{ github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up deployment to Firebase
uses: ./.github/actions/deploy-app-firebase-setup
with:
ENVIRONMENT: ${{ env.ENVIRONMENT }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets[format('{0}_FIREBASE_SERVICE_ACCOUNT', env.ENVIRONMENT)] }} # zizmor: ignore[overprovisioned-secrets]
# Deploy the app to Firebase app distribution
# Deployment via firebase-tools implicitly uses a service account assigned to $GOOGLE_APPLICATION_CREDENTIALS (from values defined in the GitHub project settings)
# This service account provides permissions for Firebase app distribution
# See: https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments
- name: Deploy the app
run: npx firebase-tools appdistribution:distribute "$ARTIFACT_NAME" --app "$FIREBASE_APP_ANDROID" --release-notes "$RELEASE_NOTES" --groups "$FIREBASE_GROUP"
env:
ARTIFACT_NAME: ${{ needs.build-android.outputs.ARTIFACT_NAME }}
FIREBASE_APP_ANDROID: ${{ vars[format('{0}_FIREBASE_APP_ANDROID', env.ENVIRONMENT)] }}
FIREBASE_GROUP: ${{ env.FIREBASE_GROUP }}
deploy-ios-to-firebase:
needs: build-ios
runs-on: ubuntu-latest
# Deploy manually via inputs, or automatically (to dev) when building on main
if: ${{ inputs.DEPLOY_FIREBASE || github.ref_name == 'main' }}
steps:
# Setup
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download iOS build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: ios-app
run-id: ${{ github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up deployment to Firebase
uses: ./.github/actions/deploy-app-firebase-setup
with:
ENVIRONMENT: ${{ env.ENVIRONMENT }}
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets[format('{0}_FIREBASE_SERVICE_ACCOUNT', env.ENVIRONMENT)] }} # zizmor: ignore[overprovisioned-secrets]
# Deploy the app to Firebase app distribution
# Deployment implicitly uses $GOOGLE_APPLICATION_CREDENTIALS; see deploy-android-to-firebase above for more details
- name: Deploy the app
run: npx firebase-tools appdistribution:distribute "$ARTIFACT_NAME" --app "$FIREBASE_APP_IOS" --release-notes "$RELEASE_NOTES" --groups "$FIREBASE_GROUP"
env:
ARTIFACT_NAME: ${{ needs.build-ios.outputs.ARTIFACT_NAME }}
FIREBASE_APP_IOS: ${{ vars[format('{0}_FIREBASE_APP_IOS', env.ENVIRONMENT)] }}
FIREBASE_GROUP: ${{ env.FIREBASE_GROUP }}
verify-ios-build:
needs: build-ios-release
runs-on: macos-latest
steps:
# Setup
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download iOS release build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: ios-app-release
run-id: ${{ github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
# Verify that the ipa can be accepted by Apple
- name: Verify the bundle with Apple
run: xcrun altool --validate-app -f "$ARTIFACT_NAME" -t ios -u "$APPLE_STORE_UPLOAD_EMAIL" -p "$APPLE_STORE_UPLOAD_PASSWORD"
env:
ARTIFACT_NAME: ${{ needs.build-ios-release.outputs.ARTIFACT_NAME }}
APPLE_STORE_UPLOAD_EMAIL: ${{ vars.APPLE_STORE_UPLOAD_EMAIL }}
APPLE_STORE_UPLOAD_PASSWORD: ${{ secrets.APPLE_STORE_UPLOAD_PASSWORD }}
deploy-ios-to-store:
needs: [build-ios-release, verify-ios-build]
runs-on: macos-latest
# Deploy manually via inputs
if: ${{ inputs.DEPLOY_STORES }}
steps:
# Setup
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download iOS release build artifact
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: ios-app-release
run-id: ${{ github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
# Deploy the ipa to Apple
- name: Deploy the bundle to Apple
run: xcrun altool --upload-app -f "$ARTIFACT_NAME" -t ios -u "$APPLE_STORE_UPLOAD_EMAIL" -p "$APPLE_STORE_UPLOAD_PASSWORD"
env:
ARTIFACT_NAME: ${{ needs.build-ios-release.outputs.ARTIFACT_NAME }}
APPLE_STORE_UPLOAD_EMAIL: ${{ vars.APPLE_STORE_UPLOAD_EMAIL }}
APPLE_STORE_UPLOAD_PASSWORD: ${{ secrets.APPLE_STORE_UPLOAD_PASSWORD }}