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.

  • Mobile & Desktop
  • $mobile-ci-fastlane
  • 184 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- mobile-ci-fastlane

Installs to ~/.agents/skills/mobile-ci-fastlane/ for every project on this machine.

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

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.

~/.agents/skills/mobile-ci-fastlane/SKILL.md

--- name: mobile-ci-fastlane description: Automates iOS and Android builds and releases with fastlane in CI - Fastfile lanes for tests, beta and production, match for shared iOS certificates… license: MIT ---

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

  1. Read AGENTS.md, then fastlane/Fastfile, Appfile, Matchfile, Pluginfile, the Gemfile and existing CI workflows.
  2. Confirm fastlane runs through Bundler with a pinned version: a Gemfile containing gem "fastlane" and a committed Gemfile.lock. Always invoke bundle exec fastlane.
  3. 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.
  4. Inventory secrets the CI provider holds. Never print them, echo them into logs, or commit decoded files.
  5. 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 nuke or revoke certificates without explicit approval: it breaks every other machine’s builds.

Credentials

PlatformUseAvoid
iOS uploads and metadataApp Store Connect API key (.p8, key ID, issuer ID)Apple ID + password / 2FA sessions on CI
iOS signingmatch with a private git repo, S3 or GCS storage, readonly on CIExporting certs by hand onto runners
Android uploadsGoogle Play service account JSON with release permissions for this app onlyPersonal Google accounts
Android signingUpload keystore from a base64 secret, decoded at build timeCommitting 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 versionCode property only takes effect if build.gradle(.kts) reads it (for example from project.findProperty("versionCode")); check before relying on it.
  • Set json_key_data or json_key for Play in the Appfile from an environment variable. Verify the key once with bundle 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_PASSWORD encrypts the repo; store it in the CI secret store and a team vault.
  • CI uses readonly: true so 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 concurrency groups 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_number is 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

  1. bundle exec fastlane ios test and bundle exec fastlane android test pass locally.
  2. A CI run on a fresh runner produces a TestFlight build and a Play internal release with incremented build numbers.
  3. Rerunning the same job does not fail on duplicate build numbers.
  4. 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem