This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Build iOS Test Container App | ||
env: | ||
# Relative path from the monorepo root to the app | ||
test_container_app_path: tests/rn-test-container | ||
# Package name in the monorepo | ||
test_app_package_name: rn-test-container | ||
# Should match the name of "ios/<app_name>.xcworkspace" | ||
ios_app_name: RNTestContainer | ||
# A unique ID to identify the app on GitHub Actions, this is used as part of cache keys and artifact names, must be unique among all workflows | ||
app_id: ios-rn-test-container-app | ||
# The command used to prebuild the app | ||
prebuild_command: yarn prebuild:native --platform ios --no-install # --no-install is used to skip installing dependencies, specifically `pod install` as we want to do it after the Cache Pods step | ||
# These should be set in the repository secrets | ||
# Redis database used for caching and remembering things between runs, such as the last build number | ||
# KV_STORE_REDIS_REST_URL: | ||
# KV_STORE_REDIS_REST_TOKEN: | ||
on: | ||
workflow_call: | ||
inputs: | ||
configuration: | ||
required: true | ||
type: string | ||
description: "Either 'Debug' or 'Release'." | ||
outputs: | ||
build-hash: | ||
description: "A hash to identify the build." | ||
value: ${{ jobs.build-ios.outputs.build-hash }} | ||
built-app-cache-key: | ||
description: "The GitHub Actions cache key of the built .app, can be used in subsequent workflows to get the built app from cache using this key." | ||
value: ${{ jobs.build-ios.outputs.built-app-cache-key }} | ||
built-app-path: | ||
description: "The path to the built .app relative to the repository root." | ||
value: ${{ jobs.build-ios.outputs.built-app-path }} | ||
jobs: | ||
build-ios: | ||
name: Build | ||
# runs-on: macos-13 | ||
runs-on: [self-hosted, macOS] | ||
permissions: | ||
contents: read | ||
pull-requests: read | ||
timeout-minutes: 60 | ||
outputs: | ||
built-app-cache-key: ${{ steps.check-has-build.outputs.cache-primary-key || steps.pre-check-has-build.outputs.cache-primary-key }} # The order is important here, since in the "pre-check-has-build" step, it's possible that the build hash part of the key is "null" - in such case, the cache won't be found at that step and the `check-has-build` step will be executed. So it's always safe to place the `check-has-build` before the `pre-check-has-build` step for this condition. | ||
build-hash: ${{ steps.calculate-build-hash.outputs.build_hash || steps.get-build-hash-from-cache.outputs.build_hash }} # Same as above, the order is important here. | ||
built-app-path: ${{ steps.get-built-app-path.outputs.built_app_path }} | ||
defaults: | ||
run: | ||
working-directory: ${{ env.test_container_app_path }} | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v4 | ||
- name: Setup upterm session | ||
uses: lhotari/action-upterm@v1 | ||
- name: Get Built App Path | ||
id: get-built-app-path | ||
env: | ||
BUILT_APP_PATH: ${{ env.test_container_app_path }}/build/Build/Products/${{ inputs.configuration }}-iphonesimulator/${{ env.ios_app_name }}.app | ||
run: | | ||
echo "Built app path: $BUILT_APP_PATH" | ||
echo "built_app_path=$BUILT_APP_PATH" >> $GITHUB_OUTPUT | ||
- name: Calculate Pre-Build Hash | ||
id: calculate-pre-build-hash | ||
env: | ||
# A hash that can save us more time if we can already know that the build hash will not change. | ||
# | ||
# Calculating the build_hash relies on generated files, for example, `Podfile.lock`, and it’ll take some time to run `yarn install`, `expo prebuild` and `pod install` in order to get that. But if `yarn.lock` didn’t change, there’s no likely that `Podfile.lock` will change - we can leverage that and skip some installation steps. | ||
# | ||
# This hash MUST be different if the build_hash will be different. | ||
# | ||
# This hash can be different if the build_hash remains the same. For example, if `yarn.lock` changes, `Podfile.lock` may not change if the updated package contains no native code. | ||
PRE_BUILD_HASH: ${{ hashFiles('yarn.lock', format('{0}/app.json', env.test_container_app_path), 'packages/vxrn/expo-plugin.cjs') }} | ||
run: | | ||
if [ -z "$PRE_BUILD_HASH" ]; then | ||
echo '[ERROR] Failed to calculate pre-build hash.' | ||
fi | ||
echo "Pre-build hash: $PRE_BUILD_HASH" | ||
echo "pre_build_hash=$PRE_BUILD_HASH" >> $GITHUB_OUTPUT | ||
- name: Read Cached Build Hash | ||
id: get-build-hash-from-cache | ||
env: | ||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }} | ||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }} | ||
run: | | ||
BUILD_HASH_FROM_CACHE=$(curl "$KV_STORE_REDIS_REST_URL/get/${{ env.app_id }}-build-hash-from-pre-build-hash-${{ steps.calculate-pre-build-hash.outputs.pre_build_hash }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" | jq -r '.result') | ||
if [ "$BUILD_HASH_FROM_CACHE" != "null" ]; then | ||
curl -X POST "$KV_STORE_REDIS_REST_URL/EXPIRE/${{ env.app_id }}-build-hash-from-pre-build-hash-${{ steps.calculate-pre-build-hash.outputs.pre_build_hash }}/2592000" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" # Reset TTL to 30 days | ||
echo "Build hash from cache: $BUILD_HASH_FROM_CACHE" | ||
echo "build_hash=$BUILD_HASH_FROM_CACHE" >> $GITHUB_OUTPUT | ||
else | ||
echo 'No cached build hash found.' | ||
echo "build_hash=null" >> $GITHUB_OUTPUT | ||
fi | ||
- name: Check If Build Already Exists | ||
uses: actions/cache/restore@v4 | ||
id: pre-check-has-build | ||
if: ${{ steps.get-build-hash-from-cache.outputs.build_hash != 'null' }} | ||
with: | ||
key: ${{ env.app_id }}-${{ inputs.configuration }}-${{ steps.get-build-hash-from-cache.outputs.build_hash }} | ||
lookup-only: true | ||
path: ${{ steps.get-built-app-path.outputs.built_app_path }} | ||
# The steps below are somehow WET (Write Everything Twice) since GitHub Actions doesn't support early exit at the time of writing. | ||
# So we basically add a if condition to every step below to skip them if we have a cache hit. | ||
- name: Install | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }} | ||
uses: ./.github/actions/install | ||
with: | ||
# To save time, only install dependencies for the test container app | ||
workspace-focus: ${{ env.test_app_package_name }} | ||
- name: Prebuild | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }} | ||
run: ${{ env.prebuild_command }} | ||
- name: Cache Pods | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }} | ||
uses: actions/cache@v4 | ||
env: | ||
cache-name: ${{ env.app_id }}-pods | ||
with: | ||
path: ${{ env.test_container_app_path }}/ios/Pods | ||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles(format('{0}/ios/Podfile', env.test_container_app_path)) }} | ||
restore-keys: | | ||
${{ runner.os }}-${{ env.cache-name }}- | ||
- name: Pod Install | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }} | ||
run: | | ||
pod install --project-directory=ios | ||
- name: Calculate Build Hash | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }} | ||
id: calculate-build-hash | ||
env: | ||
# We need to list all files that will affect native code in hashFiles. | ||
BUILD_HASH: ${{ hashFiles(format('{0}/ios/Podfile.lock', env.test_container_app_path), format('{0}/app.json', env.test_container_app_path), 'packages/vxrn/expo-plugin.cjs') }} | ||
run: | | ||
if [ -z "$BUILD_HASH" ]; then | ||
echo '[ERROR] Failed to calculate build hash.' | ||
exit 1 | ||
fi | ||
echo "Build hash: $BUILD_HASH" | ||
echo "build_hash=$BUILD_HASH" >> $GITHUB_OUTPUT | ||
- name: Write Build Hash | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }} | ||
env: | ||
KV_STORE_REDIS_REST_URL: ${{ secrets.KV_STORE_REDIS_REST_URL }} | ||
KV_STORE_REDIS_REST_TOKEN: ${{ secrets.KV_STORE_REDIS_REST_TOKEN }} | ||
run: | | ||
curl -X POST "$KV_STORE_REDIS_REST_URL/SETEX/${{ env.app_id }}-build-hash-from-pre-build-hash-${{ steps.calculate-pre-build-hash.outputs.pre_build_hash }}/2592000/${{ steps.calculate-build-hash.outputs.build_hash }}" -H "Authorization: Bearer $KV_STORE_REDIS_REST_TOKEN" # Save and set TTL to 30 days | ||
- name: Check If Build Already Exists | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit }} | ||
uses: actions/cache/restore@v4 | ||
id: check-has-build | ||
with: | ||
key: ${{ env.app_id }}-${{ inputs.configuration }}-${{ steps.calculate-build-hash.outputs.build_hash }} | ||
lookup-only: true | ||
path: ${{ steps.get-built-app-path.outputs.built_app_path }} | ||
- name: Restore Build Cache | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }} | ||
id: restore-build-cache | ||
uses: actions/cache/restore@v4 | ||
env: | ||
cache-name: ${{ env.app_id }}-build | ||
with: | ||
# This cache can be huge, so we share it among all configurations instead of creating a cache for each one. | ||
# key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.configuration }}-${{ steps.calculate-build-hash.outputs.build_hash }} | ||
# restore-keys: | | ||
# ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.configuration }}- | ||
# ${{ runner.os }}-${{ env.cache-name }}- | ||
key: ${{ runner.os }}-${{ env.cache-name }} | ||
path: | | ||
${{ env.test_container_app_path }}/build | ||
- name: Build | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }} | ||
run: | | ||
set -o pipefail # Since we pipe the output to xcpretty, we need this to fail this step if `xcrun xcodebuild` fails | ||
xcrun xcodebuild -scheme '${{ env.ios_app_name }}' \ | ||
-workspace 'ios/${{ env.ios_app_name }}.xcworkspace' \ | ||
-configuration ${{ inputs.configuration }} \ | ||
-sdk 'iphonesimulator' \ | ||
-destination 'generic/platform=iOS Simulator' \ | ||
-derivedDataPath build | tee xcodebuild.log | xcpretty | ||
- name: Upload Built App to Cache | ||
if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }} | ||
uses: actions/cache/save@v4 | ||
with: | ||
key: ${{ steps.check-has-build.outputs.cache-primary-key }} | ||
path: ${{ steps.get-built-app-path.outputs.built_app_path }} | ||
- name: Upload Build Log | ||
uses: actions/[email protected] | ||
if: ${{ always() && !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }} | ||
with: | ||
name: xcodebuild-${{ env.app_id }}-${{ inputs.configuration }}.log | ||
path: | | ||
${{ env.test_container_app_path }}/xcodebuild.log | ||
- name: Save Build Cache | ||
uses: actions/cache/save@v4 | ||
if: ${{ always() && !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }} | ||
with: | ||
key: ${{ steps.restore-build-cache.outputs.cache-primary-key }} | ||
path: | | ||
${{ env.test_container_app_path }}/build | ||
# Not useful since it's hard to find which run that actually built the app. Instead, we re-upload the app to artifacts in subsequent workflows if tests failed. | ||
# - name: Upload Built App to Artifacts | ||
# if: ${{ !steps.pre-check-has-build.outputs.cache-hit && !steps.check-has-build.outputs.cache-hit }} | ||
# uses: actions/[email protected] | ||
# continue-on-error: true | ||
# with: | ||
# name: $${{ format('{0}-{1}', env.app_id, inputs.configuration) }} | ||
# path: | | ||
# ${{ steps.get-built-app-path.outputs.built_app_path }} |