Fix CI flakiness: data-stream races, QoS inversions, deterministic tests #1674
This file contains hidden or 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: CI | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| TEST_TIMEOUT_MINUTES: 30 | |
| # TEMPORARY: 1 so any flake fails the job and the log artifacts actually | |
| # upload. Restore to 3 before merging. | |
| TEST_RETRY_ATTEMPTS: 1 | |
| jobs: | |
| build-and-test: | |
| name: Build & Test | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md | |
| - os: macos-15 | |
| xcode: 16.4 | |
| platform: "iOS Simulator,name=iPhone 16 Pro,OS=18.5" | |
| - os: macos-15 | |
| xcode: 16.4 | |
| platform: "macOS" | |
| - os: macos-15 | |
| xcode: 16.4 | |
| platform: "macOS,variant=Mac Catalyst" | |
| - os: macos-15 | |
| xcode: 16.4 | |
| platform: "tvOS Simulator,name=Apple TV,OS=18.5" | |
| # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "iOS Simulator,name=iPhone 17 Pro,OS=26.5" | |
| symbol-graph: true | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "iOS Simulator,name=iPhone 17 Pro,OS=26.5" | |
| extension-api-only: true | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "macOS" | |
| symbol-graph: true | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "macOS" | |
| asan: true | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "macOS" | |
| tsan: true | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "macOS" | |
| strict-concurrency-env: true | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "macOS,variant=Mac Catalyst" | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "visionOS Simulator,name=Apple Vision Pro,OS=26.5" | |
| - os: macos-26 | |
| xcode: 26.6 | |
| platform: "tvOS Simulator,name=Apple TV,OS=26.5" | |
| # https://github.com/actions/runner-images/blob/main/images/macos/xcode-27-Readme.md | |
| - os: xcode-27 | |
| xcode: latest | |
| platform: "macOS" | |
| - os: xcode-27 | |
| xcode: latest | |
| platform: "macOS,variant=Mac Catalyst" | |
| - os: xcode-27 | |
| xcode: latest | |
| platform: "iOS Simulator,name=iPhone 17 Pro,OS=27.0" | |
| - os: xcode-27 | |
| xcode: latest | |
| platform: "visionOS Simulator,name=Apple Vision Pro,OS=27.0" | |
| # This image has no plain "Apple TV" device, unlike macos-15/macos-26. | |
| - os: xcode-27 | |
| xcode: latest | |
| platform: "tvOS Simulator,name=Apple TV 4K (3rd generation),OS=27.0" | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 | |
| env: | |
| LIBDISPATCH_COOPERATIVE_POOL_STRICT: ${{ matrix.strict-concurrency-env == true && '1' || '0' }} | |
| NSUnbufferedIO: "YES" | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Run LiveKit Server | |
| id: livekit-server | |
| uses: livekit/dev-server-action@61e2b4dcb170dd3591e0c9b0db3c3fe5db93b500 | |
| with: | |
| github-token: ${{ github.token }} | |
| config: 'room: { departure_timeout: 1 }' | |
| - uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: ${{ matrix.xcode }} | |
| - name: Xcode Version | |
| run: xcodebuild -version | |
| - name: Swift Version | |
| run: xcrun swift --version | |
| - name: Build & Test | |
| uses: nick-fields/retry@v4 | |
| with: | |
| timeout_minutes: ${{ env.TEST_TIMEOUT_MINUTES }} | |
| max_attempts: ${{ env.TEST_RETRY_ATTEMPTS }} | |
| command: | | |
| set -o pipefail && xcodebuild test \ | |
| -scheme LiveKit \ | |
| -destination 'platform=${{ matrix.platform }}' \ | |
| -enableAddressSanitizer ${{ matrix.asan == true && 'YES' || 'NO' }} \ | |
| -enableThreadSanitizer ${{ matrix.tsan == true && 'YES' || 'NO' }} \ | |
| APPLICATION_EXTENSION_API_ONLY=${{ matrix.extension-api-only == true && 'YES' || 'NO' }} \ | |
| -only-testing:LiveKitCoreTests \ | |
| -only-testing:LiveKitObjCTests \ | |
| -parallel-testing-enabled NO \ | |
| | tee -a "$RUNNER_TEMP/xcodebuild.log" \ | |
| | xcbeautify --renderer github-actions | |
| # xcbeautify's github-actions renderer collapses a parameterized Swift Testing | |
| # failure to "Recorded an issue (1) argument(s)" — no message, no failing | |
| # argument. xcodebuild's own output has both, so keep it for post-mortems | |
| # instead of re-running CI blind. Appended across retry attempts. | |
| - name: Upload Test Log | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-log-${{ strategy.job-index }} | |
| path: ${{ runner.temp }}/xcodebuild.log | |
| retention-days: 5 | |
| # Client-side logs can't distinguish "we gave up" from "the SFU hung up on | |
| # us": a failed e2e test just shows `Connection reset by peer`. The server's | |
| # JSONL log records the participant close reason, so capture both sides. | |
| - name: Upload Server Log | |
| if: ${{ failure() }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: server-log-${{ strategy.job-index }} | |
| path: ${{ steps.livekit-server.outputs.log-path }} | |
| retention-days: 5 | |
| - name: Build for Release | |
| if: ${{ matrix.symbol-graph }} | |
| run: | | |
| set -o pipefail && xcodebuild build\ | |
| -scheme LiveKit \ | |
| -configuration Release \ | |
| -destination 'platform=${{ matrix.platform }}' \ | |
| APPLICATION_EXTENSION_API_ONLY=${{ matrix.extension-api-only == true && 'YES' || 'NO' }} \ | |
| BUILD_LIBRARY_FOR_DISTRIBUTION=YES \ | |
| OTHER_SWIFT_FLAGS="-Xfrontend -emit-symbol-graph\ | |
| -Xfrontend -emit-symbol-graph-dir\ | |
| -Xfrontend \"${PWD}/symbol-graph\"" \ | |
| DOCC_EXTRACT_EXTENSION_SYMBOLS=YES | xcbeautify --renderer github-actions | |
| - name: Upload Symbol Graph | |
| if: ${{ matrix.symbol-graph }} | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: symbol-graph-${{ matrix.platform }} | |
| path: symbol-graph | |
| retention-days: 1 | |
| lint: | |
| name: Lint | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Install SwiftLint | |
| run: brew install swiftlint | |
| - name: SwiftLint | |
| run: swiftlint lint --strict --reporter github-actions-logging | |
| - name: SwiftFormat Lint | |
| if: always() | |
| run: swiftformat --lint . --reporter github-actions-log | |
| # Comes pre-installed on macOS runners | |
| build-docs: | |
| name: Build Docs | |
| needs: build-and-test | |
| if: always() | |
| runs-on: macos-26 | |
| steps: | |
| - name: Checkout Documentation Catalog | |
| uses: actions/checkout@v7 | |
| with: | |
| sparse-checkout: Sources/LiveKit/LiveKit.docc | |
| - uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Download Symbol Graphs | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: symbol-graph-* | |
| path: symbol-graphs | |
| - name: List Symbol Graphs | |
| run: cd symbol-graphs && ls -al | |
| - name: Build Docs | |
| run: | | |
| $(xcrun --find docc) convert \ | |
| Sources/LiveKit/LiveKit.docc \ | |
| --output-dir docs \ | |
| --additional-symbol-graph-dir symbol-graphs \ | |
| --transform-for-static-hosting \ | |
| --hosting-base-path /reference/client-sdk-swift/ | |
| - name: Archive | |
| run: zip -r docs.zip docs | |
| - name: Upload Archive | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: docs | |
| path: docs.zip | |
| retention-days: 1 | |
| app-size: | |
| name: App Size | |
| runs-on: macos-26 | |
| timeout-minutes: 30 | |
| env: | |
| # Budget for the SDK's uncompressed .app delta, in MB. Bump deliberately | |
| # when a size increase is intended (see the job summary for the measured value). | |
| SIZE_BASELINE_MB: "17.5" | |
| SIZE_TOLERANCE_PCT: "5" | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| - uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Install tools | |
| run: | | |
| brew install xcodegen swiftly swift-sh | |
| swiftly init --quiet-shell-followup --skip-install -y | |
| # Archives an empty app + a LiveKit app (unsigned, arm64 device slice) and | |
| # writes the size breakdown to the job summary, failing if over budget. | |
| - name: Measure app size | |
| run: | | |
| swiftly run +xcode swift-sh .github/size-check/size-check.swift \ | |
| --baseline "$SIZE_BASELINE_MB" \ | |
| --tolerance "$SIZE_TOLERANCE_PCT" | |
| check-protocol: | |
| name: Check Protocol | |
| runs-on: macos-26 | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| submodules: recursive | |
| - uses: maxim-lobanov/setup-xcode@v1 | |
| with: | |
| xcode-version: latest-stable | |
| - name: Install swift-protobuf | |
| run: brew install protobuf swift-protobuf | |
| - name: Generate Swift protobuf files | |
| run: make proto | |
| - name: Check working tree | |
| run: | | |
| if ! git diff --quiet; then | |
| echo "::error::Working tree is not clean after running 'make proto'" | |
| echo "The following files have been modified:" | |
| git diff --name-only | |
| echo "" | |
| echo "Please run 'make proto' locally and commit the changes." | |
| exit 1 | |
| else | |
| echo "Working tree is clean - all Swift protobuf files are up to date" | |
| fi | |
| check-changes: | |
| name: Check Changes | |
| if: github.event_name == 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Check for .changes files | |
| id: check-changes | |
| run: | | |
| if [ -z "$(git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} | grep '^\.changes/')" ]; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Comment on PR | |
| if: steps.check-changes.outputs.has_changes == 'false' | |
| uses: mshick/add-pr-comment@v3 | |
| with: | |
| message: | | |
| ⚠️ This PR does not contain any files in the `.changes` directory. | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} |