Skip to content
Open
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
147 changes: 136 additions & 11 deletions .github/workflows/web-publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Web — Publish to npm
name: Web — Publish to npm and CDN

on:
release:
Expand All @@ -10,7 +10,7 @@ on:
required: false
type: string
dry-run:
description: "Run the full pipeline + pack but skip the actual publish. Defaults to true for manual safety; uncheck to actually publish."
description: "Run the full pipeline + pack but skip npm and CDN publishing. Defaults to true for manual safety; uncheck to publish."
required: false
type: boolean
default: true
Expand All @@ -25,7 +25,7 @@ concurrency:

jobs:
publish:
name: Publish @shopify/checkout-kit to npm
name: Publish @shopify/checkout-kit to npm and CDN
# Only run when either:
# - A GitHub Release tagged `web/X.Y.Z` is published (auto trigger), OR
# - The workflow is manually dispatched from the `main` branch. The
Expand Down Expand Up @@ -75,17 +75,20 @@ jobs:
fi
echo "✓ Tag '$TAG_NAME' matches package.json version '$VERSION_FROM_PKG'."

- name: Verify version is not already published
- name: Check whether version is already published
id: npm-version
run: |
set -euo pipefail
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
URL="https://registry.npmjs.org/${NAME}/${VERSION}"
if curl -fs "$URL" > /dev/null; then
echo "::error::${NAME}@${VERSION} is already published on npm. Bump platforms/web/package.json before re-running."
exit 1
echo "already_published=true" >> "$GITHUB_OUTPUT"
echo "::notice::${NAME}@${VERSION} is already published — skipping npm publish."
else
echo "already_published=false" >> "$GITHUB_OUTPUT"
echo "::notice::${NAME}@${VERSION} is not yet on npm — safe to proceed."
fi
echo "::notice::${NAME}@${VERSION} is not yet on npm — safe to proceed."

- name: Lint (typecheck + oxlint + format)
run: pnpm lint
Expand Down Expand Up @@ -129,22 +132,144 @@ jobs:
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Select CDN channel
id: cdn-policy
run: node scripts/cdn-release-policy.mjs
env:
DIST_TAG: ${{ steps.tag.outputs.tag }}
PRERELEASE: ${{ github.event.release.prerelease }}

# Pre-flight: prove the CDN identity works before publishing to npm so a
# broken binding fails the run before anything irreversible happens.
# Runs on dry runs too. Deliberately does NOT write a credentials file or
# export env vars — only a short-lived token held in a step output — so
# no package code that runs later can pick up CDN credentials.
- name: Pre-flight CDN identity
id: gcp-preflight
uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7
with:
project_id: shopify-tiers
workload_identity_provider: projects/197494322927/locations/global/workloadIdentityPools/github-actions/providers/github-actions
service_account: gha-checkout-kit-deploy@shopify-tiers.iam.gserviceaccount.com
token_format: access_token
create_credentials_file: false
export_environment_variables: false

- name: Verify CDN bucket access
env:
GCP_ACCESS_TOKEN: ${{ steps.gcp-preflight.outputs.access_token }}
CDN_PREFIX: ${{ steps.cdn-policy.outputs.prefix }}
run: |
set -euo pipefail
# Ask GCS which of the permissions the upload steps need the identity
# actually holds. Side-effect free, so safe on dry runs. Overwriting
# an existing loader requires delete as well as create.
REQUIRED="storage.objects.create storage.objects.delete storage.objects.list"
QUERY=""
for PERM in $REQUIRED; do QUERY="${QUERY}permissions=${PERM}&"; done
STATUS=$(curl -sS -o /tmp/iam-test.json -w '%{http_code}' \
-H "Authorization: Bearer ${GCP_ACCESS_TOKEN}" \
"https://storage.googleapis.com/storage/v1/b/checkout-kit-deployments/iam/testPermissions?${QUERY%&}")
if [ "$STATUS" != "200" ]; then
echo "::error::Cannot query IAM on gs://checkout-kit-deployments (HTTP ${STATUS}). Check the workload identity binding."
cat /tmp/iam-test.json
exit 1
fi
GRANTED=$(node -p "(JSON.parse(require('fs').readFileSync('/tmp/iam-test.json','utf8')).permissions ?? []).join(' ')")
MISSING=""
for PERM in $REQUIRED; do
case " $GRANTED " in *" $PERM "*) ;; *) MISSING="$MISSING $PERM" ;; esac
done
if [ -n "$MISSING" ]; then
echo "::error::CDN deploy identity is missing permissions on gs://checkout-kit-deployments:${MISSING}"
exit 1
fi
echo "::notice::CDN deploy identity holds ${REQUIRED} on gs://checkout-kit-deployments (deploying to ${CDN_PREFIX}/)"

