Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions e2e/BITRISE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,26 +60,29 @@ Duplicate in-progress PR pipelines are cancelled by Bitrise native Rolling build

Nightly pipelines ship the sample apps to the stores from the same E2E test storefront the PR pipeline uses, so they need no storefront configuration of their own.

| Pipeline | App | Destination |
| ------------------------------------- | ---------------------------- | ----------- |
| `nightly-swift-ios-testflight` | `CheckoutKitSwiftDemo` | TestFlight |
| `nightly-react-native-ios-testflight` | `CheckoutKitReactNativeDemo` | TestFlight |
| Pipeline | App | Destination |
| ------------------------------------- | ---------------------------- | ------------------- |
| `nightly-swift-ios-testflight` | `CheckoutKitSwiftDemo` | TestFlight |
| `nightly-react-native-ios-testflight` | `CheckoutKitReactNativeDemo` | TestFlight |
| `nightly-kotlin-android-play` | `CheckoutKitAndroidDemo` | Play internal track |

These pipelines are deliberately absent from `trigger_map`, so nothing starts them on a pull request. Create a daily **scheduled build** under **Project settings > Scheduled builds**, targeting `main` and selecting the pipeline. The schedule is the one part of this design that Bitrise keeps outside the repository.

### Commit age gate

Every nightly pipeline starts with `nightly-decide-should-build`, which runs on the default Linux stack and publishes `NIGHTLY_SHOULD_BUILD`. The release workflow is gated on it with `run_if`, so a night with no new commits never boots a macOS machine and never consumes a store build number.
Every nightly pipeline starts with `nightly-decide-should-build`, which runs on the default Linux stack and publishes `NIGHTLY_SHOULD_BUILD`. The release workflow is gated on it with `run_if`, so a night with no new commits never boots a build machine and never consumes a store build number.

The gate asks whether HEAD was committed inside `NIGHTLY_COMMIT_WINDOW`, which defaults to `24 hours`. **Keep this window equal to the schedule interval.** A window shorter than the interval skips commits, and a longer one re-uploads work that already shipped.

### Build numbers

The build number is `$BITRISE_BUILD_NUMBER`, injected as an `xcodebuild` build-setting override. No committed file changes value, so nothing has to be bumped by hand and no two uploads can collide.
Every nightly build numbers itself from `$BITRISE_BUILD_NUMBER`, injected at build time. No committed file changes value, so nothing has to be bumped by hand and no two uploads can collide.

This only works because each sample binds `CFBundleVersion` to `$(CURRENT_PROJECT_VERSION)` rather than to a literal. `CheckoutKitSwiftDemo` binds it in its XcodeGen spec, and `CheckoutKitReactNativeDemo` binds it in its committed `Info.plist`. Without that binding the literal wins, the override is silently discarded, and App Store Connect rejects every upload after the first. Both build scripts call `e2e_assert_archived_build_number`, which re-reads the archived plist and fails the build if the number did not land.
On iOS the value arrives as an `xcodebuild` build-setting override. This only works because each sample binds `CFBundleVersion` to `$(CURRENT_PROJECT_VERSION)` rather than to a literal. `CheckoutKitSwiftDemo` binds it in its XcodeGen spec, and `CheckoutKitReactNativeDemo` binds it in its committed `Info.plist`. Without that binding the literal wins, the override is silently discarded, and App Store Connect rejects every upload after the first. Both build scripts call `e2e_assert_archived_build_number`, which re-reads the archived plist and fails the build if the number did not land.

### Signing
On Android the value arrives as the Gradle property `-PcheckoutKitVersionCode`, which `app/build.gradle` reads for both `versionCode` and the derived `versionName`. The committed default is unchanged, so PR builds and local development are unaffected. The build script then calls `e2e_assert_android_version_code`, which re-reads the merged manifest and fails the build if the number did not land.

### iOS signing and upload

