diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 62f9e756..53c024ef 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -94,8 +94,8 @@ Once merged, run the [Release package workflow](../../actions/workflows/release. 1. Select `iOS` as the platform. 2. Enter the expected version. The workflow reads the SDK version from the checked-in files and fails if the typed version does not match. -3. Leave `dry-run` enabled first to review the release plan. -4. Rerun with `dry-run` disabled. By default this creates a draft GitHub Release with the bare semver tag (e.g. `3.8.1`) for human review. +3. Select `Dry run` first to review the release plan without creating a release. +4. Rerun with `Draft release` to create a draft GitHub Release with the bare semver tag (e.g. `3.8.1`) for human review. 5. Publish the draft release when ready. Publishing the draft kicks off the [Swift publish workflow](../../actions/workflows/swift-publish.yml), which publishes the new version to CocoaPods. --- @@ -142,8 +142,8 @@ Once merged, run the [Release package workflow](../../actions/workflows/release. 1. Select `Android` as the platform. 2. Enter the expected version. The workflow reads the SDK version from `platforms/android/lib/build.gradle` and fails if the typed version does not match. -3. Leave `dry-run` enabled first to review the release plan. -4. Rerun with `dry-run` disabled. By default this creates a draft GitHub Release with the `android/`-prefixed tag (e.g. `android/3.0.1`) for human review. +3. Select `Dry run` first to review the release plan without creating a release. +4. Rerun with `Draft release` to create a draft GitHub Release with the `android/`-prefixed tag (e.g. `android/3.0.1`) for human review. 5. Publish the draft release when ready. Publishing the draft kicks off the [Android publish workflow](../../actions/workflows/android-publish.yml). **A manual approval by a maintainer is required before publication to Maven Central.** --- @@ -176,3 +176,20 @@ If your change intentionally modifies the public API: 3. Commit the updated `.api.md` file in the same PR. If you did *not* intend to change public API and `api:check` is failing, the diff shows what your change inadvertently affected — treat it as a signal that something in your PR has consumer-visible impact. + +### Releasing a new React Native version + +Open a pull request with the following changes: + +1. Bump the `version` in `platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json`. +2. Add an entry to the React Native changelog. + +Supported release versions are `X.Y.Z` and prerelease versions are `X.Y.Z-{alpha|beta|rc}.N`. + +Once merged, run the [Release package workflow](../../actions/workflows/release.yml): + +1. Select `React Native` as the platform. +2. Enter the expected version. The workflow reads the SDK version from `platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json` and fails if the typed version does not match. +3. Select `Dry run` first to review the release plan without creating a release. +4. Rerun with `Draft release` to create a draft GitHub Release with the `react-native/`-prefixed tag (e.g. `react-native/4.0.1`) for human review. +5. Publish the draft release when ready. Publishing the draft kicks off the [React Native publish workflow](../../actions/workflows/rn-publish.yml), which publishes `@shopify/checkout-kit-react-native` to npm. diff --git a/.github/scripts/package-json-version b/.github/scripts/package-json-version new file mode 100755 index 00000000..5e815927 --- /dev/null +++ b/.github/scripts/package-json-version @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); + +const file = process.argv[2]; + +if (!file) { + console.error("Usage: package-json-version "); + process.exit(2); +} + +if (!fs.existsSync(file)) { + console.error(`::error file=${file}::Version source file does not exist.`); + process.exit(1); +} + +let pkg; +try { + pkg = JSON.parse(fs.readFileSync(file, "utf8")); +} catch (error) { + console.error(`::error file=${file}::Could not parse JSON: ${error.message}`); + process.exit(1); +} + +if (typeof pkg.version !== "string" || pkg.version.length === 0) { + console.error(`::error file=${file}::Could not extract version.`); + process.exit(1); +} + +process.stdout.write(pkg.version); diff --git a/.github/scripts/validate-release-version b/.github/scripts/validate-release-version index 1db0866e..5d4cc326 100755 --- a/.github/scripts/validate-release-version +++ b/.github/scripts/validate-release-version @@ -8,7 +8,7 @@ Usage: validate-release-version [expected-version] [expected-tag] Validates the selected SDK's checked-in version declarations, optional user version input, optional git tag, and prints GitHub Actions outputs to stdout. -Platforms: iOS, Android +Platforms: iOS, Android, React Native USAGE } @@ -42,6 +42,12 @@ extract_first_match() { printf '%s\n' "$value" } +json_version() { + local file="$1" + + .github/scripts/package-json-version "$file" +} + check_same_version() { local expected="$1" local file="$2" @@ -84,8 +90,19 @@ case "$PLATFORM_INPUT" in VERSION=$(extract_first_match "$ANDROID_VERSION_FILE" 's/^[[:space:]]*def[[:space:]]+versionName[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p') ;; + "React Native"|react-native|ReactNative|rn|RN) + PLATFORM="react-native" + DISPLAY_PLATFORM="React Native" + RELEASE_TITLE_PREFIX="React Native" + TAG_PREFIX="react-native/" + PUBLISH_WORKFLOW="rn-publish.yml" + + RN_PACKAGE_FILE="platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json" + VERSION=$(json_version "$RN_PACKAGE_FILE") + ;; + *) - echo "::error::Unsupported platform '$PLATFORM_INPUT'. Expected one of: iOS, Android." >&2 + echo "::error::Unsupported platform '$PLATFORM_INPUT'. Expected one of: iOS, Android, React Native." >&2 exit 1 ;; esac @@ -110,8 +127,10 @@ fi if [[ "$VERSION" == *-* ]]; then PRERELEASE="true" + NPM_TAG="next" else PRERELEASE="false" + NPM_TAG="latest" fi { @@ -122,6 +141,7 @@ fi echo "release_title=$RELEASE_TITLE" echo "publish_workflow=$PUBLISH_WORKFLOW" echo "prerelease=$PRERELEASE" + echo "npm_tag=$NPM_TAG" } echo "✓ ${DISPLAY_PLATFORM} version '$VERSION' validates and maps to tag '$TAG'." >&2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7750a65..bbbdf6e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,11 @@ jobs: - '.github/workflows/rn-build-ios.yml' - '.github/workflows/rn-check-packed-files.yml' - '.github/workflows/rn-lint.yml' + - '.github/workflows/rn-publish.yml' + - '.github/workflows/release.yml' + - '.github/scripts/package-json-version' + - '.github/scripts/validate-release-version' + - '.github/actions/setup/**' - '.github/workflows/breaking-changes.yml' - '.github/workflows/ci.yml' web: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e00b060..829097d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,20 +10,20 @@ on: options: - iOS - Android + - React Native version: description: Expected SDK version. Must match the checked-in SDK version for the selected platform. required: true type: string - dry-run: - description: Dry run + mode: + description: Release mode required: false - type: boolean - default: true - draft: - description: Draft release - required: false - type: boolean - default: true + type: choice + default: Dry run + options: + - Dry run + - Draft release + - Production release permissions: contents: write @@ -81,14 +81,13 @@ jobs: - name: Print release plan env: - DRY_RUN: ${{ inputs['dry-run'] && 'true' || 'false' }} + MODE: ${{ inputs.mode }} DISPLAY_PLATFORM: ${{ steps.release.outputs.display_platform }} VERSION: ${{ steps.release.outputs.version }} TAG: ${{ steps.release.outputs.tag }} RELEASE_TITLE: ${{ steps.release.outputs.release_title }} PRERELEASE: ${{ steps.release.outputs.prerelease }} PUBLISH_WORKFLOW: ${{ steps.release.outputs.publish_workflow }} - DRAFT: ${{ inputs.draft && 'true' || 'false' }} run: | set -euo pipefail echo "Release plan:" @@ -98,21 +97,25 @@ jobs: echo " Title: ${RELEASE_TITLE}" echo " Prerelease: ${PRERELEASE}" echo " Publish workflow: ${PUBLISH_WORKFLOW}" - echo " Dry run: ${DRY_RUN}" - echo " Draft release: ${DRAFT}" - if [ "$DRY_RUN" = "false" ] && [ "$DRAFT" = "true" ]; then + echo " Mode: ${MODE}" + if [ "$MODE" = "Dry run" ]; then + echo " Release creation: skipped" + echo " Publish dispatch: skipped" + elif [ "$MODE" = "Draft release" ]; then + echo " Release creation: draft GitHub Release" echo " Publish dispatch: skipped until the draft release is manually published" - elif [ "$DRY_RUN" = "false" ]; then + else + echo " Release creation: published GitHub Release" echo " Publish dispatch: ${PUBLISH_WORKFLOW} will be dispatched after release creation" fi - name: Create GitHub Release - if: ${{ !inputs['dry-run'] }} + if: ${{ inputs.mode != 'Dry run' }} env: TAG: ${{ steps.release.outputs.tag }} RELEASE_TITLE: ${{ steps.release.outputs.release_title }} PRERELEASE: ${{ steps.release.outputs.prerelease }} - DRAFT: ${{ inputs.draft && 'true' || 'false' }} + DRAFT: ${{ inputs.mode == 'Draft release' && 'true' || 'false' }} GH_TOKEN: ${{ github.token }} run: | set -euo pipefail @@ -131,7 +134,7 @@ jobs: gh release create "${args[@]}" - name: Dispatch publish workflow - if: ${{ !inputs['dry-run'] && !inputs.draft }} + if: ${{ inputs.mode == 'Production release' }} env: TAG: ${{ steps.release.outputs.tag }} PUBLISH_WORKFLOW: ${{ steps.release.outputs.publish_workflow }} @@ -143,8 +146,7 @@ jobs: - name: Summary env: - DRY_RUN: ${{ inputs['dry-run'] && 'true' || 'false' }} - DRAFT: ${{ inputs.draft && 'true' || 'false' }} + MODE: ${{ inputs.mode }} DISPLAY_PLATFORM: ${{ steps.release.outputs.display_platform }} VERSION: ${{ steps.release.outputs.version }} TAG: ${{ steps.release.outputs.tag }} @@ -154,8 +156,7 @@ jobs: cat >> "$GITHUB_STEP_SUMMARY" <> "$GITHUB_OUTPUT" + + # `ignore-scripts` prevents dependency postinstall scripts from running in + # this privileged npm publishing job. + - name: Setup Node.js, pnpm, and install dependencies + uses: ./.github/actions/setup + with: + node-version-file: platforms/react-native/package.json + cache-dependency-path: platforms/react-native/pnpm-lock.yaml + package-json-file: platforms/react-native/package.json + working-directory: platforms/react-native + ignore-scripts: "true" + + - name: Verify version is not already published + run: | + set -euo pipefail + NAME=$(node -p "require('./modules/@shopify/checkout-kit-react-native/package.json').name") + VERSION=$(node -p "require('./modules/@shopify/checkout-kit-react-native/package.json').version") + ENCODED_NAME=$(node -p "encodeURIComponent(require('./modules/@shopify/checkout-kit-react-native/package.json').name)") + URL="https://registry.npmjs.org/${ENCODED_NAME}/${VERSION}" + if curl -fs "$URL" > /dev/null; then + echo "::error::${NAME}@${VERSION} is already published on npm. Bump platforms/react-native/modules/@shopify/checkout-kit-react-native/package.json before re-running." + exit 1 + fi + echo "::notice::${NAME}@${VERSION} is not yet on npm — safe to proceed." + + - name: Build package + run: | + set -euo pipefail + cp README.md modules/@shopify/checkout-kit-react-native/README.md + pnpm module clean + pnpm module build + + - name: Pack and inspect contents + run: | + set -euo pipefail + mkdir -p /tmp/react-native-publish + cd modules/@shopify/checkout-kit-react-native + pnpm pack --dry-run + pnpm pack --pack-destination /tmp/react-native-publish + echo "Tarball contents:" + tar -tzf /tmp/react-native-publish/*.tgz | sort + + - name: Verify packed manifest has no workspace dependencies + run: | + set -euo pipefail + tar -xOf /tmp/react-native-publish/*.tgz package/package.json > /tmp/react-native-publish/package.json + node <<'NODE' + const pkg = require('/tmp/react-native-publish/package.json'); + const sections = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']; + const workspaceDependencies = []; + for (const section of sections) { + for (const [name, version] of Object.entries(pkg[section] ?? {})) { + if (typeof version === 'string' && version.startsWith('workspace:')) { + workspaceDependencies.push(`${section}.${name}=${version}`); + } + } + } + if (workspaceDependencies.length > 0) { + console.error(`::error::Packed package still contains workspace dependencies: ${workspaceDependencies.join(', ')}`); + process.exit(1); + } + console.log('Packed package manifest has no workspace: dependencies.'); + NODE + + - name: Print npm auth mode + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + if [ -n "${NPM_TOKEN:-}" ]; then + echo "::notice::NPM_TOKEN is present — bootstrap/token publish path will be used." + pnpm whoami + else + echo "::notice::NPM_TOKEN is not present — relying on npm trusted publishing/OIDC." + fi + + - name: Publish to npm + run: | + set -euo pipefail + cd modules/@shopify/checkout-kit-react-native + pnpm publish --no-git-checks --ignore-scripts --access public --tag "$NPM_TAG" --provenance + env: + NPM_TAG: ${{ steps.release.outputs.npm_tag }} + NPM_CONFIG_PROVENANCE: "true" + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + diff --git a/platforms/react-native/docs/contributing/release.md b/platforms/react-native/docs/contributing/release.md index 56d3dcaa..7fb02fc9 100644 --- a/platforms/react-native/docs/contributing/release.md +++ b/platforms/react-native/docs/contributing/release.md @@ -10,12 +10,26 @@ following steps: appropriate value. 2. Add a [Changelog](./CHANGELOG.md) entry. 3. Merge your PR to `main`. -4. Create a [Release](/releases) for your new version. +4. Run the [Release package workflow](/actions/workflows/release.yml). -Creating and publishing a Github release with begin the automated process of -publishing the latest version of the package to NPM. It will clean the module -folder, build a new version, run `npm pack --dry-run` to verify the contents and -publish to the NPM registry. +Supported release versions are: -You can follow the release action process via -https://github.com/Shopify/checkout-kit/actions/workflows/publish.yml. +- Stable: `X.Y.Z` +- Prerelease: `X.Y.Z-{alpha|beta|rc}.N` + +The release workflow reads the version from +`modules/@shopify/checkout-kit-react-native/package.json`, validates it, and +creates the correctly namespaced `react-native/` tag (for example, +`react-native/4.0.1`). The manually entered workflow version is only a safety +check; it must match the package version exactly. + +Select `Dry run` on the first run to review the planned tag without creating a +release. Rerun with `Draft release` to create a draft GitHub Release 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. + +You can follow the publish action process via +https://github.com/Shopify/checkout-kit/actions/workflows/rn-publish.yml. diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/RNShopifyCheckoutKit.podspec b/platforms/react-native/modules/@shopify/checkout-kit-react-native/RNShopifyCheckoutKit.podspec index 48b6dfc8..939af1f0 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/RNShopifyCheckoutKit.podspec +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/RNShopifyCheckoutKit.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |s| s.authors = package["author"] s.platforms = { :ios => "13.0" } - s.source = { :git => "https://github.com/Shopify/checkout-kit.git", :tag => "#{s.version}" } + s.source = { :git => "https://github.com/Shopify/checkout-kit.git", :tag => "react-native/#{s.version}" } s.source_files = "ios/*.{h,m,mm,swift}" # `ios/Package.swift` is the manifest for the nested SwiftPM test package diff --git a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/build.gradle b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/build.gradle index 1107c001..a3463030 100644 --- a/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/build.gradle +++ b/platforms/react-native/modules/@shopify/checkout-kit-react-native/android/build.gradle @@ -118,12 +118,20 @@ if (shopifySdkVersion == null || shopifySdkVersion.trim().isEmpty()) { def shopifySdkArtifact = "com.shopify:checkout-kit:$shopifySdkVersion" repositories { - mavenLocal() + if (useLocalSdk) { + exclusiveContent { + forRepository { + mavenLocal() + } + filter { + includeModule("com.shopify", "checkout-kit") + } + } + } mavenCentral() google() } - dependencies { // For < 0.71, this will be from the local maven repo // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin diff --git a/platforms/react-native/sample/android/build.gradle b/platforms/react-native/sample/android/build.gradle index 63080b75..560340ca 100644 --- a/platforms/react-native/sample/android/build.gradle +++ b/platforms/react-native/sample/android/build.gradle @@ -9,7 +9,6 @@ buildscript { kotlinVersion = "2.1.20" } repositories { - mavenLocal() google() mavenCentral() } @@ -30,9 +29,20 @@ def loadProperties() { apply plugin: "com.facebook.react.rootproject" +def useLocalSdk = (System.getenv("USE_LOCAL_SDK") ?: "0") == "1" + allprojects { repositories { - mavenLocal() + if (useLocalSdk) { + exclusiveContent { + forRepository { + mavenLocal() + } + filter { + includeModule("com.shopify", "checkout-kit") + } + } + } google() mavenCentral() } diff --git a/platforms/react-native/sample/ios/Podfile.lock b/platforms/react-native/sample/ios/Podfile.lock index 4b4b5ff0..91e87364 100644 --- a/platforms/react-native/sample/ios/Podfile.lock +++ b/platforms/react-native/sample/ios/Podfile.lock @@ -2996,7 +2996,7 @@ SPEC CHECKSUMS: RNGestureHandler: eeb622199ef1fb3a076243131095df1c797072f0 RNReanimated: 237d420b7bb4378ef1dacc7d7a5c674fddb4b5d2 RNScreens: 3fc29af06302e1f1c18a7829fe57cbc2c0259912 - RNShopifyCheckoutKit: 23f4881a5a839cc7b5e0840ccfec3b317c3c3bd6 + RNShopifyCheckoutKit: e19eba6efb68ed31936d7e7b413a02fdb104a053 RNVectorIcons: be4d047a76ad307ffe54732208fb0498fcb8477f ShopifyCheckoutKit: ffd719db529ac48907536d855182d7c57c85659f SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748