# `DIST_TAG` is passed via `env:` (not direct ${{ }} interpolation) so
# that a workflow_dispatch input like `latest$(whoami)` is treated as a
# literal string by bash rather than command substitution.
# `--ignore-scripts` skips `prepack`, so the tarball contains the dist/
# that was built and verified above rather than a fresh, unverified build.
- name: Publish to npm
if: ${{ !inputs.dry-run }}
run: pnpm dlx npm@11.5.1 publish --no-git-checks --tag "$DIST_TAG" --access public --provenance
if: ${{ !inputs.dry-run && steps.npm-version.outputs.already_published != 'true' }}
run: pnpm dlx npm@11.5.1 publish --no-git-checks --ignore-scripts --tag "$DIST_TAG" --access public --provenance
env:
DIST_TAG: ${{ steps.tag.outputs.tag }}
NPM_CONFIG_PROVENANCE: "true"
NPM_TOKEN: ""
NODE_AUTH_TOKEN: ""

# Redeploying an already-published version to the CDN is only safe when
# the checkout matches what was published: a release tag, or a re-run
# (run_attempt > 1, same SHA) of the manual run that published it, e.g.
# to repair a failed CDN upload. A *fresh* manual dispatch runs from
# `main`, which may have moved on without a version bump; deploying that
# would put code on the CDN that was never published to npm, labelled
# with the old version.
- name: Guard against deploying unpublished code
if: ${{ github.event_name == 'workflow_dispatch' && github.run_attempt == '1' && steps.npm-version.outputs.already_published == 'true' }}
env:
DRY_RUN: ${{ inputs.dry-run }}
run: |
set -euo pipefail
VERSION=$(node -p "require('./package.json').version")
MESSAGE="${VERSION} is already on npm. A fresh manual run cannot redeploy it to the CDN because main may not match the published tarball. Re-run the workflow run that published it (release or manual) instead, or bump the version."
if [ "${DRY_RUN}" = "true" ]; then
echo "::warning::${MESSAGE}"
else
echo "::error::${MESSAGE}"
exit 1
fi

- name: Prepare CDN release
id: cdn
if: ${{ !inputs.dry-run }}
env:
CDN_PREFIX: ${{ steps.cdn-policy.outputs.prefix }}
run: |
set -euo pipefail
RELEASE_DIR="/tmp/checkout-kit-cdn/${CDN_PREFIX}"
mkdir -p "$RELEASE_DIR/assets" "$RELEASE_DIR/loader"
cp dist/web-components.js dist/web-components.js.map "$RELEASE_DIR/loader"
cp -R dist/assets/. "$RELEASE_DIR/assets"

# Full authentication (credentials file) only now, after all package
# code has run, and only for the upload steps that need it.
- name: Authenticate to Google Cloud
if: ${{ steps.cdn.outcome == 'success' }}
uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7
with:
project_id: shopify-tiers
workload_identity_provider: projects/197494322927/locations/global/workloadIdentityPools/github-actions/providers/github-actions
service_account: gha-checkout-kit-deploy@shopify-tiers.iam.gserviceaccount.com

# Upload content-addressed chunks before the loader references them.
# Cache-Control is not set per object: the CDN applies one caching policy
# to every /checkout-kit/ path. See platforms/web/CDN-PUBLISHING.md.
- name: Upload CDN implementation chunks
if: ${{ steps.cdn.outcome == 'success' }}
uses: google-github-actions/upload-cloud-storage@386ab77f37fdf51c0e38b3d229fad286861cc0d0 # v2.2.1
with:
path: /tmp/checkout-kit-cdn/${{ steps.cdn-policy.outputs.prefix }}/assets
destination: checkout-kit-deployments/${{ steps.cdn-policy.outputs.prefix }}/assets
parent: false

- name: Upload CDN loader
if: ${{ steps.cdn.outcome == 'success' }}
uses: google-github-actions/upload-cloud-storage@386ab77f37fdf51c0e38b3d229fad286861cc0d0 # v2.2.1
with:
path: /tmp/checkout-kit-cdn/${{ steps.cdn-policy.outputs.prefix }}/loader
destination: checkout-kit-deployments/${{ steps.cdn-policy.outputs.prefix }}
parent: false

- name: Dry-run summary
if: ${{ inputs.dry-run }}
env:
DIST_TAG: ${{ steps.tag.outputs.tag }}
CDN_CHANNEL: ${{ steps.cdn-policy.outputs.channel }}
CDN_PREFIX: ${{ steps.cdn-policy.outputs.prefix }}
run: |
echo "::notice::Dry-run requested — skipped npm publish."
echo "Would have published with: --tag $DIST_TAG --access public --provenance"
echo "::notice::Dry-run requested — skipped npm and CDN publishing."
echo "Would have published to npm with: --tag $DIST_TAG --access public --provenance"
echo "Would have uploaded the ${CDN_CHANNEL} CDN loader to: /checkout-kit/${CDN_PREFIX}/web-components.js"
Loading
Loading