Mobile CI with fastlane skill for Codex
Signed builds from a clean runner, every time. Codex writes fastlane lanes that run on a fresh CI runner with no one logged in: App Store Connect API keys instead of Apple ID sessions, match in readonly mode behind setup_ci, a keystore decoded from a secret, and build numbers read from TestFlight and Play tracks. Production promotion stays a separate manual lane, and nothing touching shared certificates runs without your approval.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- mobile-ci-fastlane Installs to ~/.agents/skills/mobile-ci-fastlane/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install mobile-ci-fastlane The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project mobile-ci-fastlane Installs to .agents/skills/mobile-ci-fastlane/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/mobile-ci-fastlane.zipunzip -o mobile-ci-fastlane.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) mobile-ci-fastlane PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $mobile-ci-fastlane, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Automates iOS and Android builds and releases with fastlane in CI - Fastfile lanes for tests, beta and production, match for shared iOS certificates and profiles, App Store Connect API keys instead of Apple ID logins, setup_ci keychains, build numbers from TestFlight and Play tracks, Android keystore handling, upload_to_testflight and upload_to_play_store, plus GitHub Actions workflows. Use when setting up mobile CI, fixing code signing failures on a CI runner, adding a TestFlight or Play internal track lane, or moving from manual Xcode releases.
Use it when
- Automating iOS or Android builds and store uploads in CI
- Code signing works locally and fails on CI runners
- Sharing certificates and profiles across a team with match
- Build number collisions break uploads
Use something else when
- The app builds through Expo EAS: use expo-eas-builds
- You need App Review strategy or a manual Xcode release: use ios-app-store-release
How to check the result
A run on a fresh CI runner produces a TestFlight build and a Play internal release with new build numbers, a rerun does not collide, and the logs contain no secret values.
The mistake it prevents
Running match on CI without setup_ci, which installs the certificate into a keychain the build cannot read and fails with no signing certificate found.
Done well means: a clean CI runner can build, sign and upload either platform with no human logged in, signing assets live encrypted in one place with read-only access from CI, build numbers never collide, and production promotion is a separate, deliberate step.
Before changing anything
- Read
AGENTS.md, thenfastlane/Fastfile,Appfile,Matchfile,Pluginfile, theGemfileand existing CI workflows. - Confirm fastlane runs through Bundler with a pinned version: a
Gemfilecontaininggem "fastlane"and a committedGemfile.lock. Always invokebundle exec fastlane. - Inventory signing: iOS team ID, bundle IDs for app and every extension, current certificates and profiles, whether match is already set up; Android keystore location, alias and whether Play App Signing is enabled.
- Inventory secrets the CI provider holds. Never print them, echo them into logs, or commit decoded files.
- Running lanes that upload builds or change signing assets touches shared accounts.
Confirm with the user before the first real upload, and never run
match nukeor revoke certificates without explicit approval: it breaks every other machine’s builds.
Credentials
| Platform | Use | Avoid |
|---|---|---|
| iOS uploads and metadata | App Store Connect API key (.p8, key ID, issuer ID) | Apple ID + password / 2FA sessions on CI |
| iOS signing | match with a private git repo, S3 or GCS storage, readonly on CI | Exporting certs by hand onto runners |
| Android uploads | Google Play service account JSON with release permissions for this app only | Personal Google accounts |
| Android signing | Upload keystore from a base64 secret, decoded at build time | Committing the keystore or passwords |
Fastfile
default_platform(:ios)
platform :ios do
before_all do
setup_ci if ENV["CI"] # temporary keychain on CI runners
end
private_lane :asc_key do
app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_P8_BASE64"],
is_key_content_base64: true
)
end
lane :test do
run_tests(scheme: "App", devices: ["iPhone 16"], result_bundle: true)
end
lane :beta do
api_key = asc_key
match(type: "appstore", readonly: is_ci, api_key: api_key)
increment_build_number(
xcodeproj: "App.xcodeproj",
build_number: latest_testflight_build_number(api_key: api_key) + 1
)
build_app(scheme: "App", export_method: "app-store", output_directory: "build")
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
end
end
platform :android do
lane :test do
gradle(task: "testReleaseUnitTest")
end
lane :internal do
code = google_play_track_version_codes(track: "internal").max.to_i + 1
gradle(
task: "bundle", build_type: "Release",
properties: { "versionCode" => code }
)
upload_to_play_store(track: "internal", aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH])
end
lane :promote_production do |options|
upload_to_play_store(
track: "internal", track_promote_to: "production",
rollout: options.fetch(:rollout, "0.1"),
skip_upload_aab: true, skip_upload_metadata: true, skip_upload_images: true, skip_upload_screenshots: true
)
end
end
Notes:
- The Gradle
versionCodeproperty only takes effect ifbuild.gradle(.kts)reads it (for example fromproject.findProperty("versionCode")); check before relying on it. - Set
json_key_dataorjson_keyfor Play in theAppfilefrom an environment variable. Verify the key once withbundle exec fastlane run validate_play_store_json_key json_key:<path>. - Production lanes take explicit parameters and run only from a manual CI trigger with an approval step; never chain them after beta automatically.
match setup (once, on a developer machine)
bundle exec fastlane match init # choose storage, writes Matchfile
bundle exec fastlane match appstore --app_identifier "com.example.app,com.example.app.widget"
bundle exec fastlane match development
- List every bundle ID that ships (extensions, widgets, App Clips) or export fails later.
MATCH_PASSWORDencrypts the repo; store it in the CI secret store and a team vault.- CI uses
readonly: trueso it can never create or revoke certificates.
Android signing
- name: Decode keystore
run: echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/upload.jks"
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
Pass the path and passwords to Gradle as environment variables read by the
signingConfigs block; do not write them to gradle.properties in the repo. With Play
App Signing, this is the upload key; losing it is recoverable through Play Console, losing
an app signing key without Play App Signing is not.
GitHub Actions
jobs:
ios-beta:
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- run: sudo xcode-select -s /Applications/Xcode_16.4.app # pin; check the runner image's installed list
- 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_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }}
- Pin the Xcode version explicitly; runner images change their default.
- Cache Ruby gems (
bundler-cache), CocoaPods or SPM packages and the Gradle cache; do not cache DerivedData across Xcode versions. - Use
concurrencygroups so two beta runs do not race for the same build number. - macOS minutes cost several times Linux minutes; run Android and unit tests on Linux.
Gotchas
- “No signing certificate found” on CI with a working local build usually means match
ran without
setup_ci, so the cert went into a keychain the build cannot read. - Automatic signing in Xcode and match fight each other; set manual signing for Release
with the
match AppStore <bundle id>profiles, per target. latest_testflight_build_numberis per version string; bumping the marketing version resets it. Combine with a CI run number if you need global monotonic numbers.- Apple’s API keys with the Admin role can do anything; create an App Manager key for CI.
- Uploads need outbound network to Apple and Google; sandboxed local runs will fail at that step, so test lanes with uploads disabled first.
Verification
bundle exec fastlane ios testandbundle exec fastlane android testpass locally.- A CI run on a fresh runner produces a TestFlight build and a Play internal release with incremented build numbers.
- Rerunning the same job does not fail on duplicate build numbers.
- Logs contain no secret values (search the log for the first characters of each).
Output
**Lanes:** <platform:lane -> what it does, trigger>
**Signing:** iOS match storage + readonly | Android keystore source
**Secrets required:** <names only>
**First run:** <CI link or local result> -> TestFlight build / Play internal version What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Mobile CI with fastlane"
short_description: "Signed builds from a clean runner, every time"
default_prompt: "$mobile-ci-fastlane Set up fastlane and GitHub Actions to push every main build to TestFlight and Play internal" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.