skip to content

For a scooter-sharing Flutter app, how would you set up Codemagic so a version tag signs both platforms and ships builds to Google Play internal testing and TestFlight?

level: seniorimportance: should knowfreq 34%

answer

  1. two workflows, one tag pattern
  2. android_signing exports CM_KEYSTORE_PATH
  3. build number from the store
  4. google_play track internal
  5. auth: integration, post-processing

basics

~20 s

Use two tag-triggered workflows sharing anchors: Android with android_signing, a store-derived build number and google_play publishing to the internal track; iOS with ios_signing app_store, use-profiles and app_store_connect publishing via the API-key integration. The first Play upload is manual; App Store Connect needs an app record.

solid answer

~40 s

I would add an Apple Developer Portal integration (API key with App Manager access), upload the Android keystore to Code signing identities, and store the Play service-account JSON as a Secret in a `google_credentials` group. Two workflows share a tag trigger through an anchor. The Android one lists `android_signing: [scooter_keystore]`, so Codemagic exports `CM_KEYSTORE_PATH`, `CM_KEYSTORE_PASSWORD`, `CM_KEY_ALIAS` and `CM_KEY_PASSWORD` for the Gradle signing config; it sets the build number from `google-play get-latest-build-number` plus one and publishes with `google_play: {credentials, track: internal}`. The iOS one uses `ios_signing` with `app_store`, `xcode-project use-profiles`, a build number from `app-store-connect get-latest-testflight-build-number`, and `app_store_connect: {auth: integration, beta_groups}`. The first Play upload and the App Store Connect app record are manual; TestFlight actions run later in post-processing.

code

yaml · 67 lines
yaml
definitions:
  release_trigger: &release_trigger
    events:
      - tag
    tag_patterns:
      - pattern: 'v+([0-9]).+([0-9]).+([0-9])'
        include: true

