From e4b421aadd49ae31c8e409a011dfe0b3f72de3e1 Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Mon, 28 Sep 2026 09:44:03 +0100 Subject: [PATCH 1/5] Build Android against the published protocol by default --- .github/CONTRIBUTING.md | 20 +++++++++++- .github/scripts/validate-release-version | 2 +- .github/workflows/android-publish.yml | 3 ++ .github/workflows/android-test.yml | 8 +++++ dev.yml | 12 ++++++- platforms/android/AGENTS.md | 8 +++-- platforms/android/gradle/libs.versions.toml | 3 ++ platforms/android/lib/build.gradle | 18 ++++++++++- .../CheckoutKitAndroidDemo/settings.gradle | 6 ++-- platforms/android/settings.gradle | 6 ++-- .../scripts/publish_android_snapshot | 1 + .../test/release_protocol_dependency_test.rb | 32 +++++++++++++++++++ 12 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 scripts/test/release_protocol_dependency_test.rb diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5c6218518..a17407a62 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -255,6 +255,10 @@ Open a pull request with the following changes: 1. Bump `embeddedCheckoutProtocolAndroid` in `platforms/android/gradle/libs.versions.toml`. 2. Update `protocol/languages/kotlin/embedded-checkout-protocol/api/embedded-checkout-protocol.api` if the public protocol API changed. +Keep `embeddedCheckoutProtocolAndroidDependency` at the existing published version +until the new protocol artifact is available. This lets the protocol release PR +merge while Kit continues building against its current dependency. + Supported protocol release versions are `YYYY.MM.DD.PATCH` and prerelease versions are `YYYY.MM.DD.PATCH-{alpha|beta|rc}.N`. Once merged, run the [Release package workflow](../../actions/workflows/release.yml): @@ -270,7 +274,21 @@ Once merged, run the [Release package workflow](../../actions/workflows/release. Open a pull request with the following changes: 1. Bump `checkoutKitAndroid` in `platforms/android/gradle/libs.versions.toml`. -2. If the Android Kit release depends on a new protocol version, release `embeddedCheckoutProtocolAndroid` first. +2. If Kit needs a new protocol version, publish that protocol release first, then + update `embeddedCheckoutProtocolAndroidDependency` to it in the same catalog. + +The Android library and sample compile against the protocol artifact pinned in +`platforms/android/gradle/libs.versions.toml` from Maven Central. CI uses the same +dependency, and the publish workflow runs unit tests and API checks before uploading. +Changes to protocol source are tested separately with `dev protocol test kotlin`. + +For joint development against unreleased protocol changes, run `dev android local test`, +`dev android local build`, or `dev android local start`. The `local` prefix works with +any Android command and sets the Gradle property `useLocalProtocol=true` for that +invocation. When running Gradle directly, pass `-PuseLocalProtocol=true`. +Remote publication rejects local mode; React Native's explicit `--local` flow can +still publish both artifacts to Maven Local. Kit changes that need a new protocol +version become mergeable once that version is published and normal CI passes. Supported release versions are `X.Y.Z` and prerelease versions are `X.Y.Z-{alpha|beta|rc}.N`. diff --git a/.github/scripts/validate-release-version b/.github/scripts/validate-release-version index caff3bcd4..46cfda90e 100755 --- a/.github/scripts/validate-release-version +++ b/.github/scripts/validate-release-version @@ -102,7 +102,7 @@ case "$PLATFORM_INPUT" in ANDROID_VERSION_FILE="platforms/android/gradle/libs.versions.toml" VERSION=$(version_catalog_value "$ANDROID_VERSION_FILE" "checkoutKitAndroid") - ANDROID_PROTOCOL_VERSION=$(version_catalog_value "$ANDROID_VERSION_FILE" "embeddedCheckoutProtocolAndroid") + ANDROID_PROTOCOL_VERSION=$(version_catalog_value "$ANDROID_VERSION_FILE" "embeddedCheckoutProtocolAndroidDependency") ;; "Embedded Checkout Protocol"|embedded-checkout-protocol|EmbeddedCheckoutProtocol|ecp|ECP) diff --git a/.github/workflows/android-publish.yml b/.github/workflows/android-publish.yml index 0bab975c0..54875fecd 100644 --- a/.github/workflows/android-publish.yml +++ b/.github/workflows/android-publish.yml @@ -67,6 +67,9 @@ jobs: distribution: temurin java-version: 17 + - name: Test and check API against the published protocol + run: ./gradlew :lib:testDebugUnitTest :lib:apiCheck --console=plain + - name: Publish Package run: | ./gradlew :lib:publishReleasePublicationToOssrh-staging-apiRepository diff --git a/.github/workflows/android-test.yml b/.github/workflows/android-test.yml index 8feb51ee9..574cda06f 100644 --- a/.github/workflows/android-test.yml +++ b/.github/workflows/android-test.yml @@ -31,6 +31,10 @@ jobs: - name: Run Tests run: ./gradlew test --console=plain + - name: Run Protocol Unit Tests + working-directory: protocol/languages/kotlin + run: ./gradlew test --console=plain + - name: Setup sample app environment id: sample_env if: ${{ !cancelled() }} @@ -147,6 +151,10 @@ jobs: - name: Kotlin Lint run: ./gradlew detekt + - name: Protocol Kotlin Lint + working-directory: protocol/languages/kotlin + run: ./gradlew detekt + - name: Setup sample app environment run: ${{ github.workspace }}/scripts/setup_storefront_env env: diff --git a/dev.yml b/dev.yml index 4694d9899..95351faba 100644 --- a/dev.yml +++ b/dev.yml @@ -211,7 +211,7 @@ commands: } run_kotlin() { echo "Running tests for Kotlin..." - platforms/android/gradlew -p platforms/android :embedded-checkout-protocol:test + protocol/languages/kotlin/gradlew -p protocol/languages/kotlin :embedded-checkout-protocol:test } run_typescript() { cd $root/protocol @@ -305,6 +305,16 @@ commands: aliases: ["kotlin"] desc: "Android Checkout Kit commands" subcommands: + local: + desc: Run an Android command against in-repo protocol sources + syntax: " [args...]" + run: | + if [ "$#" -eq 0 ]; then + echo "Usage: dev android local [args...]" >&2 + exit 1 + fi + ORG_GRADLE_PROJECT_useLocalProtocol=true /opt/dev/bin/dev android "$@" + build: desc: Build the library run: platforms/android/gradlew -p platforms/android :lib:build diff --git a/platforms/android/AGENTS.md b/platforms/android/AGENTS.md index 052341b5b..d733d673a 100644 --- a/platforms/android/AGENTS.md +++ b/platforms/android/AGENTS.md @@ -10,9 +10,11 @@ The main modules are: - **`lib/`** — the Checkout Kit library, published as `com.shopify:checkout-kit`. It presents Shopify checkouts as a native bottom-sheet-hosted WebView in consumer apps. - **`../../protocol/languages/kotlin/embedded-checkout-protocol/`** — the Embedded Checkout Protocol Kotlin artifact, published as `com.shopify:embedded-checkout-protocol`. The Android Gradle project path is `:embedded-checkout-protocol`, and the Kotlin package is `com.shopify.ucp.embedded.checkout`. -- **`samples/CheckoutKitAndroidDemo/`** — a demo app that consumes Checkout Kit and the Kotlin protocol artifact as source dependencies. Changes here never reach consumers; this module is for internal testing and developer onboarding. +- **`samples/CheckoutKitAndroidDemo/`** — a demo app that consumes Checkout Kit from source and the published Kotlin protocol artifact transitively. Changes here never reach consumers; this module is for internal testing and developer onboarding. -The sample is a separate Gradle composite (`samples/CheckoutKitAndroidDemo/settings.gradle`) that includes `:lib` and the Kotlin protocol `:embedded-checkout-protocol` as source dependencies. The sample's `gradle.properties` and Gradle wrapper are independent of the Android root's. The standalone Kotlin protocol Gradle root also has its own wrapper at `../../protocol/languages/kotlin/gradlew`; keep its Gradle version aligned with the Android root wrapper. +The sample is a separate Gradle build (`samples/CheckoutKitAndroidDemo/settings.gradle`) that includes `:lib` from source. The sample's `gradle.properties` and Gradle wrapper are independent of the Android root's. The standalone Kotlin protocol Gradle root also has its own wrapper at `../../protocol/languages/kotlin/gradlew`; keep its Gradle version aligned with the Android root wrapper. + +The library and sample resolve the pinned protocol artifact from Maven Central by default, including in CI. To develop against unreleased protocol source, use `dev android local ` (for example `dev android local test`) or pass `-PuseLocalProtocol=true` to Gradle. Only this explicit mode includes `:embedded-checkout-protocol` in the Android builds. Remote Kit publication rejects local mode; `publishToMavenLocal` remains available for React Native's explicit `--local` workflow. Test protocol source independently with `dev protocol test kotlin`. ## Where to make changes @@ -96,7 +98,7 @@ Raising any of these is a consumer-facing breaking change and needs visible rele Published Android artifact versions are bumped via: -1. `gradle/libs.versions.toml` (`checkoutKitAndroid` and `embeddedCheckoutProtocolAndroid`). +1. `gradle/libs.versions.toml`: `checkoutKitAndroid` is the Kit release version; `embeddedCheckoutProtocolAndroid` is the protocol release version; `embeddedCheckoutProtocolAndroidDependency` is the already-published protocol version Kit consumes. Bump the dependency only after publishing the protocol release. 2. The install snippets in `README.md` (Gradle and Maven). After the Android artifact is published, update `platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json` (`checkoutKit.nativeSdkVersions.android`) in the React Native release flow if RN should consume that published `com.shopify:checkout-kit` SemVer. RN CI resolves this value from Maven, so do not point it at an unpublished Android version. diff --git a/platforms/android/gradle/libs.versions.toml b/platforms/android/gradle/libs.versions.toml index c965c5c92..f4ace2430 100644 --- a/platforms/android/gradle/libs.versions.toml +++ b/platforms/android/gradle/libs.versions.toml @@ -1,6 +1,8 @@ [versions] checkoutKitAndroid = "4.0.0-alpha.8" +# Protocol release version; bump independently of the published dependency below. embeddedCheckoutProtocolAndroid = "2026.08.25.1-alpha.1" +embeddedCheckoutProtocolAndroidDependency = "2026.08.25.1-alpha.1" androidApplicationGradlePlugin = "9.3.1" androidLibraryGradlePlugin = "9.3.1" @@ -66,6 +68,7 @@ assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } +embedded-checkout-protocol = { module = "com.shopify:embedded-checkout-protocol", version.ref = "embeddedCheckoutProtocolAndroidDependency" } junit = { module = "junit:junit", version.ref = "junit" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidCompose" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlinStdlib" } diff --git a/platforms/android/lib/build.gradle b/platforms/android/lib/build.gradle index f812ac732..f0053bef4 100644 --- a/platforms/android/lib/build.gradle +++ b/platforms/android/lib/build.gradle @@ -1,5 +1,6 @@ import io.gitlab.arturbosch.detekt.Detekt import org.gradle.api.file.FileCollection +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository import org.gradle.api.tasks.Classpath import org.gradle.process.CommandLineArgumentProvider import org.jetbrains.kotlin.gradle.dsl.JvmTarget @@ -19,6 +20,7 @@ apply from: file('../gradle/android-library-versions.gradle') def kotlinCompatibility = kotlinVersionCompatibility def versionName = libs.versions.checkoutKitAndroid.get() +def useLocalProtocol = providers.gradleProperty('useLocalProtocol').getOrElse('false').toBoolean() class MockitoAgentArgumentProvider implements CommandLineArgumentProvider { @Classpath @@ -120,7 +122,11 @@ dependencies { detektPlugins libs.detekt.formatting mockitoAgent libs.mockito.core - api project(':embedded-checkout-protocol') + if (useLocalProtocol) { + api project(':embedded-checkout-protocol') + } else { + api libs.embedded.checkout.protocol + } testImplementation libs.junit testImplementation libs.robolectric testImplementation libs.mockito.core @@ -142,6 +148,16 @@ dependencies { implementation libs.androidx.webkit } +// Local protocol sources are for joint development only. Maven Local remains +// available for React Native's explicit --local builds. +tasks.withType(PublishToMavenRepository).configureEach { + doFirst { + if (useLocalProtocol) { + throw new GradleException('Cannot publish Checkout Kit with useLocalProtocol=true. Build against the published protocol first.') + } + } +} + signing { def signingKeyId = findProperty("signingKeyId") def signingKey = findProperty("signingKey") diff --git a/platforms/android/samples/CheckoutKitAndroidDemo/settings.gradle b/platforms/android/samples/CheckoutKitAndroidDemo/settings.gradle index 00b46edd6..3b391741c 100644 --- a/platforms/android/samples/CheckoutKitAndroidDemo/settings.gradle +++ b/platforms/android/samples/CheckoutKitAndroidDemo/settings.gradle @@ -26,7 +26,9 @@ dependencyResolutionManagement { } rootProject.name = "CheckoutKitAndroidDemo" include ':app' -include ':embedded-checkout-protocol' include ':lib' -project(':embedded-checkout-protocol').projectDir = new File(settingsDir, '../../../../protocol/languages/kotlin/embedded-checkout-protocol') +if (providers.gradleProperty('useLocalProtocol').getOrElse('false').toBoolean()) { + include ':embedded-checkout-protocol' + project(':embedded-checkout-protocol').projectDir = new File(settingsDir, '../../../../protocol/languages/kotlin/embedded-checkout-protocol') +} project(':lib').projectDir = new File(settingsDir, '../../lib') diff --git a/platforms/android/settings.gradle b/platforms/android/settings.gradle index 247447593..0569aa1ea 100644 --- a/platforms/android/settings.gradle +++ b/platforms/android/settings.gradle @@ -16,6 +16,8 @@ dependencyResolutionManagement { } rootProject.name = "checkout-kit" -include ':embedded-checkout-protocol' -project(':embedded-checkout-protocol').projectDir = file('../../protocol/languages/kotlin/embedded-checkout-protocol') +if (providers.gradleProperty('useLocalProtocol').getOrElse('false').toBoolean()) { + include ':embedded-checkout-protocol' + project(':embedded-checkout-protocol').projectDir = file('../../protocol/languages/kotlin/embedded-checkout-protocol') +} include ':lib' diff --git a/platforms/react-native/scripts/publish_android_snapshot b/platforms/react-native/scripts/publish_android_snapshot index c295312ce..5e8bb80f6 100755 --- a/platforms/react-native/scripts/publish_android_snapshot +++ b/platforms/react-native/scripts/publish_android_snapshot @@ -11,6 +11,7 @@ ANDROID_SDK_PATH="$SCRIPT_DIR/../../android" cd "$ANDROID_SDK_PATH" ./gradlew \ + -PuseLocalProtocol=true \ :embedded-checkout-protocol:publishToMavenLocal \ :lib:publishToMavenLocal \ -q diff --git a/scripts/test/release_protocol_dependency_test.rb b/scripts/test/release_protocol_dependency_test.rb new file mode 100644 index 000000000..f7b1a2ba9 --- /dev/null +++ b/scripts/test/release_protocol_dependency_test.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require "minitest/autorun" +require "fileutils" +require "open3" +require "tmpdir" + +class ReleaseProtocolDependencyTest < Minitest::Test + VALIDATOR = File.expand_path("../../.github/scripts/validate-release-version", __dir__) + + def test_protocol_can_release_before_kit_adopts_it + Dir.mktmpdir("protocol-release") do |root| + catalog_dir = File.join(root, "platforms/android/gradle") + FileUtils.mkdir_p(catalog_dir) + File.write(File.join(catalog_dir, "libs.versions.toml"), <<~TOML) + [versions] + checkoutKitAndroid = "4.0.0" + embeddedCheckoutProtocolAndroid = "2026.09.28.1" + embeddedCheckoutProtocolAndroidDependency = "2026.08.25.1" + TOML + + android, error, status = Open3.capture3(VALIDATOR, "Android", chdir: root) + assert status.success?, error + assert_includes android.lines, "android_protocol_version=2026.08.25.1\n" + + protocol, error, status = Open3.capture3(VALIDATOR, "Embedded Checkout Protocol", chdir: root) + assert status.success?, error + assert_includes protocol.lines, "version=2026.09.28.1\n" + assert_includes protocol.lines, "tag=embedded-checkout-protocol/2026.09.28.1\n" + end + end +end From e7d22ca2067c7b2a26f48ec65dfd475f3485b23e Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Mon, 28 Sep 2026 09:51:10 +0100 Subject: [PATCH 2/5] Document the ECP to Android to React Native release sequence --- .github/CONTRIBUTING.md | 89 +++++++++++++++++++ .github/pull_request_template.md | 12 +++ platforms/android/README.md | 6 ++ .../react-native/docs/contributing/release.md | 69 +++++++++++--- 4 files changed, 166 insertions(+), 10 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a17407a62..dcc63fed7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -156,6 +156,91 @@ where they help consumers understand the change or migrate. Keep `None.` for sections that do not apply. The generated list of included pull requests and contributors remains below the curated sections. +### Coordinating ECP, Android, and React Native releases + +When a feature needs changes across all three packages, release them in this order: + +```text +Embedded Checkout Protocol (Maven Central) + → Checkout Kit Android (Maven Central) + → Checkout Kit React Native (npm) +``` + +Each downstream package must build and pass CI against its newly published +dependency before it is released. A GitHub tag or release alone is not enough: +wait for the upstream publish workflow and any registry approval/propagation to +finish, then let the downstream Gradle build resolve the artifact from Maven +Central. The release workflows run independently; they do not automatically +sequence these three stages. + +The version declarations have different jobs. The Android catalog is +`platforms/android/gradle/libs.versions.toml`; the RN manifest is +`platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json`. + +| Package | Its release version | Dependency it consumes | +| --- | --- | --- | +| Kotlin ECP | Catalog: `embeddedCheckoutProtocolAndroid` | — | +| Android Kit | Catalog: `checkoutKitAndroid` | Catalog: `embeddedCheckoutProtocolAndroidDependency` | +| React Native | RN manifest: `version` | RN manifest: `checkoutKit.nativeSdkVersions.android` | + +1. **Release ECP.** Merge the protocol changes, any API baseline changes, and + `embeddedCheckoutProtocolAndroid` bump after protocol tests/API checks pass. + Leave Kit's dependency pin and RN's native SDK pins at their existing published + versions. Follow [the ECP release steps](#releasing-a-new-embedded-checkout-protocol-version) + and wait for the new protocol JAR and metadata to be available on Maven Central. +2. **Adopt ECP and release Android.** In the Kit PR, update + `embeddedCheckoutProtocolAndroidDependency`, make the SDK changes, and bump + `checkoutKitAndroid` and the installation snippets. Run normal SDK and sample + tests/builds and API checks against the published ECP dependency. Merge with + passing CI, follow [the Android release steps](#releasing-a-new-android-version), + and wait for the Kit AAR and metadata to be available on Maven Central. +3. **Adopt Android and release RN.** In the RN PR, update + `checkoutKit.nativeSdkVersions.android` to the published Kit version, make the + wrapper changes, and bump the RN package's own `version`. Run the normal RN + Android tests and sample build, plus the other required RN checks, before + merging. Follow [the RN release steps](#releasing-a-new-react-native-version) + to publish the npm package. RN resolves ECP transitively through Kit; it does + not need a separate ECP version pin. Update the iOS pin only when needed, after + the required Swift release is available on CocoaPods. + +This is a dependency waterfall, not a requirement to release every package every +time. An Android-only fix can retain its ECP pin and start at stage 2. An RN-only +fix can retain its native SDK pins and start at stage 3. An ECP release does not +force Kit or RN to adopt it immediately, and the package versions need not match. + +#### Why publish in this order? + +- **Each layer tests the artifacts it declares.** Kit compiles against the same + published ECP API it asks consumers to resolve. RN then compiles against the + released Kit and its transitive protocol dependency. Missing types or methods + can fail those builds before a downstream release ships. +- **Versions form an explicit boundary.** A protocol source change does not + silently change Kit's dependency, and a Kit source change does not silently + change RN's dependency. Each adoption is a reviewable pin update with CI results. +- **The packages remain composable.** Apps and other SDKs can use standalone ECP + or Kit's normal transitive dependency without Kit embedding another copy of the + protocol classes. Gradle can resolve the shared dependency through its metadata. + +The cost is waiting for upstream publication and running downstream CI between +releases. That waiting is deliberate: a successful local source build does not +prove that the declared published dependency supports the new code. The RN npm +publish job builds and packs the JavaScript module; it does not rerun Android +compilation, so passing RN Android CI against the published SDK is required before +the RN release. + +#### Developing changes across packages + +Develop the changes together on stacked branches using `dev android local test` +or `dev android local api check` for Kit/ECP, and `dev rn test android --local` +or `dev rn android --local` for RN/native changes. These explicit overrides allow +early integration before publication. On the RN development branch, set +`checkoutKit.nativeSdkVersions.android` to the in-repo `checkoutKitAndroid` version +so Maven Local resolves the artifact just built. Keep downstream adoption PRs open +until their upstream artifacts are published, then rerun normal builds and CI without +the overrides. Clear `USE_LOCAL_SDK` and `ORG_GRADLE_PROJECT_useLocalProtocol` if +they were exported in your shell. Local success does not replace the published +dependency checks or change the release order. + --- ## Swift (`platforms/swift/`) @@ -321,6 +406,10 @@ When updating the Swift or Android SDK version that React Native should consume, For coordinated native and React Native releases, publish Android and Swift first, then update these React Native native SDK version pointers and publish React Native. +For an Android change that also needs a new protocol version, use the full +[ECP → Android → RN release sequence](#coordinating-ecp-android-and-react-native-releases). +Keep an unchanged iOS dependency pinned to its existing published release. + ### Public API surface The library's public API is tracked via a committed report at `platforms/react-native/modules/@shopify/checkout-kit-react-native/api/checkout-kit-react-native.api.md`, generated by [@microsoft/api-extractor](https://api-extractor.com/) from the bob-produced `.d.ts` files. The unified `Breaking Changes` CI workflow runs `dev rn api check` on every PR that touches React Native sources and fails if the regenerated report diverges from the committed one. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 05fcbeacc..1b62d4382 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -33,6 +33,7 @@ - [ ] I have bumped `embeddedCheckoutProtocolAndroid` in `platforms/android/gradle/libs.versions.toml` - [ ] I have updated `protocol/languages/kotlin/embedded-checkout-protocol/api/embedded-checkout-protocol.api` if the public API changed +- [ ] Kit's `embeddedCheckoutProtocolAndroidDependency` still references an available Maven Central release @@ -41,8 +42,19 @@ - [ ] I have bumped `checkoutKitAndroid` in `platforms/android/gradle/libs.versions.toml` - [ ] I have updated the Gradle/Maven version snippets in `platforms/android/README.md` +- [ ] The ECP version in `embeddedCheckoutProtocolAndroidDependency` is published, and normal Android CI passes against it + + + +
+Releasing a new React Native version? + +- [ ] I have bumped `version` in `platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json` +- [ ] Any updated `checkoutKit.nativeSdkVersions` pins are available on Maven Central/CocoaPods +- [ ] Normal RN Android tests and the sample build pass against the published SDK, without `--local` or `USE_LOCAL_SDK=1`
> [!TIP] > See the [Contributing documentation](./CONTRIBUTING.md) for the full release process per platform. +> Changes spanning ECP, Android, and RN follow the [dependency release sequence](./CONTRIBUTING.md#coordinating-ecp-android-and-react-native-releases). diff --git a/platforms/android/README.md b/platforms/android/README.md index 48eed9226..0edaaea58 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -748,6 +748,12 @@ See [samples](samples/README.md). `CheckoutKitAndroidDemo` demonstrates an Apoll See [CONTRIBUTING](../../.github/CONTRIBUTING.md). +The SDK and sample use the published ECP dependency by default. For joint protocol +development, use `dev android local ` or Gradle's `-PuseLocalProtocol=true`. +Changes spanning the protocol, Android SDK, and React Native follow the +[ECP → Android → RN release sequence](../../.github/CONTRIBUTING.md#coordinating-ecp-android-and-react-native-releases), +with normal CI against each published dependency before releasing its consumer. + Useful checks before opening an Android change: ```sh diff --git a/platforms/react-native/docs/contributing/release.md b/platforms/react-native/docs/contributing/release.md index f8481b2e5..15c12b4b0 100644 --- a/platforms/react-native/docs/contributing/release.md +++ b/platforms/react-native/docs/contributing/release.md @@ -1,15 +1,49 @@ # Release -The `@shopify/checkout-kit-react-native` module is published to the NPM package -registry with public access. +The `@shopify/checkout-kit-react-native` module is published to npm with public +access. Its Android wrapper consumes the published `com.shopify:checkout-kit` +artifact, which in turn depends on `com.shopify:embedded-checkout-protocol` (ECP). -In order to publish a new version of the package, you must complete the -following steps: +## Publish dependencies before their consumers -1. Bump the version in `modules/@shopify/checkout-kit-react-native/package.json` to an - appropriate value. -2. Merge your PR to `main`. -3. Run the [Release package workflow](/actions/workflows/release.yml). +For a change spanning the Android dependency chain, the release order is: + +```text +ECP → Android Kit → React Native +``` + +Follow the repository's [coordinated release process](../../../../.github/CONTRIBUTING.md#coordinating-ecp-android-and-react-native-releases) +to publish ECP and then Android Kit. Wait for each required Maven Central artifact +and its metadata to be available before running the next package's normal CI. +Creating a GitHub release does not by itself mean the artifact is ready to resolve. + +Only release dependencies that need changes. An RN-only fix can keep its existing +native SDK pins. An Android-only fix can keep its existing ECP pin. If RN also +needs a new Swift API, publish that SDK to CocoaPods before updating the iOS pin; +an Android change does not require a new Swift release. + +This ordering lets each package test the published dependencies its consumers +will resolve. Missing native API becomes a build failure before RN ships, and +each dependency update is explicit and reviewable. ECP remains a normal transitive +dependency, so RN needs no separate ECP pin or bundled copy of the protocol. +The tradeoff is waiting for upstream releases and rerunning downstream CI. + +## Prepare the RN release + +1. After the required native artifacts are published, update the relevant + `checkoutKit.nativeSdkVersions.android` and/or `.ios` entry in + `modules/@shopify/checkout-kit-react-native/package.json`. These are dependency + versions; they do not have to match the RN package version. +2. Make the wrapper changes, bump the manifest's own `version`, and update the + public API report with `dev rn api dump` if the API changed. +3. Run normal checks against published dependencies, including `dev rn test android`. + Require the [RN Android test workflow](https://github.com/Shopify/checkout-kit/actions/workflows/rn-test-android.yml) + and [Android sample build](https://github.com/Shopify/checkout-kit/actions/workflows/rn-build-android.yml), + along with the other required RN CI checks, to pass before merging. Run these + without `--local` or `USE_LOCAL_SDK=1`. +4. Merge the release PR to `main`, then run the + [Release package workflow](https://github.com/Shopify/checkout-kit/actions/workflows/release.yml) + with `React Native` selected. Supported release versions are: @@ -27,8 +61,23 @@ release. Rerun with `Draft release` to create a draft GitHub Release with generated release notes for human review; publish the draft release when ready to start the React Native publish workflow. -The publish workflow cleans the module folder, builds a new version, runs -`pnpm pack --dry-run` to verify the contents, and publishes to the NPM registry. +The publish workflow cleans and builds the JavaScript module, inspects the npm +tarball, and publishes it. It does not build or test the Android wrapper again: +passing the native CI checks against the published SDK before release is essential. You can follow the publish action process via https://github.com/Shopify/checkout-kit/actions/workflows/rn-publish.yml. + +## Develop ahead of a native release + +Use `dev rn test android --local` or `dev rn android --local` for early integration +on an unmerged branch. These commands publish the in-repo Kit and ECP to Maven +Local. On that branch, the RN manifest's `checkoutKit.nativeSdkVersions.android` +must match `checkoutKitAndroid` in `platforms/android/gradle/libs.versions.toml` +so Gradle selects the artifact just built. Run the local command again after +changing native source. + +This allows a stack of ECP, Android, and RN changes to be developed together. +Keep the downstream adoption PRs open until their required artifacts are published, +then rerun normal CI. Clear an exported `USE_LOCAL_SDK` before verifying published +dependencies. Local success does not remove the ECP → Android → RN release order. From 753619bcbf5b9a67315ca59bd19806b2c346a712 Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Mon, 28 Sep 2026 10:07:52 +0100 Subject: [PATCH 3/5] Verify resolved protocol before Android tests and publication --- .github/CONTRIBUTING.md | 5 +++++ platforms/android/AGENTS.md | 1 + platforms/android/lib/build.gradle | 33 ++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index dcc63fed7..e8a7ff45d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -365,6 +365,11 @@ Open a pull request with the following changes: The Android library and sample compile against the protocol artifact pinned in `platforms/android/gradle/libs.versions.toml` from Maven Central. CI uses the same dependency, and the publish workflow runs unit tests and API checks before uploading. +Unit tests and remote publication also run `:lib:verifyPublishedProtocol`, which +checks that the release and unit-test classpaths resolve ECP as an external module +at exactly the catalog-pinned dependency version. This rejects accidental project +substitution or version changes introduced by dependency resolution. Explicit local +mode skips this assertion; remote publication still rejects local mode. Changes to protocol source are tested separately with `dev protocol test kotlin`. For joint development against unreleased protocol changes, run `dev android local test`, diff --git a/platforms/android/AGENTS.md b/platforms/android/AGENTS.md index d733d673a..3668549f3 100644 --- a/platforms/android/AGENTS.md +++ b/platforms/android/AGENTS.md @@ -75,6 +75,7 @@ If `apiCheck` fails and you did *not* intend to change public API, the diff tell ## Common commands +- Published protocol resolution: `./gradlew :lib:verifyPublishedProtocol`. Unit tests and remote publication run this automatically to require the catalog-pinned external module on release and unit-test classpaths. Explicit local mode skips it; remote publication still rejects local mode. - Tests: `./gradlew test` (or `dev android test`) - API surface: `./gradlew :lib:apiCheck` / `./gradlew :lib:apiDump` for Checkout Kit, `./gradlew :embedded-checkout-protocol:apiCheck` / `./gradlew :embedded-checkout-protocol:apiDump` from `protocol/languages/kotlin` for protocol, or `dev android api check` / `dev android api dump` for both. - Lint: `./gradlew detekt lintRelease` (or `dev android lint`) diff --git a/platforms/android/lib/build.gradle b/platforms/android/lib/build.gradle index f0053bef4..8592eea4d 100644 --- a/platforms/android/lib/build.gradle +++ b/platforms/android/lib/build.gradle @@ -1,4 +1,7 @@ import io.gitlab.arturbosch.detekt.Detekt +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ModuleComponentSelector +import org.gradle.api.artifacts.result.ResolvedDependencyResult import org.gradle.api.file.FileCollection import org.gradle.api.publish.maven.tasks.PublishToMavenRepository import org.gradle.api.tasks.Classpath @@ -148,9 +151,39 @@ dependencies { implementation libs.androidx.webkit } +def protocolDependency = libs.embedded.checkout.protocol.get() +def verifyPublishedProtocol = tasks.register('verifyPublishedProtocol') { + group = 'verification' + description = 'Verify release and unit-test classpaths use the declared published protocol.' + onlyIf { !useLocalProtocol } + doLast { + def classpaths = ['releaseCompileClasspath', 'releaseRuntimeClasspath'] + configurations.names.findAll { + it.endsWith('UnitTestCompileClasspath') || it.endsWith('UnitTestRuntimeClasspath') + } + classpaths.each { classpath -> + // Follow the requested module so substitutions cannot hide the selected component. + def dependency = configurations.getByName(classpath).incoming.resolutionResult.rootComponent.get().dependencies.find { + it.requested instanceof ModuleComponentSelector && + it.requested.group == protocolDependency.group && it.requested.module == protocolDependency.name + } + def resolved = dependency instanceof ResolvedDependencyResult ? dependency.selected.id : null + if (!(resolved instanceof ModuleComponentIdentifier) || + resolved.group != protocolDependency.group || resolved.module != protocolDependency.name || + resolved.version != protocolDependency.version) { + throw new GradleException("$classpath must resolve the published $protocolDependency; found ${resolved ?: dependency}") + } + } + } +} + +tasks.withType(Test).configureEach { + dependsOn verifyPublishedProtocol +} + // Local protocol sources are for joint development only. Maven Local remains // available for React Native's explicit --local builds. tasks.withType(PublishToMavenRepository).configureEach { + dependsOn verifyPublishedProtocol doFirst { if (useLocalProtocol) { throw new GradleException('Cannot publish Checkout Kit with useLocalProtocol=true. Build against the published protocol first.') From fad21533ed7f7bc2dc015bd177baa1b16537a399 Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Mon, 28 Sep 2026 10:16:48 +0100 Subject: [PATCH 4/5] Clarify local protocol behavior in Android and RN command help --- dev.yml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/dev.yml b/dev.yml index 95351faba..19641bdaa 100644 --- a/dev.yml +++ b/dev.yml @@ -307,6 +307,17 @@ commands: subcommands: local: desc: Run an Android command against in-repo protocol sources + long_desc: | + Android commands use the published ECP dependency by default. Use this wrapper + when developing Android Kit against unreleased ECP changes. It sets + useLocalProtocol=true for the command; remote publication remains blocked. + + Examples: + dev android local test + dev android local build samples + + For React Native, use `dev rn android --local` or `dev rn test android --local`. + Those commands already enable local ECP when publishing Kit and ECP to Maven Local. syntax: " [args...]" run: | if [ "$#" -eq 0 ]; then @@ -710,8 +721,9 @@ commands: `checkoutKit.nativeSdkVersions.android` in the React Native module, matching CI. --local - Resolve the in-repo Android SDK from Maven Local instead. Only for - unreleased native API that this PR changes. + Publish the in-repo Android Kit and ECP sources to Maven Local, then + resolve both from there. Enables useLocalProtocol=true automatically. + Only for unreleased native API that this PR changes. syntax: optional: --local run: | @@ -880,7 +892,8 @@ commands: Builds and runs the Android sample app on an emulator. --local - Build against in-repo SDK sources (publishes a local Maven snapshot first). + Publish the in-repo Android Kit and ECP sources to Maven Local, then build + against them. Enables useLocalProtocol=true automatically. Only for unreleased native API that this PR adds; CI resolves published Maven artifacts, so the PR stays red until release. syntax: From aed326b9a9852069a32370a6299851ae55a4337a Mon Sep 17 00:00:00 2001 From: Daniel Kift Date: Mon, 28 Sep 2026 19:30:29 +0100 Subject: [PATCH 5/5] Use --local for Android protocol development --- .github/CONTRIBUTING.md | 12 +-- dev.yml | 101 +++++++++++++----- platforms/android/AGENTS.md | 2 +- platforms/android/README.md | 2 +- .../android/scripts/parse_local_protocol_flag | 22 ++++ 5 files changed, 102 insertions(+), 37 deletions(-) create mode 100755 platforms/android/scripts/parse_local_protocol_flag diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e8a7ff45d..a02bb3497 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -230,8 +230,8 @@ the RN release. #### Developing changes across packages -Develop the changes together on stacked branches using `dev android local test` -or `dev android local api check` for Kit/ECP, and `dev rn test android --local` +Develop the changes together on stacked branches using `dev android test --local` +or `dev android api check --local` for Kit/ECP, and `dev rn test android --local` or `dev rn android --local` for RN/native changes. These explicit overrides allow early integration before publication. On the RN development branch, set `checkoutKit.nativeSdkVersions.android` to the in-repo `checkoutKitAndroid` version @@ -372,10 +372,10 @@ substitution or version changes introduced by dependency resolution. Explicit lo mode skips this assertion; remote publication still rejects local mode. Changes to protocol source are tested separately with `dev protocol test kotlin`. -For joint development against unreleased protocol changes, run `dev android local test`, -`dev android local build`, or `dev android local start`. The `local` prefix works with -any Android command and sets the Gradle property `useLocalProtocol=true` for that -invocation. When running Gradle directly, pass `-PuseLocalProtocol=true`. +For joint development against unreleased protocol changes, run `dev android test --local`, +`dev android build --local`, or `dev android start --local`. The `--local` flag works with +Android build, test, lint, format, check, and API commands and sets the Gradle property +`useLocalProtocol=true` for that invocation. When running Gradle directly, pass `-PuseLocalProtocol=true`. Remote publication rejects local mode; React Native's explicit `--local` flow can still publish both artifacts to Maven Local. Kit changes that need a new protocol version become mergeable once that version is published and normal CI passes. diff --git a/dev.yml b/dev.yml index 19641bdaa..ae6690e2c 100644 --- a/dev.yml +++ b/dev.yml @@ -305,38 +305,35 @@ commands: aliases: ["kotlin"] desc: "Android Checkout Kit commands" subcommands: - local: - desc: Run an Android command against in-repo protocol sources - long_desc: | - Android commands use the published ECP dependency by default. Use this wrapper - when developing Android Kit against unreleased ECP changes. It sets - useLocalProtocol=true for the command; remote publication remains blocked. - - Examples: - dev android local test - dev android local build samples - - For React Native, use `dev rn android --local` or `dev rn test android --local`. - Those commands already enable local ECP when publishing Kit and ECP to Maven Local. - syntax: " [args...]" - run: | - if [ "$#" -eq 0 ]; then - echo "Usage: dev android local [args...]" >&2 - exit 1 - fi - ORG_GRADLE_PROJECT_useLocalProtocol=true /opt/dev/bin/dev android "$@" - build: desc: Build the library - run: platforms/android/gradlew -p platforms/android :lib:build + long_desc: | + --local + Build against the in-repo ECP source rather than the published dependency. + syntax: + optional: --local + run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" + platforms/android/gradlew -p platforms/android :lib:build subcommands: samples: desc: Build all sample applications - run: platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo build + syntax: + optional: --local + run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" + platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo build start: desc: Build the android sample app and install it to the booted emulator - run: platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo installDebug + syntax: + optional: --local + run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" + platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo installDebug e2e: desc: Run Android sample end-to-end tests @@ -355,7 +352,11 @@ commands: test: desc: Run all library and demo app tests + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" set -e scripts/check_storefront_env platforms/android/gradlew -p platforms/android clean test --console=plain @@ -363,21 +364,39 @@ commands: subcommands: samples: desc: Run JVM tests for the Checkout Kit Android sample app - run: platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :app:testDebugUnitTest --console=plain + syntax: + optional: --local + run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" + platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :app:testDebugUnitTest --console=plain specific: desc: Run specific test class - syntax: (e.g. CheckoutBridgeTest) - run: platforms/android/gradlew -p platforms/android :lib:testDebugUnitTest --tests "$1" + syntax: [--local] (e.g. CheckoutBridgeTest) + run: | + test_class="$1" + shift + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" + platforms/android/gradlew -p platforms/android :lib:testDebugUnitTest --tests "$test_class" demo: desc: Run demo app unit tests + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" scripts/check_storefront_env platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :app:testDebugUnitTest --console=plain lint: desc: Check code style and lint (detekt + Android lint, including the sample) aliases: [style] + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" scripts/check_storefront_env platforms/android/gradlew -p platforms/android detekt lintRelease platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :app:detekt :app:lintRelease @@ -385,27 +404,43 @@ commands: format: desc: Auto-format and apply safe lint autocorrections, including the sample aliases: [fix] + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" scripts/check_storefront_env platforms/android/gradlew -p platforms/android detekt --auto-correct platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :app:detekt --auto-correct check: desc: Run all Android checks (detekt, Android lint) + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" set -e - /opt/dev/bin/dev android check detekt - /opt/dev/bin/dev android check android-lint + /opt/dev/bin/dev android check detekt "$@" + /opt/dev/bin/dev android check android-lint "$@" subcommands: detekt: desc: Run library static analysis and sample formatting/linting checks + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" scripts/check_storefront_env platforms/android/gradlew -p platforms/android detekt platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :app:detekt android-lint: desc: Run Android lint, including the sample + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" scripts/check_storefront_env platforms/android/gradlew -p platforms/android lintRelease platforms/android/samples/CheckoutKitAndroidDemo/gradlew -p platforms/android/samples/CheckoutKitAndroidDemo :app:lintRelease @@ -453,13 +488,21 @@ commands: subcommands: check: desc: Verify public APIs match the committed baselines + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" set -e platforms/android/gradlew -p platforms/android :lib:apiCheck protocol/languages/kotlin/gradlew -p protocol/languages/kotlin :embedded-checkout-protocol:apiCheck dump: desc: Regenerate the baselines after intentional public API changes + syntax: + optional: --local run: | + source platforms/android/scripts/parse_local_protocol_flag + parse_local_protocol_flag "$@" set -e platforms/android/gradlew -p platforms/android :lib:apiDump protocol/languages/kotlin/gradlew -p protocol/languages/kotlin :embedded-checkout-protocol:apiDump diff --git a/platforms/android/AGENTS.md b/platforms/android/AGENTS.md index 3668549f3..78721c1ef 100644 --- a/platforms/android/AGENTS.md +++ b/platforms/android/AGENTS.md @@ -14,7 +14,7 @@ The main modules are: The sample is a separate Gradle build (`samples/CheckoutKitAndroidDemo/settings.gradle`) that includes `:lib` from source. The sample's `gradle.properties` and Gradle wrapper are independent of the Android root's. The standalone Kotlin protocol Gradle root also has its own wrapper at `../../protocol/languages/kotlin/gradlew`; keep its Gradle version aligned with the Android root wrapper. -The library and sample resolve the pinned protocol artifact from Maven Central by default, including in CI. To develop against unreleased protocol source, use `dev android local ` (for example `dev android local test`) or pass `-PuseLocalProtocol=true` to Gradle. Only this explicit mode includes `:embedded-checkout-protocol` in the Android builds. Remote Kit publication rejects local mode; `publishToMavenLocal` remains available for React Native's explicit `--local` workflow. Test protocol source independently with `dev protocol test kotlin`. +The library and sample resolve the pinned protocol artifact from Maven Central by default, including in CI. To develop against unreleased protocol source, pass `--local` to an Android build, test, lint, format, check, or API command (for example `dev android test --local`) or pass `-PuseLocalProtocol=true` to Gradle. Only this explicit mode includes `:embedded-checkout-protocol` in the Android builds. Remote Kit publication rejects local mode; `publishToMavenLocal` remains available for React Native's explicit `--local` workflow. Test protocol source independently with `dev protocol test kotlin`. ## Where to make changes diff --git a/platforms/android/README.md b/platforms/android/README.md index 0edaaea58..d4cd50b2d 100644 --- a/platforms/android/README.md +++ b/platforms/android/README.md @@ -749,7 +749,7 @@ See [samples](samples/README.md). `CheckoutKitAndroidDemo` demonstrates an Apoll See [CONTRIBUTING](../../.github/CONTRIBUTING.md). The SDK and sample use the published ECP dependency by default. For joint protocol -development, use `dev android local ` or Gradle's `-PuseLocalProtocol=true`. +development, use an Android command's `--local` flag (for example, `dev android test --local`) or Gradle's `-PuseLocalProtocol=true`. Changes spanning the protocol, Android SDK, and React Native follow the [ECP → Android → RN release sequence](../../.github/CONTRIBUTING.md#coordinating-ecp-android-and-react-native-releases), with normal CI against each published dependency before releasing its consumer. diff --git a/platforms/android/scripts/parse_local_protocol_flag b/platforms/android/scripts/parse_local_protocol_flag new file mode 100755 index 000000000..25aa6373d --- /dev/null +++ b/platforms/android/scripts/parse_local_protocol_flag @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# shellcheck shell=bash +# Source from dev.yml commands that support the optional --local flag. +# It is intentionally limited to this one flag so it cannot leak to Gradle, +# test selectors, or other wrapped commands. +parse_local_protocol_flag() { + case "$#" in + 0) ;; + 1) + if [ "$1" = "--local" ]; then + export ORG_GRADLE_PROJECT_useLocalProtocol=true + else + echo "Only the optional --local flag is supported." >&2 + exit 1 + fi + ;; + *) + echo "Only the optional --local flag is supported." >&2 + exit 1 + ;; + esac +}