---
title: "Give QA an add-to-app build, then promote it"
description: "Hand a QA build of your host app to testers, send it patches on a track of its own, and promote each one to your users once QA signs off."
---

If your host app (Android or iOS) has a QA build that goes to a group of testers before your users get anything, Shorebird fits into it with a [track](/code-push/tracks). The QA build checks the `qa` track and the build you ship checks `stable`. You publish each patch to `qa`, and when QA signs off you promote that same patch to `stable`, without rebuilding it.

This page assumes you have already followed the [Android](/code-push/guides/add-to-app/android) or [iOS](/code-push/guides/add-to-app/ios) add-to-app guide.

## How releases and patches reach QA

* **A release** is the module build that `shorebird release aar` or `shorebird release ios-framework` produced. It reaches devices only inside a host app build, so QA sees a new release as soon as you give them a host build that contains it, and your users see it when you ship that host app to the store. Shorebird doesn’t need to hold the release back.
* **A patch** reaches every device running its release that checks the patch’s track. Publish it to `qa` and only QA builds get it. Promote it to `stable` and your users get it too.

For a patch to work in both builds, the QA build and the build you ship must contain the **same** module release: the same `aar` or `.xcframework`s from the same `release` directory. The QA build differs only in the host app: it tells the module which track to check.

Caution

Don’t make the QA build out of a different build of the module, such as a module flavor or a Dart change that points it at your QA servers. That is a separate release with its own patches, so a patch QA tested doesn’t apply to the module your users run. Keep everything that differs between QA and production in the host app, and pass it to the module at startup, the same way this page passes the track.

## Set up the module once, before you release

