From e3bc23b9317ced15d27db888d64dc124542607ce Mon Sep 17 00:00:00 2001 From: David Schilling Date: Fri, 6 Feb 2026 15:54:33 +0100 Subject: [PATCH 1/5] Add GitHub Actions iOS CI workflow --- .github/workflows/ios-ci.yml | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/ios-ci.yml diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml new file mode 100644 index 0000000..8798e0f --- /dev/null +++ b/.github/workflows/ios-ci.yml @@ -0,0 +1,88 @@ +name: iOS CI + +on: + push: + branches: + - '**' + pull_request: + workflow_dispatch: + +concurrency: + group: ios-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build and Test (SOPA) + runs-on: macos-15 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Show Xcode version + run: | + xcodebuild -version + swift --version + + - name: Resolve Swift packages + run: | + xcodebuild \ + -project SOPA.xcodeproj \ + -scheme SOPA \ + -resolvePackageDependencies + + - name: Find iOS simulator destination + id: sim + shell: bash + run: | + set -euo pipefail + RUNTIME=$(xcrun simctl list runtimes available | grep -E 'iOS 18\\.' | head -n 1 | awk -F'[()]' '{print $2}') + + if [[ -z "${RUNTIME:-}" ]]; then + echo "No iOS 18.x runtime found on this runner." + xcrun simctl list runtimes available + exit 1 + fi + + DEVICE_UDID=$(xcrun simctl list devices available "${RUNTIME}" | grep 'iPhone 16 (' | head -n 1 | awk -F '[()]' '{print $2}') + + if [[ -z "${DEVICE_UDID:-}" ]]; then + echo "No iPhone 16 simulator found for runtime ${RUNTIME}." + xcrun simctl list devices available "${RUNTIME}" + exit 1 + fi + + echo "destination=id=${DEVICE_UDID}" >> "$GITHUB_OUTPUT" + echo "Using destination id=${DEVICE_UDID} on runtime ${RUNTIME}" + + - name: Build + run: | + xcodebuild \ + -project SOPA.xcodeproj \ + -scheme SOPA \ + -destination '${{ steps.sim.outputs.destination }}' \ + build + + - name: Test + run: | + xcodebuild \ + -project SOPA.xcodeproj \ + -scheme SOPA \ + -destination '${{ steps.sim.outputs.destination }}' \ + -resultBundlePath TestResults.xcresult \ + test + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: xcresult + path: TestResults.xcresult + if-no-files-found: ignore From fb51275a7b98814cda2e271d481ecb56719b24b3 Mon Sep 17 00:00:00 2001 From: David Schilling Date: Fri, 6 Feb 2026 15:57:02 +0100 Subject: [PATCH 2/5] Make simulator selection runtime-agnostic in CI --- .github/workflows/ios-ci.yml | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 8798e0f..553245d 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -43,18 +43,34 @@ jobs: shell: bash run: | set -euo pipefail - RUNTIME=$(xcrun simctl list runtimes available | grep -E 'iOS 18\\.' | head -n 1 | awk -F'[()]' '{print $2}') + # Pick the newest available iOS runtime on the runner. + RUNTIME=$( + xcrun simctl list runtimes available \ + | awk -F'[()]' '/iOS [0-9]+/{print $2}' \ + | tail -n 1 + ) if [[ -z "${RUNTIME:-}" ]]; then - echo "No iOS 18.x runtime found on this runner." + echo "No iOS runtime found on this runner." xcrun simctl list runtimes available exit 1 fi - DEVICE_UDID=$(xcrun simctl list devices available "${RUNTIME}" | grep 'iPhone 16 (' | head -n 1 | awk -F '[()]' '{print $2}') + # Prefer iPhone 16 to match local development, fallback to any available iPhone. + DEVICE_UDID=$( + xcrun simctl list devices available "${RUNTIME}" \ + | awk -F'[()]' '/iPhone 16 /{print $2; exit}' + ) if [[ -z "${DEVICE_UDID:-}" ]]; then - echo "No iPhone 16 simulator found for runtime ${RUNTIME}." + DEVICE_UDID=$( + xcrun simctl list devices available "${RUNTIME}" \ + | awk -F'[()]' '/iPhone [0-9]+/{print $2; exit}' + ) + fi + + if [[ -z "${DEVICE_UDID:-}" ]]; then + echo "No iPhone simulator found for runtime ${RUNTIME}." xcrun simctl list devices available "${RUNTIME}" exit 1 fi From 90be54e17b37bd5fb36ea9e53986e1bc040b13a1 Mon Sep 17 00:00:00 2001 From: David Schilling Date: Fri, 6 Feb 2026 15:58:26 +0100 Subject: [PATCH 3/5] Use xcodebuild showdestinations to pick simulator in CI --- .github/workflows/ios-ci.yml | 36 ++++++++++-------------------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 553245d..1c77565 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -43,40 +43,24 @@ jobs: shell: bash run: | set -euo pipefail - # Pick the newest available iOS runtime on the runner. - RUNTIME=$( - xcrun simctl list runtimes available \ - | awk -F'[()]' '/iOS [0-9]+/{print $2}' \ - | tail -n 1 + DEST_LINE=$( + xcodebuild \ + -project SOPA.xcodeproj \ + -scheme SOPA \ + -showdestinations \ + | grep -m1 "platform:iOS Simulator" || true ) - if [[ -z "${RUNTIME:-}" ]]; then - echo "No iOS runtime found on this runner." - xcrun simctl list runtimes available - exit 1 - fi - - # Prefer iPhone 16 to match local development, fallback to any available iPhone. - DEVICE_UDID=$( - xcrun simctl list devices available "${RUNTIME}" \ - | awk -F'[()]' '/iPhone 16 /{print $2; exit}' - ) - - if [[ -z "${DEVICE_UDID:-}" ]]; then - DEVICE_UDID=$( - xcrun simctl list devices available "${RUNTIME}" \ - | awk -F'[()]' '/iPhone [0-9]+/{print $2; exit}' - ) - fi + DEVICE_UDID=$(echo "${DEST_LINE}" | sed -n 's/.*id:\([^,}]*\).*/\1/p' | tr -d ' ') if [[ -z "${DEVICE_UDID:-}" ]]; then - echo "No iPhone simulator found for runtime ${RUNTIME}." - xcrun simctl list devices available "${RUNTIME}" + echo "No usable iOS Simulator destination found for scheme SOPA." + xcodebuild -project SOPA.xcodeproj -scheme SOPA -showdestinations exit 1 fi echo "destination=id=${DEVICE_UDID}" >> "$GITHUB_OUTPUT" - echo "Using destination id=${DEVICE_UDID} on runtime ${RUNTIME}" + echo "Using destination id=${DEVICE_UDID}" - name: Build run: | From 1051245fd99869a03853440c8ba261569c101efe Mon Sep 17 00:00:00 2001 From: David Schilling Date: Fri, 6 Feb 2026 16:01:34 +0100 Subject: [PATCH 4/5] Create simulator device when only placeholder destination exists --- .github/workflows/ios-ci.yml | 43 ++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index 1c77565..e73dcc6 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -51,14 +51,49 @@ jobs: | grep -m1 "platform:iOS Simulator" || true ) - DEVICE_UDID=$(echo "${DEST_LINE}" | sed -n 's/.*id:\([^,}]*\).*/\1/p' | tr -d ' ') + DEVICE_UDID=$(echo "${DEST_LINE}" | sed -n 's/.*id:\([0-9A-F-]\{36\}\).*/\1/p') if [[ -z "${DEVICE_UDID:-}" ]]; then - echo "No usable iOS Simulator destination found for scheme SOPA." - xcodebuild -project SOPA.xcodeproj -scheme SOPA -showdestinations - exit 1 + echo "No concrete simulator destination found, creating one via simctl." + + RUNTIME_ID=$( + xcrun simctl list runtimes available \ + | sed -n 's/.*(\(com\.apple\.CoreSimulator\.SimRuntime\.iOS-[^)]*\)).*/\1/p' \ + | tail -n 1 + ) + + DEVICE_TYPE_ID=$( + xcrun simctl list devicetypes \ + | sed -n 's/.*iPhone 16.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' \ + | head -n 1 + ) + + if [[ -z "${DEVICE_TYPE_ID:-}" ]]; then + DEVICE_TYPE_ID=$( + xcrun simctl list devicetypes \ + | sed -n 's/.*iPhone.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' \ + | head -n 1 + ) + fi + + if [[ -z "${RUNTIME_ID:-}" || -z "${DEVICE_TYPE_ID:-}" ]]; then + echo "Unable to resolve runtime/device type for simulator creation." + xcrun simctl list runtimes available + xcrun simctl list devicetypes + exit 1 + fi + + DEVICE_UDID=$(xcrun simctl create "SOPA-CI-iPhone" "${DEVICE_TYPE_ID}" "${RUNTIME_ID}") + + if [[ -z "${DEVICE_UDID:-}" ]]; then + echo "Failed to create simulator device." + exit 1 + fi fi + xcrun simctl boot "${DEVICE_UDID}" || true + xcrun simctl bootstatus "${DEVICE_UDID}" -b || true + echo "destination=id=${DEVICE_UDID}" >> "$GITHUB_OUTPUT" echo "Using destination id=${DEVICE_UDID}" From fb4869e9ea952b3d97cdb1e9ea67c0f939691d24 Mon Sep 17 00:00:00 2001 From: David Schilling Date: Fri, 6 Feb 2026 16:33:48 +0100 Subject: [PATCH 5/5] Fix simctl runtime parser for Xcode 26 output format --- .github/workflows/ios-ci.yml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ios-ci.yml b/.github/workflows/ios-ci.yml index e73dcc6..f614be0 100644 --- a/.github/workflows/ios-ci.yml +++ b/.github/workflows/ios-ci.yml @@ -58,20 +58,29 @@ jobs: RUNTIME_ID=$( xcrun simctl list runtimes available \ - | sed -n 's/.*(\(com\.apple\.CoreSimulator\.SimRuntime\.iOS-[^)]*\)).*/\1/p' \ + | sed -nE 's/.*- (com\.apple\.CoreSimulator\.SimRuntime\.iOS-[[:alnum:]-]+).*/\1/p' \ | tail -n 1 ) + if [[ -z "${RUNTIME_ID:-}" ]]; then + # Fallback for alternate simctl output formats. + RUNTIME_ID=$( + xcrun simctl list runtimes available \ + | sed -nE 's/.*\((com\.apple\.CoreSimulator\.SimRuntime\.iOS-[^)]*)\).*/\1/p' \ + | tail -n 1 + ) + fi + DEVICE_TYPE_ID=$( xcrun simctl list devicetypes \ - | sed -n 's/.*iPhone 16.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' \ - | head -n 1 + | sed -nE 's/.*iPhone 16.*\((com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*)\).*/\1/p' \ + | tail -n 1 ) if [[ -z "${DEVICE_TYPE_ID:-}" ]]; then DEVICE_TYPE_ID=$( xcrun simctl list devicetypes \ - | sed -n 's/.*iPhone.*(\(com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*\)).*/\1/p' \ + | sed -nE 's/.*iPhone.*\((com\.apple\.CoreSimulator\.SimDeviceType\.[^)]*)\).*/\1/p' \ | head -n 1 ) fi