How to build an AI agent harness around CI/CD with fastlane
AI coding agents can write a feature in minutes. The hard part is everything after that: tests, signing, builds and the moment code reaches real users. Here is how I wrap a mobile CI/CD pipeline around agents with fastlane, so they move fast and still can't ship anything on their own.
What “harness” means here
An AI agent harness is everything around the model that decides what it may touch, how its work is checked and who lets it out. For a mobile app the harness has a simple rule: the agent proposes, the pipeline verifies, a human releases. fastlane is a good backbone for this, because it turns every build, test and upload step into a named command that behaves the same on a laptop, on an agent box and in CI.
Layer 1 — one entry point for humans and agents
Agents get confused by ten ways of doing the same thing. Give them one. Every check lives in a fastlane lane, and the agent's instructions (an AGENTS.md or similar file in the repo) say exactly which lanes it may run.
default_platform(:ios)
platform :ios do
before_all { setup_ci if ENV["CI"] }
# Safe for agents: no secrets needed
lane :check do
run_tests(scheme: "App", devices: ["iPhone 16"])
end
# CI only: needs signing + App Store Connect access
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_P8"],
is_key_content_base64: true
)
match(type: "appstore", readonly: true, api_key: api_key)
increment_build_number(
build_number: latest_testflight_build_number(api_key: api_key) + 1
)
build_app(scheme: "App", export_method: "app-store")
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
end
end
The check lane needs nothing secret, so an agent can run it as often as it likes. The beta lane can only succeed where release secrets exist — and they only exist in CI.
Layer 2 — keep signing out of the agent's reach
Code signing is the real “ship” button, so it is the first thing to fence off:
- Certificates and profiles live in an encrypted
matchrepository. CI reads them withreadonly: true; nothing in the pipeline can create or revoke them. - App Store Connect access uses an API key stored as a CI secret, never a personal Apple ID and never a file in the repo.
- The agent's environment has none of these. No
MATCH_PASSWORD, no.p8key. If an agent tries to runbeta, it fails loudly — exactly what you want.
Layer 3 — CI that verifies, and a human who releases
Pull requests (including the ones agents open) run the safe lane. Merges to main run the release lane inside a protected environment that waits for a human reviewer before any secret is exposed.
name: ios
on:
pull_request:
push:
branches: [main]
jobs:
check:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bundle exec fastlane ios check
beta:
if: github.ref == 'refs/heads/main'
needs: check
runs-on: macos-15
environment: release # required reviewers = the human gate
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bundle exec fastlane ios beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
Store the release secrets as environment secrets on release, not repository-wide, so a job outside that environment cannot read them.
Layer 4 — protect the harness itself
An agent that can edit the pipeline can edit its own rules. Close that loop:
- Branch protection on
main: pull requests only, required status checks, at least one human approval. - A
CODEOWNERSentry forfastlane/and.github/workflows/, so any change to the harness needs an explicit human review. - The agent's git token can push branches and open pull requests — never push to
mainor change repository settings.
Android uses the same pattern
The Android side mirrors iOS: a check lane with gradle(task: "test"), and a CI-only lane that builds a release bundle and calls upload_to_play_store(track: "internal") with a Play service-account key stored as an environment secret. Start on the internal track; promotion to production stays a human decision.
Checklist
- Every build, test and upload step is a fastlane lane.
- Agents may run only lanes that need no secrets.
- Signing via
matchin read-only mode; App Store Connect via an API key. - Release jobs run in a protected environment with a required reviewer.
CODEOWNERSguardsfastlane/and the CI workflows.
With this in place, agents can iterate as fast as they like inside the fence, and every build that reaches TestFlight or Play has passed the same tests and the same human gate. Building Android on a remote Linux machine? The next step is keeping that VPS on a private VPN.