name: iOS build and tests on: push: branches: [main] pull_request: workflow_dispatch: permissions: contents: read concurrency: group: ios-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: simulator: runs-on: macos-15 timeout-minutes: 25 steps: - uses: actions/checkout@v4 with: persist-credentials: false - name: Inspect toolchain and project run: | xcodebuild -version swift --version python3 tools/validate_project.py xcodebuild -list -project MusicBridge.xcodeproj - name: Choose available iPhone simulator run: | xcrun simctl list devices available --json > /tmp/musicbridge-simulators.json python3 tools/select_simulator.py /tmp/musicbridge-simulators.json "$(xcrun --sdk iphonesimulator --show-sdk-version)" >> "$GITHUB_ENV" - name: Build and run XCTest shell: bash run: | set -o pipefail mkdir -p build python3 -u tools/tls_fixture.py > build/tls-fixture.log 2>&1 & TLS_PID=$! trap 'kill "$TLS_PID" 2>/dev/null || true' EXIT for attempt in {1..30}; do if grep -q 'TLS fixtures ready' build/tls-fixture.log; then break; fi if ! kill -0 "$TLS_PID" 2>/dev/null; then cat build/tls-fixture.log; exit 1; fi sleep 1 done grep -q 'TLS fixtures ready' build/tls-fixture.log || { cat build/tls-fixture.log; exit 1; } xcodebuild test \ -project MusicBridge.xcodeproj \ -scheme MusicBridge \ -configuration Debug \ -destination "platform=iOS Simulator,id=$SIMULATOR_ID" \ -destination-timeout 120 \ -derivedDataPath DerivedData \ -resultBundlePath build/MusicBridge.xcresult \ CODE_SIGNING_ALLOWED=NO \ | tee build/xcodebuild.log - name: Save diagnostics if: always() uses: actions/upload-artifact@v4 with: name: ios-test-results path: | build/xcodebuild.log build/tls-fixture.log build/MusicBridge.xcresult if-no-files-found: warn retention-days: 7 - name: Archive simulator application run: ditto -c -k --sequesterRsrc --keepParent DerivedData/Build/Products/Debug-iphonesimulator/MusicBridge.app build/MusicBridge-simulator.zip - name: Capture pairing screen run: | xcrun simctl bootstatus "$SIMULATOR_ID" -b xcrun simctl install "$SIMULATOR_ID" DerivedData/Build/Products/Debug-iphonesimulator/MusicBridge.app xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge sleep 3 xcrun simctl io "$SIMULATOR_ID" screenshot build/pairing-screen.png xcrun simctl terminate "$SIMULATOR_ID" ru.yukinoki.musicbridge xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player sleep 2 xcrun simctl io "$SIMULATOR_ID" screenshot build/player-light-screen.png xcrun simctl ui "$SIMULATOR_ID" appearance dark sleep 1 xcrun simctl io "$SIMULATOR_ID" screenshot build/player-dark-screen.png xcrun simctl terminate "$SIMULATOR_ID" ru.yukinoki.musicbridge xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player --preview-large-text sleep 2 xcrun simctl io "$SIMULATOR_ID" screenshot build/player-large-text-screen.png xcrun simctl terminate "$SIMULATOR_ID" ru.yukinoki.musicbridge xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player --preview-live-card sleep 2 xcrun simctl io "$SIMULATOR_ID" screenshot build/live-card-dark-screen.png xcrun simctl ui "$SIMULATOR_ID" appearance light sleep 1 xcrun simctl io "$SIMULATOR_ID" screenshot build/live-card-light-screen.png RUNTIME=$(python3 -c 'import json,os; d=json.load(open("/tmp/musicbridge-simulators.json")); print(next(k for k,v in d["devices"].items() if any(x["udid"]==os.environ["SIMULATOR_ID"] for x in v)))') COMPACT_ID=$(xcrun simctl create MusicBridge-Compact com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation "$RUNTIME") xcrun simctl bootstatus "$COMPACT_ID" -b xcrun simctl install "$COMPACT_ID" DerivedData/Build/Products/Debug-iphonesimulator/MusicBridge.app xcrun simctl launch "$COMPACT_ID" ru.yukinoki.musicbridge --preview-player sleep 3 xcrun simctl io "$COMPACT_ID" screenshot build/player-compact-screen.png - name: Save pairing screen uses: actions/upload-artifact@v4 with: name: ios-ui-preview path: build/*-screen.png retention-days: 7 - name: Save simulator application uses: actions/upload-artifact@v4 with: name: MusicBridge-simulator path: build/MusicBridge-simulator.zip retention-days: 7 - name: Build iPhone IPA for local signing shell: bash run: bash tools/package_ipa.sh - name: Save iPhone IPA uses: actions/upload-artifact@v4 with: name: MusicBridge-iPhone path: build/MusicBridge-iPhone.ipa if-no-files-found: error retention-days: 7 - name: Save device build log if: always() uses: actions/upload-artifact@v4 with: name: ios-device-build-log path: build/device-build.log if-no-files-found: ignore retention-days: 7