If you're an indie developer working on a Unity project, you've probably felt it: building for iOS and Android manually eats up half your day, and the App Store submission process is tedious enough to mess up even when you've done it dozens of times.
The good news is that GitHub Actions combined with GameCI can fully automate that pipeline — from the moment you push code, right through to a TestFlight build ready for testers. And writing the workflow YAML? That's exactly where Antigravity shines.
Why GitHub Actions + GameCI Is the Right Choice for Indie Developers
There are a few ways to automate Unity builds, but the GitHub Actions + GameCI combination stands out for indie developers for specific reasons.
Cost: GitHub Actions gives you 2,000 free Linux runner minutes per month. Most Unity test runs and basic builds land in the 15–30 minute range, so you can comfortably run 60–100 builds per month without paying anything. macOS runners (required for iOS builds) cost 10x more, but limiting those to main branch merges only keeps costs manageable.
Simplicity: GameCI provides Docker images specifically built for Unity. It handles Unity installation and license activation automatically — you don't need to write your own Dockerfile or figure out how to install Unity headlessly on a CI server.
Antigravity compatibility: YAML syntax errors, mismatched secret names, and Unity-specific configuration mistakes are exactly the kind of thing Antigravity catches quickly. Paste your error log into the chat and you get a targeted fix, not a documentation link to scroll through.
For deeper coverage of CI/CD patterns with Antigravity, see the Advanced GitHub Actions CI/CD Pipeline with Antigravity guide.
A Basic Workflow You Can Set Up in 30 Minutes
Start with the simplest useful thing: automatically running Unity's PlayMode tests on every push. Tell Antigravity your project structure and ask it to generate a GameCI-based Unity test workflow, and you'll get something close to this:
# .github/workflows/unity-ci.yml
name: Unity CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Unity Test Runner
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true
- name: Cache Unity Library
uses: actions/cache@v4
with:
path: Library
key: Library-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-
- name: Run PlayMode Tests
uses: game-ci/unity-test-runner@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
projectPath: .
testMode: PlayMode
artifactsPath: test-results
githubToken: ${{ secrets.GITHUB_TOKEN }}
checkName: Unity PlayMode Test Results
- name: Upload Test Results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ github.run_number }}
path: test-resultsThe first place most people get stuck is the UNITY_LICENSE secret. Ask Antigravity "how do I get the value for the UNITY_LICENSE secret?" and it will walk you through the GameCI activation flow — generating a license request file locally, activating it through Unity, and Base64-encoding the result for the secret.
How to Use Antigravity Effectively When Writing Workflows
After setting up several of these pipelines, a few patterns make a real difference.
Paste error logs directly: When a GitHub Actions run fails, copy the entire error output into Antigravity. It's good at distinguishing between YAML indentation issues, wrong secret names, and GameCI version mismatches — and it tells you exactly which line to fix. Don't summarize the error; paste it verbatim.
Add features incrementally: Build the pipeline in stages — tests first, then builds, then deployment. If you try to write everything at once and something breaks, it's hard to know where the problem is. Ask Antigravity to add one stage at a time: "The test step is working. Now add an Android build job."
Include your ProjectSettings in the context: Paste your directory structure or the contents of ProjectSettings/ProjectSettings.asset into the chat. Antigravity can then generate YAML that accounts for your actual Bundle ID, minimum SDK version, and build targets — instead of producing generic templates you'd need to manually adjust.
iOS Builds and Cost Management
iOS builds require macOS runners, which are significantly more expensive. The practical approach for indie developers is to be selective about when macOS runners run.
Key strategies for managing costs:
- Only trigger iOS builds on pushes to
main, not on pull requests - Use
needs: testso the build job only runs if tests pass - Cache the Unity Library directory to avoid full rebuilds every time
build-ios:
name: iOS Build
runs-on: macos-latest
needs: test # Only runs if test job succeeds
if: github.ref == 'refs/heads/main' # Main branch only
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true
- name: Cache Unity Library (iOS)
uses: actions/cache@v4
with:
path: Library
key: Library-iOS-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-iOS-
Library-
- name: Build iOS Xcode Project
uses: game-ci/unity-builder@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
with:
targetPlatform: iOS
buildName: MyGame
buildsPath: build
- name: Upload iOS Build Artifact
uses: actions/upload-artifact@v4
with:
name: ios-build-${{ github.run_number }}
path: build/iOSConverting the generated Xcode project to an IPA and signing it for the App Store requires additional codesigning configuration. The Fastlane × Antigravity App Store Automation guide covers that in detail.
Uploading to TestFlight with Fastlane
Once GameCI produces an Xcode project, you can pipe it straight to TestFlight using Fastlane with an App Store Connect API key — no password authentication needed.
- name: Sign and Upload to TestFlight
env:
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
CERTIFICATE_P12: ${{ secrets.CERTIFICATE_P12 }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
PROVISIONING_PROFILE: ${{ secrets.PROVISIONING_PROFILE }}
run: |
gem install fastlane --no-document
fastlane ios betaThe corresponding Fastfile can be generated by asking Antigravity: "Write a Fastfile that builds an IPA from a Unity-generated Xcode project and uploads it to TestFlight using an App Store Connect API key." It also outputs the list of secrets you'll need to add to GitHub — which is useful when you're setting this up for the first time.
Adding an Android Build Job
Adding Android to the same workflow is straightforward since Android builds run on Linux runners (no extra cost). The key difference is that Android requires a keystore for signing, which you'll store as a base64-encoded GitHub Secret.
build-android:
name: Android Build
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true
- name: Cache Unity Library (Android)
uses: actions/cache@v4
with:
path: Library
key: Library-Android-${{ hashFiles('Assets/**', 'Packages/**', 'ProjectSettings/**') }}
restore-keys: |
Library-Android-
Library-
- name: Build Android APK/AAB
uses: game-ci/unity-builder@v4
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
ANDROID_KEYSTORE_NAME: mygame.keystore
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
ANDROID_KEYALIAS_NAME: ${{ secrets.ANDROID_KEYALIAS_NAME }}
ANDROID_KEYALIAS_PASS: ${{ secrets.ANDROID_KEYALIAS_PASS }}
with:
targetPlatform: Android
androidAppBundle: true # Outputs .aab for Play Store
androidKeystoreName: mygame.keystore
androidKeystoreBase64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
androidKeystorePass: ${{ secrets.ANDROID_KEYSTORE_PASS }}
androidKeyaliasName: ${{ secrets.ANDROID_KEYALIAS_NAME }}
androidKeyaliasPass: ${{ secrets.ANDROID_KEYALIAS_PASS }}
- name: Upload Android Build
uses: actions/upload-artifact@v4
with:
name: android-build-${{ github.run_number }}
path: build/AndroidAsk Antigravity "how do I create an Android keystore and base64-encode it for a GitHub Secret?" and it will give you the exact keytool command and the one-liner to encode the resulting file. This is the kind of multi-step setup where having Antigravity walk you through it in sequence saves a lot of back-and-forth with scattered documentation.
One thing worth knowing: if your game uses Android App Bundles (.aab) for Play Store submission rather than APKs, set androidAppBundle: true as shown above. Google Play now requires AAB for new apps, and Antigravity will flag this if you ask it to review your workflow for Play Store compatibility.
What Changes When Builds Are Automated
The most noticeable shift after setting up this pipeline isn't the time saved on individual builds — it's that the waiting becomes productive. Before, a local Unity build meant sitting there half-watching the editor. With CI running in the background, you push the code and immediately go back to working on the next thing in Antigravity.
If you're starting from scratch, just implement the test-only workflow first. Builds and deployment can come later. Even just having tests run automatically on every push eliminates a whole category of "I forgot to test this before committing" bugs.
For more on speeding up your Unity development workflow with Antigravity, see the Unity × Antigravity Fast Development guide.