Skip to content

staging-deploy

staging-deploy #441

Workflow file for this run

name: E2E Staging
on:
repository_dispatch:
types: [staging-deploy]
workflow_dispatch:
inputs:
ref:
description: 'Branch to test against'
required: false
default: 'main'
type: string
clerk-go-commit-sha:
description: 'clerk_go commit SHA for status reporting'
required: false
type: string
sdk-source:
description: "SDK source: 'latest' uses published @latest from npm, 'ref' builds from the checked-out branch"
required: false
default: 'latest'
type: choice
options:
- latest
- ref
notify-slack:
description: 'Send Slack notification on failure'
required: false
default: true
type: boolean
permissions:
contents: read
actions: write
concurrency:
# Key on the clerk_go commit being validated rather than the (effectively always "main")
# ref, so distinct staging deploys no longer cancel each other and each commit can report
# its own result. Duplicate dispatches for the SAME commit still de-dupe; manual dispatches
# without a commit SHA fall back to the unique run_id and are never cancelled.
group: ${{ github.workflow }}-${{ github.event.inputs.clerk-go-commit-sha || github.event.client_payload.clerk-go-commit-sha || github.run_id }}
cancel-in-progress: true
jobs:
permissions-check:
name: Check Permissions
if: ${{ github.event_name != 'repository_dispatch' }}
runs-on: 'blacksmith-8vcpu-ubuntu-2204'
steps:
- name: Check org membership
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const org = context.repo.owner;
const username = process.env.GITHUB_TRIGGERING_ACTOR || context.actor;
try {
const { status } = await github.rest.orgs.checkMembershipForUser({
org,
username,
});
if (status !== 204) {
core.setFailed(`User '${username}' is not a member of the '${org}' organization.`);
}
} catch (error) {
if (error?.status === 404) {
core.setFailed(`User '${username}' is not a member of the '${org}' organization.`);
} else {
core.setFailed(
`Org membership check failed for '${username}' in '${org}' (status: ${error?.status ?? 'unknown'}): ${error?.message ?? 'unknown error'}`
);
}
}
validate-instances:
name: Validate Staging Instances
needs: [permissions-check]
if: ${{ always() && (needs.permissions-check.result == 'success' || needs.permissions-check.result == 'skipped') }}
runs-on: 'blacksmith-8vcpu-ubuntu-2204'
steps:
- name: Normalize inputs
id: inputs
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_REF: ${{ github.event.inputs.ref }}
PAYLOAD_REF: ${{ github.event.client_payload.ref }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "ref=${INPUT_REF:-main}" >> $GITHUB_OUTPUT
else
echo "ref=${PAYLOAD_REF:-main}" >> $GITHUB_OUTPUT
fi
- name: Validate ref
env:
REF: ${{ steps.inputs.outputs.ref }}
run: |
if [[ ! "$REF" =~ ^(main|release/.*)$ ]]; then
echo "::error::Ref '$REF' is not allowed. Only 'main' and 'release/*' branches are permitted."
exit 1
fi
- name: Checkout Repo
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
ref: ${{ steps.inputs.outputs.ref }}
sparse-checkout: scripts/validate-staging-instances.mjs
fetch-depth: 1
- name: Validate staging instance settings
run: node scripts/validate-staging-instances.mjs
env:
# Report-only unless the `STAGING_VALIDATE_STRICT` repo variable is set to "true"/"1".
# When strict, a mismatch on critical config (see CRITICAL_PATHS in the script) fails
# this job, which gates the integration-tests job below so the run stops fast with a
# clear diagnostic instead of letting a drifted staging mirror produce opaque failures.
STAGING_VALIDATE_STRICT: ${{ vars.STAGING_VALIDATE_STRICT }}
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
INTEGRATION_STAGING_INSTANCE_KEYS: ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }}
integration-tests:
name: Integration Tests (${{ matrix.test-name }}, ${{ matrix.test-project }})
needs: [permissions-check, validate-instances]
# Run when permissions passed/skipped AND the staging-config validation did not block.
# validate-instances only fails when strict gating is enabled and critical config drifted,
# so by default (report-only) this is a no-op and tests run as before.
if: ${{ always() && (needs.permissions-check.result == 'success' || needs.permissions-check.result == 'skipped') && (needs.validate-instances.result == 'success' || needs.validate-instances.result == 'skipped') }}
runs-on: 'blacksmith-8vcpu-ubuntu-2204'
defaults:
run:
shell: bash
# Must stay above the 25-minute "Run Integration Tests" step budget below, otherwise the
# job-level cap kills the run mid-suite and reports a misleading timeout/cancellation.
timeout-minutes: ${{ vars.TIMEOUT_MINUTES_LONG && fromJSON(vars.TIMEOUT_MINUTES_LONG) || 30 }}
strategy:
fail-fast: false
max-parallel: 3
matrix:
test-name:
- 'sessions:staging'
- 'handshake:staging'
- 'generic'
- 'cache-components'
- 'express'
- 'hono'
- 'quickstart'
- 'react-router'
- 'tanstack-react-start'
test-project: ['chrome']
steps:
- name: Normalize inputs
id: inputs
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_REF: ${{ github.event.inputs.ref }}
INPUT_COMMIT_SHA: ${{ github.event.inputs.clerk-go-commit-sha }}
INPUT_NOTIFY_SLACK: ${{ github.event.inputs.notify-slack }}
INPUT_SDK_SOURCE: ${{ github.event.inputs.sdk-source }}
PAYLOAD_REF: ${{ github.event.client_payload.ref }}
PAYLOAD_COMMIT_SHA: ${{ github.event.client_payload.clerk-go-commit-sha }}
PAYLOAD_NOTIFY_SLACK: ${{ github.event.client_payload.notify-slack }}
PAYLOAD_SDK_SOURCE: ${{ github.event.client_payload.sdk-source }}
TEST_NAME: ${{ matrix.test-name }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "ref=${INPUT_REF:-main}" >> $GITHUB_OUTPUT
echo "clerk-go-commit-sha=$INPUT_COMMIT_SHA" >> $GITHUB_OUTPUT
echo "notify-slack=$INPUT_NOTIFY_SLACK" >> $GITHUB_OUTPUT
echo "sdk-source=${INPUT_SDK_SOURCE:-latest}" >> $GITHUB_OUTPUT
else
echo "ref=${PAYLOAD_REF:-main}" >> $GITHUB_OUTPUT
echo "clerk-go-commit-sha=${PAYLOAD_COMMIT_SHA:-}" >> $GITHUB_OUTPUT
echo "notify-slack=${PAYLOAD_NOTIFY_SLACK:-true}" >> $GITHUB_OUTPUT
echo "sdk-source=${PAYLOAD_SDK_SOURCE:-latest}" >> $GITHUB_OUTPUT
fi
echo "artifact-suffix=${TEST_NAME//:/-}" >> $GITHUB_OUTPUT
- name: Validate ref
env:
REF: ${{ steps.inputs.outputs.ref }}
run: |
if [[ ! "$REF" =~ ^(main|release/.*)$ ]]; then
echo "::error::Ref '$REF' is not allowed. Only 'main' and 'release/*' branches are permitted."
exit 1
fi
- name: Checkout Repo
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
ref: ${{ steps.inputs.outputs.ref }}
fetch-depth: 1
fetch-tags: false
filter: 'blob:none'
show-progress: false
- name: Setup
id: config
uses: ./.github/actions/init-blacksmith
with:
cache-enabled: true
turbo-signature: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
turbo-team: ${{ vars.TURBO_TEAM }}
turbo-token: ${{ secrets.TURBO_TOKEN }}
playwright-enabled: true
- name: Verify jq is installed
run: |
if ! command -v jq &> /dev/null; then
echo "jq not found, installing..."
sudo apt-get update && sudo apt-get install -y jq
fi
jq --version
- name: Build packages
run: pnpm turbo build $TURBO_ARGS --only
# --- SDK from ref: publish to local registry ---
- name: Publish to local registry
if: ${{ steps.inputs.outputs.sdk-source == 'ref' }}
run: pkglab pub --force
- name: Edit .npmrc [link-workspace-packages=false]
if: ${{ steps.inputs.outputs.sdk-source == 'ref' }}
run: sed -i -E 's/link-workspace-packages=(deep|true)/link-workspace-packages=false/' .npmrc
- name: Install @clerk/clerk-js from local registry
if: ${{ steps.inputs.outputs.sdk-source == 'ref' }}
working-directory: ${{ runner.temp }}
run: |
mkdir clerk-js && cd clerk-js
pnpm init
pkglab add @clerk/clerk-js
- name: Install @clerk/ui from local registry
if: ${{ steps.inputs.outputs.sdk-source == 'ref' }}
working-directory: ${{ runner.temp }}
run: |
mkdir clerk-ui && cd clerk-ui
pnpm init
pkglab add @clerk/ui
# --- SDK from npm: install @latest published versions ---
- name: Install @clerk/clerk-js@latest from npm
if: ${{ steps.inputs.outputs.sdk-source == 'latest' }}
working-directory: ${{ runner.temp }}
run: |
mkdir clerk-js && cd clerk-js
pnpm init
pnpm add @clerk/clerk-js@latest
- name: Install @clerk/ui@latest from npm
if: ${{ steps.inputs.outputs.sdk-source == 'latest' }}
working-directory: ${{ runner.temp }}
run: |
mkdir clerk-ui && cd clerk-ui
pnpm init
pnpm add @clerk/ui@latest
- name: Write all ENV certificates to files in integration/certs
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
env:
INTEGRATION_CERTS: '${{ secrets.INTEGRATION_CERTS }}'
INTEGRATION_ROOT_CA: '${{ secrets.INTEGRATION_ROOT_CA }}'
with:
script: |
const fs = require('fs');
const path = require('path');
const rootCa = process.env.INTEGRATION_ROOT_CA;
if (!rootCa) {
core.setFailed('INTEGRATION_ROOT_CA secret is not set');
return;
}
fs.writeFileSync(path.join(process.env.GITHUB_WORKSPACE, 'integration/certs', 'rootCA.pem'), rootCa);
const certs = JSON.parse(process.env.INTEGRATION_CERTS);
for (const [name, cert] of Object.entries(certs)) {
fs.writeFileSync(path.join(process.env.GITHUB_WORKSPACE, 'integration/certs', name), cert);
}
- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
env:
# Staging e2e validates the LIVE staging deploy, which is not a turbo input
# (the cache key is integration/** + env). Force re-execution so a cached green
# pass can't stand in for an untested deploy, and so the Playwright JSON report
# is regenerated every run instead of being skipped on a `>>> FULL TURBO` hit.
TURBO_FORCE: 'true'
E2E_DEBUG: '1'
E2E_STAGING: '1'
E2E_WORKERS: '2'
E2E_SDK_SOURCE: ${{ steps.inputs.outputs.sdk-source }}
E2E_APP_CLERK_JS_DIR: ${{ runner.temp }}
E2E_APP_CLERK_UI_DIR: ${{ runner.temp }}
E2E_CLERK_JS_VERSION: 'latest'
E2E_CLERK_UI_VERSION: 'latest'
E2E_PROJECT: ${{ matrix.test-project }}
INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }}
INTEGRATION_STAGING_INSTANCE_KEYS: ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }}
VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
NODE_EXTRA_CA_CERTS: ${{ github.workspace }}/integration/certs/rootCA.pem
- name: Upload test-results
if: ${{ cancelled() || failure() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: playwright-traces-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.inputs.outputs.artifact-suffix }}
path: test-results
retention-days: 1
# Always upload the machine-readable report (even on success) so downstream
# reporting/classification can run regardless of the leg's pass/fail outcome.
# The json reporter's outputFile is resolved relative to the Playwright config
# directory (integration/), not the repo root, so the report lands under
# integration/playwright-report/. Keep that prefix here. `warn` (not `ignore`)
# so a future path regression surfaces in the logs instead of failing silently.
- name: Upload Playwright JSON report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: playwright-report-${{ github.run_id }}-${{ github.run_attempt }}-${{ steps.inputs.outputs.artifact-suffix }}
path: integration/playwright-report/results.json
if-no-files-found: warn
retention-days: 3
report:
name: Report Results
# validate-instances is needed so a strict-mode gate failure (which SKIPS all
# test legs rather than failing them) still reaches the Slack notification.
needs: [integration-tests, validate-instances]
if: always()
runs-on: 'blacksmith-8vcpu-ubuntu-2204'
defaults:
run:
shell: bash
steps:
- name: Normalize inputs
id: inputs
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_REF: ${{ github.event.inputs.ref }}
INPUT_COMMIT_SHA: ${{ github.event.inputs.clerk-go-commit-sha }}
INPUT_NOTIFY_SLACK: ${{ github.event.inputs.notify-slack }}
INPUT_SDK_SOURCE: ${{ github.event.inputs.sdk-source }}
PAYLOAD_REF: ${{ github.event.client_payload.ref }}
PAYLOAD_COMMIT_SHA: ${{ github.event.client_payload.clerk-go-commit-sha }}
PAYLOAD_NOTIFY_SLACK: ${{ github.event.client_payload.notify-slack }}
PAYLOAD_SDK_SOURCE: ${{ github.event.client_payload.sdk-source }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
echo "ref=${INPUT_REF:-main}" >> $GITHUB_OUTPUT
echo "clerk-go-commit-sha=$INPUT_COMMIT_SHA" >> $GITHUB_OUTPUT
echo "notify-slack=$INPUT_NOTIFY_SLACK" >> $GITHUB_OUTPUT
echo "sdk-source=${INPUT_SDK_SOURCE:-latest}" >> $GITHUB_OUTPUT
else
echo "ref=${PAYLOAD_REF:-main}" >> $GITHUB_OUTPUT
echo "clerk-go-commit-sha=${PAYLOAD_COMMIT_SHA:-}" >> $GITHUB_OUTPUT
echo "notify-slack=${PAYLOAD_NOTIFY_SLACK:-true}" >> $GITHUB_OUTPUT
echo "sdk-source=${PAYLOAD_SDK_SOURCE:-latest}" >> $GITHUB_OUTPUT
fi
- name: Notify Slack on failure
if: ${{ (needs.integration-tests.result == 'failure' || needs.validate-instances.result == 'failure') && steps.inputs.outputs.notify-slack == 'true' }}
uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*:red_circle: Staging E2E tests failed*\n*Repo:* `${{ github.repository }}`\n*Ref:* `${{ steps.inputs.outputs.ref }}`\n*SDK:* `${{ steps.inputs.outputs.sdk-source }}`\n*clerk_go commit:* `${{ steps.inputs.outputs.clerk-go-commit-sha || 'N/A' }}`\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.STAGING_E2E_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
# Uncomment when clerk_go side is ready
# - name: Post commit status to clerk_go
# if: ${{ steps.inputs.outputs.clerk-go-commit-sha != '' }}
# uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
# with:
# github-token: ${{ secrets.CLERK_COOKIE_PAT }}
# script: |
# await github.rest.repos.createCommitStatus({
# owner: 'clerk',
# repo: 'clerk_go',
# sha: '${{ steps.inputs.outputs.clerk-go-commit-sha }}',
# state: '${{ needs.integration-tests.result == 'success' && 'success' || 'failure' }}',
# target_url: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
# context: 'e2e-staging / clerk-javascript',
# description: 'JS SDK e2e tests against staging'
# });