Skip to content
Merged
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
33 changes: 19 additions & 14 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,19 @@ When `dev swift api check` fails, it prints both the unified diff and a `swift-a
Open a pull request with the following changes:

1. Bump the package version in `platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift`.
2. Bump the podspec version in `ShopifyCheckoutKit.podspec` (at the repo root).
3. Add an entry to the top of `platforms/swift/CHANGELOG.md`.
2. Bump the metadata version in `platforms/swift/Sources/ShopifyCheckoutKit/MetaData.swift`.
3. Bump the podspec version in `ShopifyCheckoutKit.podspec` (at the repo root).
4. Add an entry to the top of `platforms/swift/CHANGELOG.md`.

Once merged, draft a release on GitHub:
All Swift version declarations must match exactly. Supported release versions are `X.Y.Z` and prerelease versions are `X.Y.Z-{alpha|beta|rc}.N`.

1. Create a tag with the bare semver name (e.g. `3.8.1`) — Swift releases use bare semver so SwiftPM consumers can resolve them with `from:` constraints.
2. Use the same tag as the release name.
3. Document the changes since the previous release in the description.
4. Check "Set as the latest release".
5. Click "Publish release". This kicks off the [Swift publish workflow](../../actions/workflows/swift-publish.yml) which publishes the new version to CocoaPods.
Once merged, run the [Release package workflow](../../actions/workflows/release.yml):

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.
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 @@ -131,13 +134,15 @@ Open a pull request with the following changes:
1. Bump the `versionName` in `platforms/android/lib/build.gradle`.
2. Add an entry to the top of `platforms/android/CHANGELOG.md`.

Once merged, draft a release on GitHub:
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. Create a tag prefixed with `android/` (e.g. `android/3.0.1`) — Android releases use the `android/` prefix so the Maven publish workflow can distinguish them from Swift releases.
2. Use the same tag as the release name.
3. Document the changes since the previous release in the description.
4. Check "Set as the latest release".
5. Click "Publish release". This kicks off the [Android publish workflow](../../actions/workflows/android-publish.yml). **A manual approval by a maintainer is required before publication to Maven Central.**
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.
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
123 changes: 123 additions & 0 deletions .github/scripts/validate-release-version
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
cat >&2 <<'USAGE'
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
USAGE
}

if [ "$#" -lt 1 ] || [ "$#" -gt 3 ]; then
usage
exit 2
fi

PLATFORM_INPUT="$1"
EXPECTED_VERSION="${2:-}"
EXPECTED_TAG="${3:-}"

SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(alpha|beta|rc)\.(0|[1-9][0-9]*))?$'

extract_first_match() {
local file="$1"
local regex="$2"
local value

if [ ! -f "$file" ]; then
echo "::error file=$file::Version source file does not exist." >&2
exit 1
fi

value=$(sed -nE "$regex" "$file" | head -n 1)
if [ -z "$value" ]; then
echo "::error file=$file::Could not extract version." >&2
exit 1
fi

printf '%s\n' "$value"
}

check_same_version() {
local expected="$1"
local file="$2"
local actual="$3"

if [ "$actual" != "$expected" ]; then
echo "::error file=$file::Version '$actual' does not match source version '$expected'." >&2
exit 1
fi
}

case "$PLATFORM_INPUT" in
iOS|ios|swift|Swift)
PLATFORM="ios"
DISPLAY_PLATFORM="iOS"
TAG_PREFIX=""
PUBLISH_WORKFLOW="swift-publish.yml"

PODSPEC_FILE="ShopifyCheckoutKit.podspec"
SWIFT_VERSION_FILE="platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift"
SWIFT_METADATA_FILE="platforms/swift/Sources/ShopifyCheckoutKit/MetaData.swift"

VERSION=$(extract_first_match "$PODSPEC_FILE" 's/^[[:space:]]*s\.version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p')
SWIFT_VERSION=$(extract_first_match "$SWIFT_VERSION_FILE" 's/^[[:space:]]*public[[:space:]]+let[[:space:]]+version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p')
SWIFT_METADATA_VERSION=$(extract_first_match "$SWIFT_METADATA_FILE" 's/^[[:space:]]*package[[:space:]]+static[[:space:]]+let[[:space:]]+version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p')

check_same_version "$VERSION" "$SWIFT_VERSION_FILE" "$SWIFT_VERSION"
check_same_version "$VERSION" "$SWIFT_METADATA_FILE" "$SWIFT_METADATA_VERSION"
;;

Android|android)
PLATFORM="android"
DISPLAY_PLATFORM="Android"
TAG_PREFIX="android/"
PUBLISH_WORKFLOW="android-publish.yml"

ANDROID_VERSION_FILE="platforms/android/lib/build.gradle"
VERSION=$(extract_first_match "$ANDROID_VERSION_FILE" 's/^[[:space:]]*def[[:space:]]+versionName[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p')
;;

*)
echo "::error::Unsupported platform '$PLATFORM_INPUT'. Expected one of: iOS, Android." >&2
exit 1
;;
esac

if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
echo "::error::${DISPLAY_PLATFORM} SDK version '$VERSION' is invalid. Expected X.Y.Z or X.Y.Z-{alpha|beta|rc}.N." >&2
exit 1
fi

if [ -n "$EXPECTED_VERSION" ] && [ "$EXPECTED_VERSION" != "$VERSION" ]; then
echo "::error::Requested version '$EXPECTED_VERSION' does not match ${DISPLAY_PLATFORM} SDK version '$VERSION'. Bump the SDK version in a PR first, or rerun with '$VERSION'." >&2
exit 1
fi

TAG="${TAG_PREFIX}${VERSION}"

if [ -n "$EXPECTED_TAG" ] && [ "$EXPECTED_TAG" != "$TAG" ]; then
echo "::error::Git tag '$EXPECTED_TAG' does not match ${DISPLAY_PLATFORM} SDK version '$VERSION'. Expected tag '$TAG'." >&2
exit 1
fi

if [[ "$VERSION" == *-* ]]; then
PRERELEASE="true"
else
PRERELEASE="false"
fi

{
echo "platform=$PLATFORM"
echo "display_platform=$DISPLAY_PLATFORM"
echo "version=$VERSION"
echo "tag=$TAG"
echo "publish_workflow=$PUBLISH_WORKFLOW"
echo "prerelease=$PRERELEASE"
}

echo "✓ ${DISPLAY_PLATFORM} version '$VERSION' validates and maps to tag '$TAG'." >&2
14 changes: 14 additions & 0 deletions .github/workflows/android-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ jobs:
with:
submodules: true

- name: Validate release tag matches Android version
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 Android "" "$TAG"

- name: Install JDK 1.17
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
Expand Down
157 changes: 157 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Release package

on:
workflow_dispatch:
inputs:
platform:
description: Platform to release
required: true
type: choice
options:
- iOS
- Android
version:
description: Expected SDK version. Must match the checked-in SDK version for the selected platform.
required: true
type: string
dry-run:
description: Validate and print the release plan, but do not create a release or dispatch publishing.
required: false
type: boolean
default: true
draft:
description: Create a draft GitHub Release for human review. Publish the draft manually to start publishing.
required: false
type: boolean
default: true

permissions:
contents: write
actions: write

concurrency:
group: release-${{ inputs.platform }}
cancel-in-progress: false

jobs:
release:
name: Release ${{ inputs.platform }} ${{ inputs.version }}
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Validate requested release
id: release
env:
PLATFORM: ${{ inputs.platform }}
EXPECTED_VERSION: ${{ inputs.version }}
run: .github/scripts/validate-release-version "$PLATFORM" "$EXPECTED_VERSION" >> "$GITHUB_OUTPUT"

- name: Require main branch for stable releases
if: steps.release.outputs.prerelease == 'false' && github.ref != 'refs/heads/main'
env:
REF: ${{ github.ref }}
run: |
echo "::error::Stable releases must be created from the main branch. Current ref: $REF"
exit 1

- name: Check tag and release do not already exist
env:
TAG: ${{ steps.release.outputs.tag }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
echo "::error::Tag '${TAG}' already exists. Refusing to create a duplicate release."
exit 1
fi

if gh release view "$TAG" >/dev/null 2>&1; then
echo "::error::GitHub Release '${TAG}' already exists."
exit 1
fi

echo "::notice::Tag '${TAG}' and release '${TAG}' do not exist yet."

- name: Print release plan
env:
DRY_RUN: ${{ inputs.dry-run }}
DISPLAY_PLATFORM: ${{ steps.release.outputs.display_platform }}
VERSION: ${{ steps.release.outputs.version }}
TAG: ${{ steps.release.outputs.tag }}
PRERELEASE: ${{ steps.release.outputs.prerelease }}
PUBLISH_WORKFLOW: ${{ steps.release.outputs.publish_workflow }}
DRAFT: ${{ inputs.draft }}
run: |
set -euo pipefail
echo "Release plan:"
echo " Platform: ${DISPLAY_PLATFORM}"
echo " Version: ${VERSION}"
echo " Tag: ${TAG}"
echo " Prerelease: ${PRERELEASE}"
echo " Publish workflow: ${PUBLISH_WORKFLOW}"
echo " Dry run: ${DRY_RUN}"
echo " Draft: ${DRAFT}"
if [ "$DRY_RUN" = "false" ] && [ "$DRAFT" = "true" ]; then
echo " Publish dispatch: skipped until the draft release is manually published"
elif [ "$DRY_RUN" = "false" ]; then
echo " Publish dispatch: ${PUBLISH_WORKFLOW} will be dispatched after release creation"
fi

- name: Create GitHub Release
if: ${{ !inputs.dry-run }}
env:
TAG: ${{ steps.release.outputs.tag }}
PRERELEASE: ${{ steps.release.outputs.prerelease }}
DRAFT: ${{ inputs.draft }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

args=("$TAG" --target "$GITHUB_SHA" --title "$TAG" --generate-notes)
if [ "$DRAFT" = "true" ]; then
args+=(--draft)
fi

if [ "$PRERELEASE" = "true" ]; then
args+=(--prerelease --latest=false)
elif [ "$DRAFT" = "false" ]; then
args+=(--latest)
fi

gh release create "${args[@]}"

- name: Dispatch publish workflow
if: ${{ !inputs.dry-run && !inputs.draft }}
env:
TAG: ${{ steps.release.outputs.tag }}
PUBLISH_WORKFLOW: ${{ steps.release.outputs.publish_workflow }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh workflow run "$PUBLISH_WORKFLOW" --ref "$TAG"
echo "::notice::Dispatched ${PUBLISH_WORKFLOW} at ${TAG}."

- name: Summary
env:
DISPLAY_PLATFORM: ${{ steps.release.outputs.display_platform }}
VERSION: ${{ steps.release.outputs.version }}
TAG: ${{ steps.release.outputs.tag }}
PUBLISH_WORKFLOW: ${{ steps.release.outputs.publish_workflow }}
run: |
cat >> "$GITHUB_STEP_SUMMARY" <<SUMMARY
## Release plan

- Dry run: `${{ inputs.dry-run }}`
- Draft: `${{ inputs.draft }}`
- Platform: ${DISPLAY_PLATFORM}
- Version: ${VERSION}
- Tag: \`${TAG}\`
- Publish workflow: \`${PUBLISH_WORKFLOW}\`
SUMMARY
14 changes: 14 additions & 0 deletions .github/workflows/swift-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Validate release tag matches Swift versions
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 iOS "" "$TAG"

- uses: ruby/setup-ruby@6aaa311d81eba98ae12eaffbcb63296ace0efcde # v1.307.0
with:
bundler-cache: true
Expand Down
Loading