staging-deploy #440
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' | |
| # }); |