The nightly iOS build passes its signing arguments explicitly and calls `e2e_reject_ios_signing_overrides` first, because each `E2E_IOS_*` variable in the Code signing table below wins over the matching argument. Do not expose any of them to a nightly workflow; a release build would silently fall back to development signing.

Expand All @@ -100,6 +103,30 @@ Required Bitrise code signing assets, beyond the E2E development assets:
| App Store Connect connection | An App Store Connect API key connection on the Bitrise app, so the upload step needs `connection: automatic` and no secret. |
| App Store Connect app record | An app record for the bundle identifier. The upload cannot create one. |

Both iOS samples declare `ITSAppUsesNonExemptEncryption` as `false`, so TestFlight accepts each build without asking for an export compliance answer. Both apps reach the network only through the system HTTPS stack, which is exempt. Remove the key if either app ever adds its own encryption.

### Android signing and upload

Signing stays out of Gradle. `buildTypes.release` in `app/build.gradle` declares no `signingConfig`, so `bundleRelease` writes an unsigned bundle and `sign-apk@2` signs it afterwards. That step reads the keystore already configured on the Bitrise app, so no secret name appears in `e2e/bitrise.yml` and the sample gains no release signing surface.

`google-play-deploy@3` then uploads to the `internal` track with `status: completed`. It is passed an empty `mapping_file`, because the release build sets `minifyEnabled false` and R8 writes no mapping file; the step default points at one and fails when it is absent.

The internal track is the only track that reaches testers with no manual step. Managed publishing does not cover it, and updates to it are not reviewed, so `status: completed` publishes to the tester list within minutes. The closed track is reviewed on every release, which no daily schedule can absorb.

Two one-time exceptions apply. The app's **first** release is reviewed even on the internal track, and for up to 48 hours it shows a temporary name until that review completes. A release that follows a **rejection** is also reviewed. Every other nightly publishes automatically.

Access is invite-only. A tester needs the opt-in link and a Google account on the tester list, and the app stays out of Play search. The internal track caps at 100 testers.

| Asset | Requirement |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| Keystore | A release keystore under **Code signing**. `sign-apk@2` reads the Bitrise keystore variables by default. |
| Service account key | The Play service-account JSON in **Generic file storage**, with env key exactly `SERVICE_ACCOUNT_JSON_KEY`. |
| Service account grant | The *Release to testing tracks* permission in the Play Console. |
| Play app record | An app for the package name, with its **first release uploaded by hand**. The API cannot create the first release. |
| Tester list | An internal testing tester list, or the upload succeeds and nobody can install it. |

`dry_run: "true"` on `google-play-deploy@3` validates the credentials and the app record without publishing. It is optional here, because every run takes a fresh `$BITRISE_BUILD_NUMBER` and no two `versionCode` values can collide.

## Required app environment variables

The non-secret E2E defaults live in `e2e/bitrise.yml` under `app.envs`. Change them in this repository rather than in the Bitrise Workflow Editor.
Expand Down
48 changes: 48 additions & 0 deletions e2e/bitrise.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ pipelines:
depends_on:
- nightly-decide-should-build

nightly-kotlin-android-play:
workflows:
nightly-decide-should-build: {}
nightly-release-kotlin-android:
run_if:
expression: '{{ enveq "NIGHTLY_SHOULD_BUILD" "true" }}'
depends_on:
- nightly-decide-should-build

step_bundles:
install-node-modules:
steps:
Expand Down Expand Up @@ -519,3 +528,42 @@ workflows:
- ipa_path: $NIGHTLY_REACT_NATIVE_IOS_IPA_PATH
- deploy-to-bitrise-io@2:
is_always_run: false

