Skip to main content

CI Integration

Set up FreezeRay in your CI pipeline to catch schema drift and migration failures before they reach production. This guide covers GitHub Actions, GitLab CI, and general CI/CD principles.

Why CI Integration?

Problem: Developers can accidentally modify frozen schemas locally, commit changes, and push to main before running tests. Solution: CI automatically runs drift and migration tests on every push and pull request, blocking changes that would break production.

What You’ll Need

  • Frozen schemas with fixtures committed to git
  • A test target with drift and migration tests
  • CI/CD platform (GitHub Actions, GitLab CI, Bitrise, etc.)
  • macOS CI runners (required for Xcode builds)

GitHub Actions Setup

Basic Configuration

Create .github/workflows/test.yml:
name: Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: macos-latest  # Required for Xcode

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_15.2.app

      - name: Run tests
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -resultBundlePath TestResults

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: TestResults.xcresult
This runs all tests (including FreezeRay drift and migration tests) on every push and PR.

Targeted FreezeRay Tests

To run only FreezeRay tests (faster feedback):
jobs:
  schema-validation:
    runs-on: macos-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_15.2.app

      - name: Run drift detection tests
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/DriftTests

      - name: Run migration tests
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/MigrationTests
Use -only-testing to run specific test classes for faster CI feedback.

Parallel Jobs

Run drift and migration tests in parallel:
jobs:
  drift-tests:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - run: sudo xcode-select -s /Applications/Xcode_15.2.app
      - run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/DriftTests

  migration-tests:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - run: sudo xcode-select -s /Applications/Xcode_15.2.app
      - run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/MigrationTests

Caching Dependencies

Speed up builds with caching:
jobs:
  test:
    runs-on: macos-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Cache Swift dependencies
        uses: actions/cache@v3
        with:
          path: |
            ~/Library/Developer/Xcode/DerivedData
            .build
          key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
          restore-keys: |
            ${{ runner.os }}-spm-

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_15.2.app

      - name: Run tests
        run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15'

Matrix Testing

Test on multiple simulators:
jobs:
  test:
    runs-on: macos-latest

    strategy:
      matrix:
        simulator:
          - iPhone 15
          - iPhone 15 Pro
          - iPad Pro (12.9-inch) (6th generation)

    steps:
      - uses: actions/checkout@v3
      - run: sudo xcode-select -s /Applications/Xcode_15.2.app
      - run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=${{ matrix.simulator }}'

GitLab CI Setup

Create .gitlab-ci.yml:
stages:
  - test

test:
  stage: test
  tags:
    - macos  # Must use macOS runner
  script:
    - xcodebuild test
        -scheme MyApp
        -destination 'platform=iOS Simulator,name=iPhone 15'
        -resultBundlePath TestResults
  artifacts:
    when: always
    paths:
      - TestResults.xcresult
    expire_in: 1 week

Separate Drift and Migration Tests

drift-tests:
  stage: test
  tags:
    - macos
  script:
    - xcodebuild test
        -scheme MyApp
        -destination 'platform=iOS Simulator,name=iPhone 15'
        -only-testing:MyAppTests/DriftTests

migration-tests:
  stage: test
  tags:
    - macos
  script:
    - xcodebuild test
        -scheme MyApp
        -destination 'platform=iOS Simulator,name=iPhone 15'
        -only-testing:MyAppTests/MigrationTests

Bitrise Setup

Create bitrise.yml:
workflows:
  test:
    steps:
      - git-clone: {}
      - xcode-test:
          inputs:
            - scheme: MyApp
            - destination: 'platform=iOS Simulator,name=iPhone 15'
            - generate_code_coverage_files: yes
Or use the Bitrise UI to add an “Xcode Test” step with:
  • Scheme: MyApp
  • Destination: platform=iOS Simulator,name=iPhone 15

CircleCI Setup

Create .circleci/config.yml:
version: 2.1

jobs:
  test:
    macos:
      xcode: 15.2.0

    steps:
      - checkout

      - run:
          name: Run tests
          command: |
            xcodebuild test \
              -scheme MyApp \
              -destination 'platform=iOS Simulator,name=iPhone 15'

      - store_test_results:
          path: TestResults