The module has to read its track from the host app, and that code has to be in the release. Follow [Set up the module once, before you release](/code-push/guides/add-to-app/testing-patches#set-up-the-module-once-before-you-release): add `shorebird_code_push`, set `auto_update: false`, and check for updates on the track the host app passes as `--shorebird-track=<name>`. With no track, the module checks the default track (`stable`, or the `channel:` set in `shorebird.yaml`), so the build you ship needs no change.

## Add a QA build to your host app

Give the host app a QA build that passes `--shorebird-track=qa` to the module, and keep the build you ship passing nothing. The code that starts the Flutter engine is the same as in [Build the host app locally with your track](/code-push/guides/add-to-app/testing-patches#build-the-host-app-locally-with-your-track): it reads `SHOREBIRD_TRACK` from the build and passes it to the module only when it’s set. Here the QA build sets it to `qa`.

The QA build must be a release build, because Shorebird only patches release builds. It must also keep the host app’s version: the version and build number have to match the `--release-version` you released the module with.

### Android

Add a `qa` build type that starts from `release`, sets the track, and uses the module’s release `aar`:

app/build.gradle

```
android {
    buildFeatures {
        buildConfig true
    }
    defaultConfig {
        // From "Test an add-to-app patch before publishing it": empty unless
        // you build with -PshorebirdTrack=<name>.
        buildConfigField "String", "SHOREBIRD_TRACK",
            "\"${project.findProperty('shorebirdTrack') ?: ''}\""
    }
    buildTypes {
        release {
            // ...
        }
        qa {
            initWith release
            // Makes the QA build a separate app, so it installs next to the
            // store app and you can upload it with the same version code.
            applicationIdSuffix ".qa"
            buildConfigField "String", "SHOREBIRD_TRACK", "\"qa\""
            // For library modules that have no qa build type of their own.
            matchingFallbacks = ['release']
        }
    }
}

dependencies {
    releaseImplementation 'com.example.my_flutter_module:flutter_release:1.0'
    qaImplementation 'com.example.my_flutter_module:flutter_release:1.0'
}
```

Build the QA build with `./gradlew assembleQa` (or `bundleQa`), and hand it to QA the way you do today, for example with Firebase App Distribution. The release build still has an empty `SHOREBIRD_TRACK`, so it checks the default track.

The `.qa` suffix gives the QA build its own package name. Shorebird doesn’t mind: the module’s `app_id` comes from the `aar`, and the release version from the version name and code. But anything else keyed on the package name needs the QA one registered too, for example a Firebase app in `google-services.json`, OAuth clients, and API keys. Without the suffix, the QA build replaces the store app on testers’ devices, and you can’t upload it to Google Play under the same version code as the build you ship.

If your app uses `build.gradle.kts`, the build type is:

app/build.gradle.kts

```
buildTypes {
    create("qa") {
        initWith(getByName("release"))
        applicationIdSuffix = ".qa"
        buildConfigField("String", "SHOREBIRD_TRACK", "\"qa\"")
        matchingFallbacks += listOf("release")
    }
}

dependencies {
    "qaImplementation"("com.example.my_flutter_module:flutter_release:1.0")
}
```

### iOS

1. In your project’s **Info** tab, duplicate the **Release** configuration and name the copy **QA**. The copy keeps Release’s build settings, including the Framework Search Paths to your module’s `.xcframework`s. If your app uses CocoaPods for other dependencies, map the new configuration to release in your `Podfile` (`project 'MyApp', 'QA' => :release`) and run `pod install` again.
2. In your app target’s **Build Settings**, add a user-defined setting `SHOREBIRD_TRACK`. Leave it empty for Release and set it to `qa` for QA.
3. Still in the target’s **Build Settings**, give the QA configuration its own **Product Bundle Identifier**, such as `com.example.myapp.qa`. The QA build has to keep the version and build number of the build you ship, and App Store Connect accepts each build number only once per app, so the QA build needs an app of its own there (or distribute it ad hoc).
4. In your app’s Info.plist, add an entry `ShorebirdTrack` with the value `$(SHOREBIRD_TRACK)`.
5. Duplicate your scheme, name it for QA, and set its **Archive** build configuration to **QA**.

Archive the QA scheme and hand the build to QA the way you do today, for example with TestFlight under the QA app. Archives of your normal scheme still have an empty `ShorebirdTrack`, so they check the default track.

## Ship a new release

1. Release the module, with the version of the host app that will contain it:

   * Android

     Android

     ```
     shorebird release aar --release-version 1.2.3+1
     ```

   * iOS

     iOS

     ```
     shorebird release ios-framework --release-version 1.2.3+4
     ```

2. Build the host app’s QA build from that `release` directory and give it to QA.

3. When QA signs off, build the host app you ship from the **same** `release` directory and submit it to the store. Don’t rebuild the module in between.

## Ship a patch

1. Make your change in the module and publish the patch to `qa`:

   * Android

     Android

     ```
     shorebird patch aar --release-version 1.2.3+1 --track qa
     ```

   * iOS

     iOS

     ```
     shorebird patch ios-framework --release-version 1.2.3+4 --track qa
     ```

2. QA launches their build so the module downloads the patch (if the app starts Flutter only when a Flutter screen opens, they open one), then fully quits it and launches it again to run it. Builds from the store never see it.

3. When QA signs off, promote the patch to `stable`, with the patch number that `shorebird patch` printed (or find it with `shorebird patches list --release-version <version>`):

   * Android

     Android

     ```
     shorebird patches promote --release-version 1.2.3+1 --patch-number 2
     ```

   * iOS

     iOS

     ```
     shorebird patches promote --release-version 1.2.3+4 --patch-number 2
     ```

   Your users get the exact patch QA tested.

To patch again, publish the next patch to `qa` and repeat. A patch QA rejects stays on `qa`; publish a fixed one to `qa` and promote that instead.

## If QA and your users get the same store build

Some teams don’t have a separate QA build: QA installs from a Google Play test track or TestFlight, and the same binary is later promoted to your users. That binary can’t have `qa` built in. Choose the track at runtime instead, for example from how the app was installed or who is signed in, and pass it to the module the same way. See [Testing patches with a subset of users](/code-push/guides/testing-patches) for ways to choose it.
