First Release Pipeline

Build, sign, and distribute a mobile app with Codemagic—internal testing through App Store and Play store release

Codemagic is a cloud-based CI/CD service for building, signing, and distributing mobile apps. This quick start uses tabs so you can follow Flutter, React Native, native iOS, or native Android. The flow is the same in five steps:

  1. Unsigned build — Run a workflow without signing to confirm the project compiles in CI.
  2. Signing credentials — Create or gather Apple and Android signing inputs, then upload them in Codemagic.
  3. Signed build — Add signing to YAML and produce signed .ipa, .aab, or .apk files.
  4. Internal distribution — Publish to TestFlight (internal testers) and Google Play internal testing.
  5. Store release — Use separate workflows to submit to the App Store and Google Play production.

You do not need to modify your Xcode or Gradle project to run unsigned builds. Signing requires adding credentials, but does not require restructuring your project.

Step 4 and Step 5 are optional publishing paths. After that, Next steps: from signed build to full CI/CD lists what most teams add next; Further capabilities links stack guides and reference.

Connect your repo or use a sample repository

To connect your repo, authorize the relevant connection then choose the repo you want to use.

To try Codemagic without wiring your own app first, clone one of these sample projects:

More samples are listed on Codemagic sample projects.

Step 1: A basic unsigned build

Put codemagic.yaml at the repository root, commit it, open your stack’s tab, copy the unsigned workflow, and replace placeholders (workspace, scheme, package name, and so on) for your app.

Each workflow lives under the top-level workflows: key. The examples below start with environment, scripts, and artifacts; add publishing: in Steps 4–5 when you distribute builds (Step 5 normally uses extra workflow entries—see there).

workflows:
  my-workflow:
    name: My workflow name
    instance_type: mac_mini_m2
    max_build_duration: 60

    environment:   # variables, groups, tool versions, signing references
    triggering:    # branches, PRs, tags, webhooks
    scripts:       # build and test steps
    artifacts:     # files to keep from the build
    publishing:    # distribution and notifications
    cache:         # dependency caches

These iOS examples use xcode: latest, and the corresponding environment . Pin a specific major.minor version only if your project requires it.