workflows:
  build-and-test:
    jobs:
      - test

Best Practices

1. Fail Fast on Drift

Run drift tests first (they’re faster than migration tests):
jobs:
  drift:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/DriftTests

  migration:
    runs-on: macos-latest
    needs: drift  # Only run if drift tests pass
    steps:
      - uses: actions/checkout@v3
      - run: |
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/MigrationTests

2. Run on Every Pull Request

Catch drift before merging:
on:
  pull_request:
    branches: [main]

3. Block Merging on Test Failures

GitHub: Enable “Require status checks to pass before merging” in branch protection rules. GitLab: Set pipeline to “must succeed” in merge request settings.

4. Clear Error Messages

When tests fail, the error is logged in CI:
❌ Schema drift detected in sealed version 1.0.0

Expected checksum: 0cc298858e409d8b9c7e37a7779af0e2
Actual checksum:   26d70c10e9febf7f8c5a9e3d4b2a1f0e

→ Sealed schemas are immutable - create a new schema version instead.
This tells developers exactly what went wrong and how to fix it.

5. Test Coverage Reports

Generate code coverage for migration tests:
- name: Run tests with coverage
  run: |
    xcodebuild test \
      -scheme MyApp \
      -destination 'platform=iOS Simulator,name=iPhone 15' \
      -enableCodeCoverage YES \
      -resultBundlePath TestResults

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    files: TestResults.xcresult

Common CI Issues

Issue 1: Simulator Not Found

Error:
xcodebuild: error: Unable to find a destination matching the provided destination specifier:
  { platform:iOS Simulator, name:iPhone 17 }
Cause: iPhone 17 simulator not available in CI Xcode version. Fix: List available simulators:
- name: List available simulators
  run: xcrun simctl list devices available
Use a simulator that exists:
- run: |
    xcodebuild test \
      -scheme MyApp \
      -destination 'platform=iOS Simulator,name=iPhone 15'

Issue 2: Fixtures Not Found

Error:
⚠️  No frozen fixture found for v1.0.0
Searched in:
  - Bundle: ...
  - Working directory: ...
Run: freezeray freeze 1.0.0
Cause: Fixtures not committed to git or not included in test bundle. Fix:
  1. Commit fixtures:
    git add FreezeRay/Fixtures/
    git commit -m "Add frozen fixtures"
    
  2. Verify fixtures are in git:
    git ls-files FreezeRay/
    
  3. Ensure fixtures are in test bundle (Xcode projects):
    • Select test target → Build Phases → Copy Bundle Resources
    • Add FreezeRay/ folder

Issue 3: Build Takes Too Long

Problem: CI builds timeout (>10 minutes). Solutions:
  1. Cache dependencies:
    - name: Cache Swift dependencies
      uses: actions/cache@v3
      with:
        path: ~/Library/Developer/Xcode/DerivedData
        key: ${{ runner.os }}-xcode-${{ hashFiles('**/Package.resolved') }}
    
  2. Run only FreezeRay tests:
    - run: xcodebuild test -only-testing:MyAppTests/DriftTests
    
  3. Use faster macOS runners (GitHub: macos-latest, not macos-11)

Issue 4: Permission Denied Errors

Error:
xcodebuild: error: Could not resolve package dependencies:
  Permission denied
Cause: CI runner doesn’t have Xcode command line tools set up. Fix:
- name: Select Xcode version
  run: sudo xcode-select -s /Applications/Xcode_15.2.app

- name: Accept Xcode license
  run: sudo xcodebuild -license accept

Issue 5: Test Flakiness

Problem: Tests sometimes pass, sometimes fail. Causes:
  • Race conditions in test setup
  • Simulator state not reset between runs
  • Network-dependent tests
Fix:
  1. Reset simulator state:
    - name: Reset simulator
      run: |
        xcrun simctl shutdown all
        xcrun simctl erase all
    
  2. Increase test timeout:
    // In test file
    @Test(timeout: .seconds(60))
    func testMigration() throws { /* ... */ }
    
  3. Isolate test data:
    func testMigration() throws {
        // Use unique temp directory per test
        let testDir = URL(fileURLWithPath: NSTemporaryDirectory())
            .appendingPathComponent(UUID().uuidString)
        // ...
    }
    