nightly-release-kotlin-android:
meta:
bitrise.io:
stack: *android_stack
machine_type_id: g2.linux.large
steps:
- git-clone@8: {}
- script@1:
title: Build the Kotlin Android release bundle
timeout: 3600
no_output_timeout: 1800
inputs:
- content: |-
set -euo pipefail
e2e/scripts/build_kotlin_android_release
# Signing stays out of Gradle. This step reads the keystore already
# configured on the Bitrise app, so no secret name appears in this file and
# app/build.gradle gains no release signing config.
- sign-apk@2:
inputs:
- android_app: $NIGHTLY_KOTLIN_ANDROID_AAB_PATH
- google-play-deploy@3:
inputs:
# Bitrise derives this name from the Generic file storage env key, so
# the uploaded key must be stored as exactly SERVICE_ACCOUNT_JSON_KEY.
- service_account_json_key_path: $BITRISEIO_SERVICE_ACCOUNT_JSON_KEY_URL
- package_name: com.shopify.checkoutkit.androiddemo
# Managed publishing does not cover the internal track, and updates to
# it skip review, so `completed` reaches testers with no manual step.
# `draft` would serve nobody until someone clicks publish.
- track: internal
- status: completed
- app_path: $BITRISE_SIGNED_AAB_PATH
# The release build sets minifyEnabled false, so R8 writes no mapping
# file. The step default points at one and fails when it is absent.
- mapping_file: ""
- deploy-to-bitrise-io@2:
is_always_run: false
16 changes: 16 additions & 0 deletions e2e/scripts/bitrise_ci_helpers
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ e2e_configure_storefront() {
./scripts/setup_storefront_env --skip-optional-prompts
}

# A wrong versionCode is otherwise only reported by Google Play, long after this
# build has been marked green.
e2e_assert_android_version_code() {
local merged_manifest="$1"
local expected="$2"

e2e_log "Verifying the merged manifest versionCode"
local merged
merged="$(sed -n 's/.*android:versionCode="\([0-9]*\)".*/\1/p' "$merged_manifest" | head -1)"
if [ "$merged" != "$expected" ]; then
e2e_log "Merged versionCode is ${merged}, expected ${expected}"
echo "Check that versionCode in ${merged_manifest##*/} still reads the checkoutKitVersionCode property." >&2
return 1
fi
}

e2e_nightly_commit_window() {
printf '%s\n' "${NIGHTLY_COMMIT_WINDOW:-24 hours}"
}
Expand Down
27 changes: 27 additions & 0 deletions e2e/scripts/build_kotlin_android_release
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$script_dir/bitrise_ci_helpers"

: "${BITRISE_BUILD_NUMBER:?BITRISE_BUILD_NUMBER is required. Google Play rejects a versionCode it has already seen.}"

e2e_configure_storefront

e2e_log "Building the Kotlin Android release bundle"
cd platforms/android/samples/CheckoutKitAndroidDemo
./gradlew :app:bundleRelease \
-PcheckoutKitVersionCode="$BITRISE_BUILD_NUMBER" \
--no-daemon \
--console=plain

android_aab="$PWD/app/build/outputs/bundle/release/app-release.aab"
test -f "$android_aab"

e2e_assert_android_version_code \
"$PWD/app/build/intermediates/merged_manifests/release/processReleaseManifest/AndroidManifest.xml" \
"$BITRISE_BUILD_NUMBER"

e2e_log "Publishing Kotlin Android AAB path"
envman add --key NIGHTLY_KOTLIN_ANDROID_AAB_PATH --value "$android_aab"
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ if (!storefrontDomain || !accessToken) {
println("**** Please add a .env file with STOREFRONT_DOMAIN and STOREFRONT_ACCESS_TOKEN set *****")
}

def appVersionCode = (project.findProperty("checkoutKitVersionCode") ?: 34) as Integer

android {
namespace = 'com.shopify.checkoutkit.androiddemo'
compileSdk = 37
Expand All @@ -68,8 +70,8 @@ android {
applicationId "com.shopify.checkoutkit.androiddemo"
minSdk = 28
targetSdk = 36
versionCode = 34
versionName = "0.0.${versionCode}"
versionCode = appVersionCode
versionName = "0.0.${appVersionCode}"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
Loading