Two workflows: debug iOS without code signing (--no-codesign), and debug Android APK.
workflows:
  flutter-ios-unsigned:
    name: Flutter iOS (unsigned debug)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      flutter: stable
      xcode: latest
      cocoapods: default
    scripts:
      - name: Get Flutter packages
        script: flutter pub get
      - name: Install CocoaPods dependencies
        script: find . -name "Podfile" -execdir pod install \;
      - name: Build iOS debug without code signing
        script: flutter build ios --debug --no-codesign
    artifacts:
      - build/ios/iphoneos/**/*.app
      - /tmp/xcodebuild_logs/*.log

  flutter-android-debug:
    name: Flutter Android (debug APK)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      flutter: stable
    scripts:
      - name: Set up local.properties
        script: echo "flutter.sdk=$HOME/programs/flutter" > "$CM_BUILD_DIR/android/local.properties"
      - name: Get Flutter packages
        script: flutter pub get
      - name: Build Android debug APK
        script: flutter build apk --debug
    artifacts:
      - build/app/outputs/flutter-apk/*.apk
Two workflows: Android assembleDebug and unsigned iOS via xcodebuild with CODE_SIGNING_ALLOWED=NO. Adjust XCODE_WORKSPACE, XCODE_SCHEME, and Node version to match your project.
workflows:
  rn-android-debug:
    name: React Native Android (debug)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      node: v22.11.0
    scripts:
      - name: Install npm dependencies
        script: npm ci
      - name: Set Android SDK location
        script: echo "sdk.dir=$ANDROID_SDK_ROOT" > "$CM_BUILD_DIR/android/local.properties"
      - name: Build Android debug
        script: |
          cd android
          ./gradlew assembleDebug          
    artifacts:
      - android/app/build/outputs/**/*.apk

  rn-ios-unsigned:
    name: React Native iOS (unsigned debug)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      node: v22.11.0
      xcode: latest
      cocoapods: default
      vars:
        XCODE_WORKSPACE: "YourApp.xcworkspace"
        XCODE_SCHEME: "YourApp"
    scripts:
      - name: Install npm dependencies
        script: npm ci
      - name: Install CocoaPods dependencies
        script: |
          cd ios && pod install          
      - name: Build iOS without code signing
        script: |
          xcodebuild \
            -workspace "$CM_BUILD_DIR/ios/$XCODE_WORKSPACE" \
            -scheme "$XCODE_SCHEME" \
            -configuration Debug \
            -destination 'generic/platform=iOS' \
            CODE_SIGNING_ALLOWED=NO \
            build          
    artifacts:
      - $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.app
      - /tmp/xcodebuild_logs/*.log
Using Expo without prebuild
On CI you need ios and android folders. If you do not commit them, run npx expo prebuild during the build (and align applicationId / bundle identifier with app.json). Full steps and YAML snippets are in Using Expo without prebuild and Setting up the Android package name and iOS bundle identifier.
Assumes the Xcode workspace lives at the repository root (as in iOS native apps). If your workspace is only under ios/, use cd ios && pod install and point -workspace to $CM_BUILD_DIR/ios/YourApp.xcworkspace.
workflows:
  ios-native-unsigned:
    name: Native iOS (unsigned debug)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      xcode: latest
      cocoapods: default
      vars:
        XCODE_WORKSPACE: "YourApp.xcworkspace"
        XCODE_SCHEME: "YourApp"
    scripts:
      - name: Install CocoaPods dependencies
        script: pod install
      - name: Build iOS without code signing
        script: |
          xcodebuild \
            -workspace "$CM_BUILD_DIR/$XCODE_WORKSPACE" \
            -scheme "$XCODE_SCHEME" \
            -configuration Debug \
            -destination 'generic/platform=iOS' \
            CODE_SIGNING_ALLOWED=NO \
            build          
    artifacts:
      - $HOME/Library/Developer/Xcode/DerivedData/**/Build/**/*.app
      - /tmp/xcodebuild_logs/*.log
If you use a .xcodeproj only, swap -workspace for -project YourApp.xcodeproj in the xcodebuild command (same pattern as iOS native apps).
Assumes the Gradle project is at the repository root (as in Android native apps). If the project is in a subfolder, run Gradle from that directory and adjust local.properties paths.
workflows:
  android-native-debug:
    name: Native Android (debug)
    max_build_duration: 120
    instance_type: mac_mini_m2
    scripts:
      - name: Set Android SDK location
        script: echo "sdk.dir=$ANDROID_SDK_ROOT" > "$CM_BUILD_DIR/local.properties"
      - name: Build Android debug
        script: ./gradlew assembleDebug
    artifacts:
      - app/build/outputs/**/*.apk

Commit codemagic.yaml to your repository and push. In Codemagic, click on Start new build to run it.

If an unsigned workflow finishes successfully, your Codemagic setup is working and you can move on to signing.

If a build fails, typical causes are YAML at the wrong path, local.properties next to the wrong build.gradle, or iOS workspace/scheme/paths—see Common issues, Common iOS issues, and Common Android issues.

Step 2: Preparing for signing

Add the credentials for App Store or Google Play. The iOS and Android tabs below describe what to upload; Flutter and React Native use the same Apple and Android files as native apps when your bundle ID and application ID match.

Details and troubleshooting: Signing iOS apps, Signing Android apps; walkthroughs: iOS native, Android native.

Set this up once per platform (iOS and/or Android). You will reference the same uploaded files from your signed workflow in the next section, regardless of framework.

Requirements: Active Apple Developer Program membership.

  1. App Store Connect API key — Create in App Store Connect under Users and Access → Integrations → App Store Connect API; download the .p8 (once). Note Issuer ID and Key ID. Upload in Codemagic under Team integrations → Developer Portal → Manage keys.
  2. Signing files for your bundle IDApple Distribution certificate (e.g. .p12) and an App Store provisioning profile (.mobileprovision). Add them under Team settings → codemagic.yaml settings → Code signing identities (iOS certificates / iOS provisioning profiles), or use Fetch after the API key is saved.