Advanced Workflows

Pre-Merge Freeze Checks

Ensure no new schema versions are frozen without proper review:
name: Schema Freeze Check

on:
  pull_request:
    paths:
      - 'FreezeRay/Fixtures/**'

jobs:
  check-freeze:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Check for new frozen schemas
        run: |
          NEW_FIXTURES=$(git diff --name-only origin/main... | grep "FreezeRay/Fixtures")
          if [ -n "$NEW_FIXTURES" ]; then
            echo "⚠️  New frozen schemas detected:"
            echo "$NEW_FIXTURES"
            echo ""
            echo "Please ensure:"
            echo "  1. Schema changes were reviewed"
            echo "  2. Migration tests are added"
            echo "  3. Documentation is updated"
          fi

Slack Notifications on Failure

Notify team when drift is detected:
- name: Notify on drift failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    webhook-url: ${{ secrets.SLACK_WEBHOOK }}
    payload: |
      {
        "text": "⚠️ Schema drift detected in PR #${{ github.event.pull_request.number }}",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "Schema drift detected! Review the PR before merging."
            }
          }
        ]
      }

Auto-Comment on PRs

Add a comment to PRs when drift is detected:
- name: Comment on PR
  if: failure()
  uses: actions/github-script@v6
  with:
    script: |
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: '❌ Schema drift detected! Frozen schemas cannot be modified. Please create a new schema version instead.'
      })

Monitoring and Reporting

Track Schema Changes Over Time

Log schema versions in CI:
- name: Report schema versions
  run: |
    echo "Frozen schema versions:"
    ls FreezeRay/Fixtures/

Generate Migration Report

Create a summary of all migrations:
- name: Generate migration report
  run: |
    echo "# Migration Report" > migration_report.md
    echo "" >> migration_report.md
    echo "Frozen versions:" >> migration_report.md
    for dir in FreezeRay/Fixtures/*/; do
      version=$(basename "$dir")
      echo "- $version" >> migration_report.md
    done

- name: Upload report
  uses: actions/upload-artifact@v3
  with:
    name: migration-report
    path: migration_report.md

Example: Full GitHub Actions Workflow

Here’s a complete, production-ready workflow:
name: FreezeRay Schema Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  drift-tests:
    name: Drift Detection
    runs-on: macos-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Cache Swift dependencies
        uses: actions/cache@v3
        with:
          path: |
            ~/Library/Developer/Xcode/DerivedData
            .build
          key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
          restore-keys: |
            ${{ runner.os }}-spm-

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_15.2.app

      - name: List available simulators
        run: xcrun simctl list devices available | grep iPhone

      - name: Run drift detection tests
        run: |
          set -o pipefail
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/DriftTests \
            -resultBundlePath TestResults \
            | xcpretty

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: drift-test-results
          path: TestResults.xcresult

  migration-tests:
    name: Migration Tests
    runs-on: macos-latest
    needs: drift-tests  # Only run if drift tests pass

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Cache Swift dependencies
        uses: actions/cache@v3
        with:
          path: |
            ~/Library/Developer/Xcode/DerivedData
            .build
          key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
          restore-keys: |
            ${{ runner.os }}-spm-

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_15.2.app

      - name: Run migration tests
        run: |
          set -o pipefail
          xcodebuild test \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 15' \
            -only-testing:MyAppTests/MigrationTests \
            -resultBundlePath TestResults \
            | xcpretty

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: migration-test-results
          path: TestResults.xcresult

      - name: Comment on PR (if failed)
        if: failure() && github.event_name == 'pull_request'
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '❌ Migration tests failed! Review the test results before merging.'
            })

Summary

You’ve learned how to:
  • ✅ Set up FreezeRay tests in GitHub Actions, GitLab CI, and other CI platforms
  • ✅ Run drift and migration tests in parallel
  • ✅ Fail fast with drift detection
  • ✅ Handle common CI issues
  • ✅ Add notifications and reporting
CI integration ensures schema changes are caught before they reach production!

Next Steps