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
25 changes: 21 additions & 4 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---
Expand Down Expand Up @@ -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.**

---
Expand Down Expand Up @@ -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.
30 changes: 30 additions & 0 deletions .github/scripts/package-json-version
Original file line number Diff line number Diff line change
@@ -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 <package.json>");
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);
24 changes: 22 additions & 2 deletions .github/scripts/validate-release-version
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Usage: validate-release-version <platform> [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
}

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -110,8 +127,10 @@ fi

if [[ "$VERSION" == *-* ]]; then
PRERELEASE="true"
NPM_TAG="next"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this matches what the web workflow does too

else
PRERELEASE="false"
NPM_TAG="latest"
fi

{
Expand All @@ -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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 23 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:"
Expand All @@ -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
Expand All @@ -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 }}
Expand All @@ -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 }}
Expand All @@ -154,8 +156,7 @@ jobs:
cat >> "$GITHUB_STEP_SUMMARY" <<SUMMARY
## Release plan

- Dry run: `${DRY_RUN}`
- Draft release: `${DRAFT}`
- Mode: \`${MODE}\`
- Platform: ${DISPLAY_PLATFORM}
- Version: ${VERSION}
- Tag: \`${TAG}\`
Expand Down
137 changes: 137 additions & 0 deletions .github/workflows/rn-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
name: React Native — Publish to npm

on:
release:
types:
- published
workflow_dispatch:

permissions:
contents: read
id-token: write

concurrency:
group: react-native-publish
cancel-in-progress: false

jobs:
release:
name: Publish @shopify/checkout-kit-react-native to npm
if: |
(github.event_name == 'release' && startsWith(github.event.release.tag_name, 'react-native/'))
|| github.event_name == 'workflow_dispatch'
environment:
name: npm-react-native
url: https://www.npmjs.com/package/@shopify/checkout-kit-react-native
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: platforms/react-native

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Validate release tag matches package.json version
id: release
working-directory: ${{ github.workspace }}
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" = "release" ]; then
TAG="$RELEASE_TAG"
else
TAG="$GITHUB_REF_NAME"
fi

.github/scripts/validate-release-version "React Native" "" "$TAG" >> "$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 }}

Loading
Loading