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:
Commit fixtures:
git add FreezeRay/Fixtures/
git commit -m "Add frozen fixtures"
Verify fixtures are in git:
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:
Cache dependencies:
- name : Cache Swift dependencies
uses : actions/cache@v3
with :
path : ~/Library/Developer/Xcode/DerivedData
key : ${{ runner.os }}-xcode-${{ hashFiles('**/Package.resolved') }}
Run only FreezeRay tests:
- run : xcodebuild test -only-testing:MyAppTests/DriftTests
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:
Reset simulator state:
- name : Reset simulator
run : |
xcrun simctl shutdown all
xcrun simctl erase all
Increase test timeout:
// In test file
@Test (timeout : . seconds ( 60 ))
func testMigration () throws { /* ... */ }
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."
}
}
]
}
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
First Freeze Tutorial for freezing your first schema
Testing Migrations Learn to write custom migration tests
Drift Detection Understand how drift detection works
freeze command CLI reference for freezing schemas