Irbisa · Cheatsheet

Mobile CI/CD (Cross-platform)

Fastlane, GitHub Actions, signing, TestFlight, Play Internal, build flavors

Senior Developer · 19 questions

  1. 01

    What is the difference between continuous integration, continuous delivery and continuous deployment, and why does mobile rarely do the last one?

    CI builds and tests every merged change; continuous delivery keeps every green build releasable behind one manual step; continuous deployment ships every green build to users automatically.

  2. 02

    What does a CI/CD pipeline for a mobile app look like, from lint to store upload?

    A mobile pipeline is really two: a fast lane that must finish while the reviewer is still looking at the PR, and a slow lane that may take an hour.

    # .github/workflows/android.yml: fast lane on PRs, emulator tests only on mainname: Android CIon:  pull_request: { branches: [main] }  push: { branches: [main] }jobs:…
  3. 03

    What is Fastlane and what does it automate?

    Fastlane is a Ruby toolkit that turns the release ritual (build, sign, upload symbols, ship to a test track) into named lanes you run identically on a laptop and in CI.

    # fastlane/Fastfileplatform :android do  lane :internal do    gradle(task: "clean bundleRelease")    upload_to_play_store(track: "internal", release_status: "draft")   # takes the AAB gradle just built  end…
  4. 04

    The release build works on every laptop in the team and fails on a clean CI checkout. What do you check first?

    The runner is almost always right: something the build needs is not in the repository, or has a different name once the filesystem stops ignoring case.

    #!/usr/bin/env bash# Reproduce the CI environment locally: a clean clone, nothing from your working copy.set -euo pipefail TMP="$(mktemp -d)"git clone --depth 1 "file://$PWD" "$TMP/app"…
  5. 05

    How do you sign an Android app, and what is Play App Signing?

    You sign the AAB with your own upload key; with Play App Signing, Google keeps the app signing key and re-signs the APKs it delivers, so the key that devices verify never sits on a laptop or a CI runner.

    // app/build.gradle.kts: release signing, entirely from environment variablesandroid {  signingConfigs {    create("release") {      storeFile = rootProject.file(System.getenv("KEYSTORE_PATH") ?: "release.keystore")      storePassword = System.getenv("KEYSTORE_PASSWORD")…
  6. 06

    How does the pipeline authenticate to Google Play and App Store Connect without anyone's personal account?

    Each store gets a machine credential owned by the team rather than a person: a Google Cloud service account invited into the Play Console, and an App Store Connect team API key.

  7. 07

    Google Sign-In works in the APK your CI built and fails for everyone who installed the app from Play. What changed between the two?

    Play re-signs your upload with the app signing key, so the certificate fingerprint on a user's device is Google's and not your upload key's, and every integration keyed to that fingerprint has to know both.

    #!/usr/bin/env bashset -euo pipefail # What you signed with (the upload key), NOT what users verify.keytool -list -v -alias upload -keystore upload.jks -storepass "$KEYSTORE_PASSWORD" | grep -E 'SHA1|SHA256'…
  8. 08

    How does a headless CI runner get and use iOS signing certificates?

    The runner fetches the team's certificates and provisioning profiles read-only with fastlane match and imports them into a temporary unlocked keychain; signing then works only if the certificate, profile, entitlements and bundle id all agree.

    # fastlane/Fastfile: iOS signing on a CI runnerplatform :ios do  lane :beta do    setup_ci if is_ci            # creates and unlocks a temporary keychain     api_key = app_store_connect_api_key(…
  9. 09

    What does each extra build flavor cost your CI, and how do you keep it from multiplying the pipeline?

    Android uses productFlavors, iOS uses xcconfig-backed build configurations selected by schemes, and Flutter's --flavor drives both; each added flavor multiplies what CI builds, signs and tests.

    # .github/workflows/build.yml: which variants each lane buildson:  pull_request:  push:    branches: [main]jobs:…
  10. 10

    Who should set versionCode and CFBundleVersion in a pipeline, and where does the next value come from?

    The build number is pipeline state, not source a human edits: CI derives a value that only ever goes up.

    # fastlane/Fastfile: the store is the source of truth for the next numberplatform :ios do  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"])     # Ask App Store Connect rather than trusting whatever is committed:…
  11. 11

    Nobody touched the code and Monday's build fails. What drifts on its own in a mobile pipeline?

    Almost every unexplained CI failure comes from something nobody pinned: a runner image, a toolchain, or a dependency that resolved to a version which did not exist last week.

    # Pin the image, then pin the toolchain inside itjobs:  ios:    runs-on: macos-15          # not macos-latest: the default Xcode moves under you    steps:      - uses: actions/checkout@v6…
  12. 12

    The first crash report from the release build is a wall of hex addresses. What should the pipeline have done at build time?

    Upload the dSYMs, the R8 mapping file and native debug symbols for that exact build from the job that produced the binary, before it ships; a rebuild does not recreate them.

    # fastlane/Fastfile: symbols leave in the same lane that built the binaryplatform :ios do  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"])    build_app(scheme: "MyApp", export_method: "app-store-connect")   # also zips the dSYMs    upload_to_testflight(api_key: api_key)…
  13. 13

    How do you keep a UI test suite in CI trustworthy when it is flaky?

    Make a red build reliably mean a broken app: remove the cause of each flake, retry a single test rather than the job, and quarantine repeat offenders with an owner.

    // Make the device a declared build input, so every run uses the same imageandroid {  testOptions {    managedDevices {      localDevices {        create("pixel8api36") {…
  14. 14

    QA signed off on build 812 and the release job then rebuilt from the same tag. Why is that not the same build?

    A rebuild is a new binary (a different runner image, a dependency that resolved differently, a new build number), so what ships is not what anyone tested.

    jobs:  build:    runs-on: macos-15    steps:      - uses: actions/checkout@v6      - run: bundle exec fastlane ios build_rc build_number:$GITHUB_RUN_NUMBER…
  15. 15

    Your team ships every two weeks, review takes days, and a bad build cannot be recalled. How do you structure branches and releases around that?

    Trunk-based development with unfinished work behind flags, a release branch cut on the train's schedule that receives only cherry-picked fixes, and store builds made from tags.

  16. 16

    The PR pipeline takes forty minutes and the macOS runners cost more than the laptops. Where do you get the time and the money back?

    Cancel superseded runs, skip jobs the change does not touch, build debug on PRs, cache by lockfile, shard the tests and move everything that does not need Xcode to Linux; a build scan or Xcode's build timing summary says which pays first.

    # .github/workflows/pr.yml: the levers that actually move the numbername: PRon:  pull_request:    paths: ['android/**', 'shared/**', 'gradle/**']   # a README change builds nothingconcurrency:…
  17. 17

    A pull request arrives from a stranger's fork. What stops it from walking away with your signing key?

    Secrets have to be unreachable from any code a stranger can change: fork pull requests run without them, and release jobs sit behind a human approval.

    # A fork PR builds with no secrets present. Keep it that way.name: CIon: { pull_request: , push: { branches: [main] } }permissions:  contents: read           # least privilege; the default token grant is widerjobs:…
  18. 18

    Someone left the company with a copy of the release keystore and the iOS distribution certificate on their laptop. What can you actually rotate?

    On iOS you can revoke and reissue everything, because Apple re-signs what users install from the App Store.

  19. 19

    Marketing wants the feature live on both platforms Tuesday morning; Android is approved and iOS is still in review. How do you ship?

    Separate shipping from launching: get both binaries to users ahead of the date with the feature dark, then turn it on from the server once both are actually out.