Skip to content
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:

Check failure on line 37 in .github/workflows/build-ios-test-container-app.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/build-ios-test-container-app.yml

Invalid workflow file

You have an error in your yaml syntax on line 37
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 }}