Minimal signing block in codemagic.yaml (use your key name and bundle ID). Run xcode-project use-profiles in scripts before the IPA build step (see your stack’s signed YAML in Step 3 below).

integrations:
  app_store_connect: YOUR_API_KEY_NAME

environment:
  ios_signing:
    distribution_type: app_store
    bundle_identifier: com.example.app

Expected artifact: signed .ipa (artifact paths match your tab’s signed example below).

More: Signing iOS apps.

  1. Release keystore — Generate locally with Java keytool, or use an existing upload key:
keytool -genkey -v -keystore codemagic.keystore -storetype JKS \
  -keyalg RSA -keysize 2048 -validity 10000 -alias codemagic
  1. Upload the keystore under Team settings → codemagic.yaml settings → Code signing identities → Android keystores. Set keystore password, key alias, key password, and a reference name you will use in YAML.

  2. android_signing in codemagic.yaml — Codemagic injects the keystore on the build machine and sets CM_KEYSTORE_PATH, CM_KEYSTORE_PASSWORD, CM_KEY_ALIAS, and CM_KEY_PASSWORD:

environment:
  android_signing:
    - your_keystore_reference_name
  1. Gradle release signing (required) — Uploading the keystore and listing android_signing is not enough: release must use those variables or you still get unsigned release outputs. Add a signingConfigs.release block in android/app/build.gradle (Groovy) that reads the CM_* env vars when Codemagic sets CI=true, and point buildTypes.release at it:
android {
    // ...
    signingConfigs {
        release {
            if (System.getenv()["CI"]) {
                storeFile file(System.getenv()["CM_KEYSTORE_PATH"])
                storePassword System.getenv()["CM_KEYSTORE_PASSWORD"]
                keyAlias System.getenv()["CM_KEY_ALIAS"]
                keyPassword System.getenv()["CM_KEY_PASSWORD"]
            }
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
    }
}

For a complete android { } example with an else branch (so local ./gradlew assembleRelease still works via key.properties), see Signing Android apps using Gradle. Kotlin DSL (.kts) projects need the same wiring in build.gradle.kts — see Signing Android apps.

Expected artifact: .aab or .apk from ./gradlew bundleRelease / assembleRelease (see your tab’s signed example below).

More: Signing Android apps.

Step 3: Sign your build

Use the workflow in your stack’s tab after the identities above exist in Team settingsCode signing identities (and your App Store Connect integration key is saved under Team integrations when you build iOS). Replace placeholders such as <App Store Connect API key name>, keystore_reference, PACKAGE_NAME, bundle_identifier, workspace, and scheme names.

Two workflows: Android (signed AAB) and iOS (signed IPA). Each needs the matching credentials from Step 2 above. For more Flutter options, see Flutter apps.
workflows:
  flutter-android-signed:
    name: Flutter Android (signed release)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      android_signing:
        - keystore_reference
      vars:
        PACKAGE_NAME: "com.example.yourapp"
      flutter: stable
    scripts:
      - name: Set up local.properties
        script: echo "flutter.sdk=$HOME/programs/flutter" > "$CM_BUILD_DIR/android/local.properties"
      - name: Get Flutter packages
        script: flutter pub get
      - name: Build Android App Bundle
        script: flutter build appbundle --release
    artifacts:
      - build/**/outputs/**/*.aab

  flutter-ios-signed:
    name: Flutter iOS (signed release)
    max_build_duration: 120
    instance_type: mac_mini_m2
    integrations:
      app_store_connect: <App Store Connect API key name>
    environment:
      ios_signing:
        distribution_type: app_store
        bundle_identifier: com.example.yourapp
      flutter: stable
      xcode: latest
      cocoapods: default
    scripts:
      - name: Set up code signing settings on Xcode project
        script: xcode-project use-profiles
      - name: Get Flutter packages
        script: flutter pub get
      - name: Install CocoaPods dependencies
        script: find . -name "Podfile" -execdir pod install \;
      - name: Flutter build ipa
        script: |
          flutter build ipa --release \
            --build-name=1.0.0 \
            --build-number=1 \
            --export-options-plist=/Users/builder/export_options.plist          
    artifacts:
      - build/ios/ipa/*.ipa
      - /tmp/xcodebuild_logs/*.log
Two workflows: Android (AAB) and iOS (IPA), using the same android_signing / ios_signing settings as in Step 2 above. Adjust paths if your android/ or ios/ layout differs.
workflows:
  rn-android-signed:
    name: React Native Android (signed release)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      android_signing:
        - keystore_reference
      vars:
        PACKAGE_NAME: "com.example.yourapp"
      node: v22.11.0
    scripts:
      - name: Install npm dependencies
        script: npm ci
      - name: Set Android SDK location
        script: echo "sdk.dir=$ANDROID_SDK_ROOT" > "$CM_BUILD_DIR/android/local.properties"
      - name: Build Android release bundle
        script: |
          cd android
          ./gradlew bundleRelease          
    artifacts:
      - android/app/build/outputs/**/*.aab

  rn-ios-signed:
    name: React Native iOS (signed release)
    max_build_duration: 120
    instance_type: mac_mini_m2
    integrations:
      app_store_connect: <App Store Connect API key name>
    environment:
      ios_signing:
        distribution_type: app_store
        bundle_identifier: com.example.yourapp
      vars:
        XCODE_WORKSPACE: "YourApp.xcworkspace"
        XCODE_SCHEME: "YourApp"
        APP_STORE_APPLE_ID: 1234567890
      node: v22.11.0
      xcode: latest
      cocoapods: default
    scripts:
      - name: Install npm dependencies
        script: npm ci
      - name: Install CocoaPods dependencies
        script: |
          cd ios && pod install          
      - name: Set up code signing settings on Xcode project
        script: xcode-project use-profiles
      - name: Build ipa for distribution
        script: |
          xcode-project build-ipa \
            --workspace "$CM_BUILD_DIR/ios/$XCODE_WORKSPACE" \
            --scheme "$XCODE_SCHEME"          
    artifacts:
      - build/ios/ipa/*.ipa
      - /tmp/xcodebuild_logs/*.log
Matches the pattern in iOS native apps: ios_signing, xcode-project use-profiles, and xcode-project build-ipa.
workflows:
  ios-native-signed:
    name: Native iOS (signed release)
    max_build_duration: 120
    instance_type: mac_mini_m2
    integrations:
      app_store_connect: <App Store Connect API key name>
    environment:
      ios_signing:
        distribution_type: app_store
        bundle_identifier: com.example.yourapp
      vars:
        XCODE_WORKSPACE: "YourApp.xcworkspace"
        XCODE_SCHEME: "YourApp"
      xcode: latest
      cocoapods: default
    scripts:
      - name: Install CocoaPods dependencies
        script: pod install
      - name: Set up code signing settings on Xcode project
        script: xcode-project use-profiles
      - name: Build ipa for distribution
        script: |
          xcode-project build-ipa \
            --workspace "$CM_BUILD_DIR/$XCODE_WORKSPACE" \
            --scheme "$XCODE_SCHEME"          
    artifacts:
      - build/ios/ipa/*.ipa
      - /tmp/xcodebuild_logs/*.log
Matches Android native apps: android_signing and bundleRelease.
workflows:
  android-native-signed:
    name: Native Android (signed release)
    max_build_duration: 120
    instance_type: mac_mini_m2
    environment:
      android_signing:
        - keystore_reference
      vars:
        PACKAGE_NAME: "com.example.yourapp"
    scripts:
      - name: Set Android SDK location
        script: echo "sdk.dir=$ANDROID_SDK_ROOT" > "$CM_BUILD_DIR/local.properties"
      - name: Build Android release bundle
        script: ./gradlew bundleRelease
    artifacts:
      - app/build/outputs/**/*.aab

Common issues (signed builds)

  • iOS: Run xcode-project use-profiles before xcode-project build-ipa or flutter build ipa. Check distribution_type and bundle ID match your provisioning profile; Flutter often needs --export-options-plist=/Users/builder/export_options.plist. More detail: Common iOS issues.
  • Android: The keystore_reference name in YAML must match Code signing identities; release builds need signingConfigs wired to CM_KEYSTORE_* / CM_KEY_* when CI=true (see the Gradle section above). See Common Android issues.
  • Google Play: Upload and signing problems are covered in Common Google Play errors.

If the failure is unclear, open the failing step in the build log and search these guides for the error text.

Step 4: Distribute to internal testers

Optional: once Step 3 produces signed artifacts, add publishing: to each workflow.

To run distribution workflows when you push code, set up Webhooks from your Git host and define triggering: in codemagic.yaml as in Starting builds automatically. You can still start any workflow manually with Codemagic UI.

workflows:
  ios-testflight-internal:
    name: iOS TestFlight (internal)
    # ...
    publishing:
      app_store_connect:
        auth: integration
        submit_to_testflight: true
        beta_groups:
          - Internal testers

  android-play-internal:
    name: Android Play internal testing
    # ...
    publishing:
      google_play:
        credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS
        track: internal

The tabs below spell out each publishing block and link to the full guides.

Reuse the app_store_connect integration from signing. auth: integration uploads the .ipa from artifacts. submit_to_testflight and beta_groups run in post-processing after the main workflow finishes. Replace group names with your Internal testing groups in App Store Connect.

Full options (release scheduling, phased release, and so on): App Store Connect publishing.

    publishing:
      app_store_connect:
        auth: integration
        submit_to_testflight: true
        beta_groups:
          - Internal testers

Store the Play service account JSON as a secret environment variable (for example GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS) and attach the variable group to the workflow environment if you use groups. track: internal targets internal testing. For a new listing, upload the first .aab once in Play Console; later uploads need a higher version code—see Automatic build versioning.

Details and optional fields: Google Play publishing.

    publishing:
      google_play:
        credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS
        track: internal

For Firebase App Distribution, GitHub Releases, and other targets, see Publishing.

Step 5: Publish to the stores

Create separate workflows in the same codemagic.yaml for publishing to the App Store or Play store. Set when each workflow runs with triggering:.

Store submission has review, metadata, and rollout rules that internal testing does not; use the full guides for phased_release, staged rollouts, and Magic Actions timing.

Two sibling workflows under workflows: (production .ipa / .aab—reuse your Step 3 build configuration in place of # …):

workflows:
  ios-app-store-release:
    name: iOS App Store release
    # ...
    publishing:
      app_store_connect:
        auth: integration
        submit_to_app_store: true

  android-play-production:
    name: Android Play production
    # ...
    publishing:
      google_play:
        credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS
        track: production

submit_to_app_store: true requests App Store review in post-processing after the .ipa upload (same pattern as TestFlight actions in Step 4). You still need a valid app record, screenshots, privacy details, and so on in App Store Connect.

Details: App Store Connect publishing.

    publishing:
      app_store_connect:
        auth: integration
        submit_to_app_store: true

track: production sends the .aab to the production track. Optional rollout_fraction and promotion between tracks are in Google Play publishing.

Each upload must use a higher version code than the last one on that track—see Automatic build versioning.

    publishing:
      google_play:
        credentials: $GOOGLE_PLAY_SERVICE_ACCOUNT_CREDENTIALS
        track: production

Next steps: from signed build to full CI/CD

After optional Steps 4–5—or if you skip them—most teams go on to:

Further capabilities and next steps

Deeper guide for your stack: Flutter, React Native, iOS native, or Android native. For notifications, environment groups, and other topics, use the sidebar or search the docs.