workflows:
  scooter-android-internal:
    name: Scooter Android to Play internal
    instance_type: mac_mini_m2
    triggering: *release_trigger
    environment:
      flutter: stable
      android_signing:
        - scooter_keystore
      groups:
        - google_credentials
      vars:
        PACKAGE_NAME: "com.example.scooter"
    scripts:
      - name: Get packages
        script: flutter pub get
      - name: Build signed AAB
        script: |
          LATEST=$(google-play get-latest-build-number --package-name "$PACKAGE_NAME")
          if [ -z "$LATEST" ]; then NEXT=$BUILD_NUMBER; else NEXT=$((LATEST + 1)); fi
          flutter build appbundle --release --build-number=$NEXT
    artifacts:
      - build/**/outputs/**/*.aab
    publishing:
      google_play:
        credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS
        track: internal

  scooter-ios-testflight:
    name: Scooter iOS to TestFlight
    instance_type: mac_mini_m2
    triggering: *release_trigger
    integrations:
      app_store_connect: scooter_asc_key
    environment:
      flutter: stable
      xcode: latest
      ios_signing:
        distribution_type: app_store
        bundle_identifier: com.example.scooter
      vars:
        APP_STORE_APPLE_ID: 1555555551
    scripts:
      - name: Apply provisioning profiles
        script: xcode-project use-profiles
      - name: Get packages
        script: flutter pub get
      - name: Build signed IPA
        script: |
          LATEST=$(app-store-connect get-latest-testflight-build-number "$APP_STORE_APPLE_ID")
          flutter build ipa --release --build-number=$((LATEST + 1)) \
            --export-options-plist=/Users/builder/export_options.plist
    artifacts:
      - build/ios/ipa/*.ipa
    publishing:
      app_store_connect:
        auth: integration
        beta_groups:
          - Fleet operators

go deeper

for a junior

Recall that each platform needs its own signing and store credentials, and that Codemagic can upload to Play and App Store Connect.

for a middle

Explain android_signing's exported variables, ios_signing's app_store type and the google_play and app_store_connect publishing keys.

for a senior

Design two tag-triggered workflows, store-derived build numbers, manual first uploads and a failure checklist, and justify the split.

for a principal

Weigh one vendor-managed pipeline against portable scripts, and decide who owns release credentials and the tag that ships.

## The goal A scooter-sharing app ships often: fleet operators test on Android through Google Play's **internal** track and on iOS through **TestFlight**. The release should be one action, pushing a tag such as `v3.2.0`, and Codemagic should build, sign, number and upload both platforms. ## One-time setup outside the file 1. **Apple**: create an App Store Connect API key with App Manager access, add it under Team integrations, Developer Portal, and use it in Code signing identities to generate an Apple Distribution certificate and fetch the App Store profile for `com.example.scooter`. 2. **Android**: upload the release keystore under Code signing identities, Android keystores, with keystore password, key alias, key password and a reference name. Codemagic will **not** let you download it again, so a backup must exist elsewhere. 3. **Google Play**: create a service account, invite it in the Play Console with release permissions for the app, and paste its JSON key into a Secret variable `GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS` in a `google_credentials` group. 4. **First uploads**: Google Play requires the **first** version to be uploaded manually; App Store Connect needs an **app record** before automation, and Codemagic recommends uploading the first version by hand. ## One workflow or two | Choice | Gains | Costs | |---|---|---| | Two workflows, same tag | A failure on one store does not block the other; each re-runs alone; each has its own cache | Some duplicated YAML, reduced with anchors | | One workflow, both builds | One run, one log | One failure stops both; the whole run needs a macOS machine | Two workflows sharing a `definitions` anchor for triggering is usually the better trade. ## Android workflow - `environment.android_signing: [scooter_keystore]` installs the keystore and exports `CM_KEYSTORE_PATH`, `CM_KEYSTORE_PASSWORD`, `CM_KEY_ALIAS` and `CM_KEY_PASSWORD`. The Gradle release signing config reads them; that Gradle wiring is part of the Flutter release setup rather than Codemagic. - Every Play upload needs a higher version code, so compute it: `google-play get-latest-build-number --package-name "$PACKAGE_NAME"`, add one, and pass it to `flutter build appbundle --build-number`. Codemagic's built-in `BUILD_NUMBER` is a fallback when the store returns nothing. - `publishing.google_play` with `credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS` and `track: internal`. Optional keys include `submit_as_draft`, `rollout_fraction` (not together with draft) and `release_promotion`. ## iOS workflow - `integrations.app_store_connect: scooter_asc_key` and `ios_signing` with `distribution_type: app_store` and the bundle ID. - `xcode-project use-profiles`, then `flutter build ipa` with `--build-number` from `app-store-connect get-latest-testflight-build-number "$APP_STORE_APPLE_ID"` plus one. - `publishing.app_store_connect` with `auth: integration` and `beta_groups`. `submit_to_testflight: true` sends the build to beta review, which external testers need; a build only for internal testers can instead be marked internal-only through `use-profiles` export options. ## Beyond the internal track The same publishing blocks grow with the release process, which is why interviewers ask how you would extend them: - `google_play.track` also accepts `alpha`, `beta`, `production` or a custom closed-testing track name. - `rollout_fraction` (between 0 and 1) releases to part of the audience; it cannot be combined with `submit_as_draft`. - `release_promotion` promotes the release to a second track in the same run. - `app_store_connect.submit_to_app_store: true` sends the build to App Store review; `release_type` accepts `MANUAL` (the default), `AFTER_APPROVAL` or `SCHEDULED` with an `earliest_release_date`. - `cancel_previous_submissions` and `expire_build_submitted_for_review` clear an older build that is still waiting, so a hotfix can take its place. Keeping these off in the internal-testing workflows, and adding a separate production workflow on a different tag pattern, keeps a test tag away from customers. ## After the upload: post-processing TestFlight submission, beta groups and release notes happen in **post-processing** (Magic Actions) after the workflow ends, so the macOS machine is not held while Apple processes the build and no build minutes are used. If the build does not appear in App Store Connect within 15 minutes the step times out; the overall limit is 120 minutes. Codemagic sends no status updates for this step, so check the build log or Apple's email. ## Failure checklist - Play rejects the upload: version code not higher, or the first version was never uploaded manually. - Apple rejects or never processes: wrong `distribution_type`, key without App Manager, missing app record. - A custom publishing script uploads a stale file: publishing scripts run even when the build failed.

  • Why does Codemagic run TestFlight submission after the workflow finishes?
    Apple processes an uploaded build asynchronously. Codemagic moves `submit_to_testflight`, `beta_groups` and release notes into post-processing so the macOS machine is released and no build minutes are spent waiting. The step times out if the build is not found within 15 minutes, and after 120 minutes overall.
  • What happens if the Google Play build number query returns nothing on the first release?
    An empty result would make `$((LATEST + 1))` evaluate to 1. Codemagic's documented pattern checks for an empty value and falls back to the built-in `BUILD_NUMBER`, or exits. On a truly first release the upload itself must be manual anyway.
  • Where do the CM_KEYSTORE_* variables come from, and when must you name them yourself?
    Listing one keystore reference under `android_signing` makes Codemagic export `CM_KEYSTORE_PATH`, `CM_KEYSTORE_PASSWORD`, `CM_KEY_ALIAS` and `CM_KEY_PASSWORD`. With several keystores you must give each entry explicit variable names, such as `keystore_environment_variable`, so they do not collide.

saying these in an interview costs you the question

  • Codemagic can publish the very first Play version of an app automatically
  • Reusing Codemagic's BUILD_NUMBER is always enough for store uploads
  • A keystore uploaded to Codemagic can be downloaded later as a backup
  • submit_to_testflight runs inside the build and uses its build minutes
  • TestFlight uploads need an Apple ID password stored in Codemagic