Skip to content

Give QA an add-to-app build, then promote it

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. 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 or iOS add-to-app guide.

  • 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 .xcframeworks from the same release directory. The QA build differs only in the host app: it tells the module which track to check.

Set up the module once, before you release

Section titled “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: 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.

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: 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.

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

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:

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")
}
  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 .xcframeworks. 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.

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

    shorebird release aar --release-version 1.2.3+1
  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.

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

    shorebird patch aar --release-version 1.2.3+1 --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>):

    shorebird patches promote --release-version 1.2.3+1 --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

Section titled “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 for ways to choose it.