Make auth assertions single-use. #20482
Workflow file for this run
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
| # This workflow will build the project on pull requests with tests | |
| # Uses: | |
| # OS: ubuntu-latest | |
| # Go: go 1.x | |
| name: 👷🛠️ PR Builder | |
| # TEMPORARY WORKAROUND for runner saturation (see #4268): on pull requests, the | |
| # builder only runs once the `trigger-pr-builder` label is present. Early pushes | |
| # iterate against the review bots without consuming runners, and the label | |
| # (added when the PR is ready) turns CI on for that push and every subsequent | |
| # one. Merge queue runs are always on. Remove this gate (restore unconditional | |
| # pull_request runs) once queue pressure is resolved. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, labeled] | |
| merge_group: | |
| workflow_dispatch: | |
| # Avoid running multiple PR builders for the same PR on subsequent pushes. GitHub Actions' | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| GOFLAGS: "-mod=readonly" | |
| PRODUCT_NAME: "ThunderID" | |
| PRODUCT_NAME_LOWER: "thunderid" | |
| NODE_VERSION: "lts/*" | |
| jobs: | |
| # Single job for the fast gate checks (security audit, dependency review, and | |
| # change detection for docs/PowerShell) so they occupy one runner slot instead | |
| # of four. Runner concurrency is shared across all PRs, so fewer jobs per run | |
| # directly reduces queue time on busy periods. | |
| # | |
| # This is also the root of the job graph: every other job depends on it, so a | |
| # run requests one runner up front instead of eight competing for slots, and | |
| # nothing expensive starts until these ~20s of checks pass. | |
| preflight: | |
| name: 🛡️ Preflight Checks | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| outputs: | |
| docs-changed: ${{ steps.filter.outputs.docs }} | |
| powershell-changed: ${{ steps.filter.outputs.powershell }} | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| # Diff-based steps need a base/head pair from the pull_request or | |
| # merge_group payload, which a manual dispatch does not carry. | |
| - name: 🔍 Detect Docs and PowerShell Changes | |
| if: ${{ github.event_name != 'workflow_dispatch' }} | |
| uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3 | |
| id: filter | |
| with: | |
| base: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event.pull_request.base.sha }} | |
| ref: ${{ github.event_name == 'merge_group' && github.sha || github.event.pull_request.head.sha }} | |
| filters: | | |
| docs: | |
| - 'docs/**' | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'pnpm-workspace.yaml' | |
| powershell: | |
| - '**.ps1' | |
| - '.github/workflows/windows-powershell-validation.yml' | |
| # This job only audits the lockfile, so it must not cache a pnpm store: | |
| # an empty store published here is what every later job restored. | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache-store: 'false' | |
| - name: 🔍 Run pnpm audit | |
| run: | | |
| echo "🔍 Running pnpm audit for all workspace dependencies..." | |
| # CVE ignores are managed via auditConfig.ignoreCves in pnpm-workspace.yaml | |
| # Remove `--ignore-registry-errors` once pnpm 11 is generally available. Tracker: https://github.com/pnpm/pnpm/issues/11265 | |
| pnpm audit --ignore-registry-errors --audit-level=high | |
| - name: 🔎 Dependency Review | |
| if: ${{ github.event_name != 'workflow_dispatch' }} | |
| uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4 | |
| with: | |
| fail-on-severity: high | |
| deny-licenses: GPL-3.0-only,GPL-3.0-or-later,AGPL-3.0-only,AGPL-3.0-or-later | |
| comment-summary-in-pr: always | |
| base-ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event.pull_request.base.sha }} | |
| head-ref: ${{ github.event_name == 'merge_group' && github.sha || github.event.pull_request.head.sha }} | |
| codecov-merge-queue-status: | |
| name: 📊 Codecov Merge Queue Status | |
| # Codecov does not reliably process merge queue (merge_group) commits, leaving the | |
| # required codecov/patch status stuck at "Expected" and timing the queue entry out. | |
| # Patch coverage is already enforced on the pull request before it can enter the | |
| # queue, and the queue commit carries the same diff rebased onto main, so this job | |
| # marks codecov/patch as passed on the queue commit instead of re-running Codecov. | |
| # Tracker: https://github.com/codecov/codecov-action/issues/1796 | |
| if: github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| statuses: write | |
| steps: | |
| - name: 📊 Mark codecov/patch as passed on the merge queue commit | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh api "repos/${{ github.repository }}/statuses/${{ github.sha }}" \ | |
| -f state=success \ | |
| -f context=codecov/patch \ | |
| -f description="Patch coverage was enforced on the pull request" | |
| verify-mocks: | |
| name: 🔍 Verify Mock Files | |
| needs: [preflight] | |
| if: ${{ (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Go Environment | |
| uses: ./.github/actions/setup-go | |
| - name: 🔧 Install Mockery | |
| run: make install-mockery | |
| - name: 🏭 Generate Mocks | |
| run: make mockery | |
| - name: 🔎 Check for Outdated Mocks | |
| run: | | |
| if [ -n "$(git status --porcelain --untracked-files=all -- backend/tests/mocks backend/internal)" ]; then | |
| echo "❌ Mock files are out of sync with interface definitions!" | |
| echo "" | |
| echo "The following mock files need to be regenerated:" | |
| git status --porcelain --untracked-files=all -- backend/tests/mocks backend/internal | awk '{print $2}' | |
| echo "" | |
| echo "To fix this:" | |
| echo " 1. Run: make mockery" | |
| echo " 2. Commit the updated mock files" | |
| echo " 3. Push your changes" | |
| echo "" | |
| echo "This ensures mock implementations stay synchronized with their interfaces." | |
| exit 1 | |
| fi | |
| echo "✅ All mock files are up to date" | |
| lint: | |
| name: 🧹 Lint Code | |
| needs: [preflight] | |
| if: ${{ (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| # We need the full history so that Turbo's --affected can diff against the PR or merge-group base commit. | |
| fetch-depth: 0 | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: ⚙️ Set up Go Environment | |
| uses: ./.github/actions/setup-go | |
| - name: 📦 Prepare golangci-lint Locally | |
| run: make golangci-lint | |
| - name: 🔍 Run Backend Linter | |
| run: make lint_backend | |
| # The frontend build is needed because linting is type-aware: apps resolve | |
| # @thunderid/* package types from their built dist output. The Turbo cache | |
| # makes it a near no-op when nothing relevant changed. | |
| - name: 🗂️ Set up Turbo Cache | |
| uses: ./.github/actions/setup-turbo-cache | |
| - name: 🧩 Install Dependencies & Build Frontend | |
| run: | | |
| make build_frontend | |
| - name: 🔍 Run Frontend Linter | |
| run: | | |
| pnpm turbo run lint --affected --filter=!./tests/** | |
| env: | |
| TURBO_SCM_BASE: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event.pull_request.base.sha }} | |
| - name: 🎨 Check Frontend Formatting | |
| run: | | |
| pnpm format:check | |
| build: | |
| name: 🛠️ Build Product | |
| needs: [preflight] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Go Environment | |
| uses: ./.github/actions/setup-go | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: 🗄️ Cache Go Modules | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| id: cache-go-modules | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-modules-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-modules- | |
| - name: 📦 Install Backend Dependencies | |
| run: | | |
| cd backend | |
| go mod download | |
| cd ../tests/integration | |
| go mod download | |
| - name: 🗂️ Set up Turbo Cache | |
| uses: ./.github/actions/setup-turbo-cache | |
| - name: 📦 Install Frontend Dependencies | |
| run: | | |
| pnpm install --frozen-lockfile | |
| - name: 🧹 Clean Previous Builds | |
| run: | | |
| set -e | |
| make clean | |
| - name: 🔨 Build Frontend | |
| env: | |
| # Activates the Codecov bundle-analysis plugin in the gate/console Vite | |
| # builds, which uploads bundle stats to Codecov via GitHub OIDC. | |
| CODECOV_BUNDLE_UPLOAD: "true" | |
| run: | | |
| make build_frontend | |
| - name: 🔨 Build Backend with Coverage Instrumentation | |
| run: | | |
| set -e | |
| export LOG_LEVEL=debug | |
| ENABLE_COVERAGE=true ./build.sh build_backend $(go env GOOS) $(go env GOARCH) | |
| # Find the built distribution | |
| DIST_PATH=$(find target/dist -name "$PRODUCT_NAME_LOWER-*.zip" | head -1) | |
| echo "Built distribution: $DIST_PATH" | |
| - name: 📦 Upload Built Distribution | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: product-distribution | |
| path: target/dist/*.zip | |
| if-no-files-found: error | |
| test-backend-unit: | |
| name: 🧪 Backend Unit Tests | |
| needs: [preflight] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Set up Go Environment | |
| uses: ./.github/actions/setup-go | |
| - name: 🗄️ Cache Go Modules | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-modules-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-modules- | |
| - name: 📦 Install Backend Dependencies | |
| run: | | |
| cd backend | |
| go mod download | |
| - name: 🧪 Run Backend Unit Tests | |
| run: | | |
| set -e | |
| ./build.sh test_unit $(go env GOOS) $(go env GOARCH) | |
| - name: 📊 Upload Unit Test Coverage Report to Codecov | |
| # Skipped on merge queue commits: Codecov cannot process them, and the | |
| # codecov-merge-queue-status job stamps the required status instead. | |
| if: github.event_name != 'merge_group' | |
| uses: codecov/codecov-action@v5 | |
| timeout-minutes: 5 | |
| continue-on-error: true | |
| with: | |
| use_oidc: true | |
| files: backend/coverage_unit.out | |
| disable_search: true | |
| flags: backend-unit | |
| name: Backend Unit Tests | |
| fail_ci_if_error: false | |
| - name: 📦 Archive Unit Coverage Report | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: unit-coverage-report | |
| path: backend/coverage_unit.out | |
| if-no-files-found: error | |
| test-frontend-packages: | |
| name: 🧪 Frontend Packages Tests | |
| needs: [preflight] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| # Checkout, turbo cache and artifact upload need nothing beyond read access; the | |
| # artifact action authenticates with its own token rather than the GITHUB_TOKEN. | |
| permissions: | |
| contents: read | |
| container: | |
| image: mcr.microsoft.com/playwright:v1.60.0-noble | |
| options: --user root | |
| # PLAYWRIGHT_BROWSERS_PATH: the Playwright image installs browsers under /ms-playwright but | |
| # some tooling (e.g. @vitest/browser-playwright) resolves off HOME/.cache/ms-playwright | |
| # when the image's ENV is masked by the runner. Pinning at job level guarantees resolution. | |
| # | |
| # HOME=/root: GitHub Actions overrides HOME=/github/home for container jobs, but that | |
| # directory is owned by uid 1001 (the default runner user); since we run as root, Firefox | |
| # refuses to launch ("$HOME folder isn't owned by the current user"). Chromium and WebKit | |
| # tolerate it; Firefox does not. Setting HOME=/root aligns ownership with the running user. | |
| env: | |
| PLAYWRIGHT_BROWSERS_PATH: /ms-playwright | |
| HOME: /root | |
| # GitHub Actions defaults container jobs to POSIX `sh`; force bash so brace expansion, | |
| # arrays, and `[[ ... ]]` used elsewhere in this workflow behave the same as on runners. | |
| defaults: | |
| run: | |
| shell: bash | |
| timeout-minutes: 30 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: 📦 Enable pnpm via corepack | |
| # The Playwright image ships Node with corepack; use it instead of setup-node/setup-pnpm, | |
| # both of which are noisy inside containers and add ~15-20s. | |
| run: | | |
| corepack enable | |
| corepack prepare pnpm@11.21.0 --activate | |
| pnpm --version | |
| - name: 📦 Install Frontend Dependencies | |
| run: | | |
| pnpm install --frozen-lockfile | |
| - name: 🗂️ Set up Turbo Cache | |
| uses: ./.github/actions/setup-turbo-cache | |
| - name: 🔨 Build Frontend Packages | |
| working-directory: frontend | |
| run: | | |
| pnpm build:packages | |
| - name: 🧪 Run Tests for Frontend Packages with Coverage | |
| working-directory: frontend | |
| run: | | |
| pnpm test:packages:coverage | |
| # Each package writes its own coverage/lcov.info with repo-relative paths, so the | |
| # whole set is uploaded under one flag rather than one flag per package. | |
| - name: 📤 Upload packages coverage artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: packages-coverage | |
| path: frontend/packages/*/coverage/lcov.info | |
| if-no-files-found: error | |
| test-frontend-gate-app: | |
| name: 🧪 Gate App Tests with Coverage | |
| needs: [preflight] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: 📦 Install Frontend Dependencies | |
| run: | | |
| pnpm install --frozen-lockfile | |
| - name: 🗂️ Set up Turbo Cache | |
| uses: ./.github/actions/setup-turbo-cache | |
| - name: 🔨 Build Frontend | |
| run: | | |
| pnpm build:frontend | |
| - name: 🧪 Run Gate Tests with Coverage | |
| run: | | |
| cd frontend/apps/gate | |
| pnpm test:coverage | |
| # Strip branch data (BRDA/BRF/BRH) from frontend LCOV reports before uploading. | |
| # React Compiler (babel-plugin-react-compiler) generates phantom branch points in compiled | |
| # output that inflate the partial-line count in Codecov's coverage calculation. | |
| # This is a known upstream issue: https://github.com/facebook/react/issues/32950 | |
| # Line and function coverage remain unaffected and accurately reflect test quality. | |
| - name: 🧹 Strip branch data from gate LCOV report | |
| run: | | |
| if [ -f "frontend/apps/gate/coverage/lcov.info" ]; then | |
| sed -i '/^BRDA:/d;/^BRF:/d;/^BRH:/d' "frontend/apps/gate/coverage/lcov.info" | |
| fi | |
| - name: 📦 Upload gate coverage artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: gate-coverage | |
| path: frontend/apps/gate/coverage/lcov.info | |
| if-no-files-found: error | |
| # The console suite is the longest job in the pipeline (~12 min of vitest in a | |
| # single run), so it is split across shards that execute concurrently. Each | |
| # shard reports coverage for the whole app with only its own tests counted; | |
| # test-frontend-console-app below merges the partial reports back into one | |
| # LCOV file, so coverage consumers still see a single console-coverage | |
| # artifact. | |
| test-frontend-console-app-shards: | |
| name: 🧪 Console App Tests (shard ${{ matrix.shard }}) | |
| needs: [preflight] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| strategy: | |
| # Report every failing shard, not just the first one, so a single run | |
| # surfaces all breakages. | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2, 3] | |
| env: | |
| SHARD_COUNT: 3 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: 📦 Install Frontend Dependencies | |
| run: | | |
| pnpm install --frozen-lockfile | |
| - name: 🗂️ Set up Turbo Cache | |
| uses: ./.github/actions/setup-turbo-cache | |
| - name: 🔨 Build Frontend | |
| run: | | |
| pnpm build:frontend | |
| - name: 🧪 Run Console Tests with Coverage | |
| working-directory: frontend/apps/console | |
| run: | | |
| pnpm exec vitest run --coverage --shard=${{ matrix.shard }}/${{ env.SHARD_COUNT }} | |
| # Strip branch data (BRDA/BRF/BRH) from frontend LCOV reports before uploading. | |
| # React Compiler (babel-plugin-react-compiler) generates phantom branch points in compiled | |
| # output that inflate the partial-line count in Codecov's coverage calculation. | |
| # This is a known upstream issue: https://github.com/facebook/react/issues/32950 | |
| # Line and function coverage remain unaffected and accurately reflect test quality. | |
| - name: 🧹 Strip branch data from console LCOV report | |
| run: | | |
| if [ -f "frontend/apps/console/coverage/lcov.info" ]; then | |
| sed -i '/^BRDA:/d;/^BRF:/d;/^BRH:/d' "frontend/apps/console/coverage/lcov.info" | |
| fi | |
| # Named without "coverage" so the patch coverage gate, which downloads | |
| # every "*coverage*" artifact, only ever sees the merged report. | |
| - name: 📦 Upload console shard LCOV report | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: console-lcov-shard-${{ matrix.shard }} | |
| path: frontend/apps/console/coverage/lcov.info | |
| if-no-files-found: error | |
| # Keeps the console suite reporting as one check and one coverage artifact | |
| # regardless of how many shards run. Uses always() with an explicit result | |
| # check so a failing shard fails this job too: a skipped required check counts | |
| # as passing for branch protection, which would let a red shard through. | |
| test-frontend-console-app: | |
| name: 🧪 Console App Tests with Coverage | |
| needs: [test-frontend-console-app-shards] | |
| if: ${{ always() && (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: ✅ Check shard results | |
| env: | |
| SHARDS_RESULT: ${{ needs.test-frontend-console-app-shards.result }} | |
| run: | | |
| if [ "$SHARDS_RESULT" != "success" ]; then | |
| echo "❌ Console test shards did not pass (result: $SHARDS_RESULT)" | |
| exit 1 | |
| fi | |
| echo "✅ All console test shards passed" | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: 📥 Download console shard LCOV reports | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: console-lcov-shard-* | |
| path: console-shards | |
| - name: 🧩 Merge console shard LCOV reports | |
| uses: ./.github/actions/merge-lcov | |
| with: | |
| reports-dir: console-shards | |
| output-file: frontend/apps/console/coverage/lcov.info | |
| - name: 📦 Upload console coverage artifact | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: console-coverage | |
| path: frontend/apps/console/coverage/lcov.info | |
| if-no-files-found: error | |
| upload-frontend-coverage: | |
| name: 📊 Upload Frontend Coverage | |
| needs: [test-frontend-gate-app, test-frontend-console-app, test-frontend-packages] | |
| if: always() && (needs.test-frontend-gate-app.result == 'success' && needs.test-frontend-console-app.result == 'success' && needs.test-frontend-packages.result == 'success') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: 📥 Download console coverage artifact | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: console-coverage | |
| path: coverage/console | |
| - name: 📥 Download gate coverage artifact | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: gate-coverage | |
| path: coverage/gate | |
| - name: 📥 Download packages coverage artifact | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: packages-coverage | |
| path: coverage/packages | |
| - name: 📊 Upload `@thunderid/console` Unit Test Coverage Report to Codecov | |
| if: github.event_name != 'merge_group' | |
| uses: codecov/codecov-action@v5 | |
| timeout-minutes: 5 | |
| continue-on-error: true | |
| with: | |
| use_oidc: true | |
| files: coverage/console/lcov.info | |
| disable_search: true | |
| flags: frontend-apps-console-unit | |
| name: Frontend Console App Unit Tests | |
| fail_ci_if_error: false | |
| - name: 📊 Upload `@thunderid/gate` Unit Test Coverage Report to Codecov | |
| if: github.event_name != 'merge_group' | |
| uses: codecov/codecov-action@v5 | |
| timeout-minutes: 5 | |
| continue-on-error: true | |
| with: | |
| use_oidc: true | |
| files: coverage/gate/lcov.info | |
| disable_search: true | |
| flags: frontend-apps-gate-unit | |
| name: Frontend Gate App Unit Tests | |
| fail_ci_if_error: false | |
| # One report per package, so search the downloaded tree instead of naming a single | |
| # file. Without this every package under frontend/packages/ stays unmeasured, which | |
| # is why moving console features into packages silently dropped them from coverage. | |
| - name: 📊 Upload Frontend Packages Unit Test Coverage Report to Codecov | |
| if: github.event_name != 'merge_group' | |
| uses: codecov/codecov-action@v5 | |
| timeout-minutes: 5 | |
| continue-on-error: true | |
| with: | |
| use_oidc: true | |
| directory: coverage/packages | |
| flags: frontend-packages-unit | |
| name: Frontend Packages Unit Tests | |
| fail_ci_if_error: false | |
| patch-coverage-gate: | |
| name: 🛡️ Patch Coverage Gate | |
| # Deterministic patch coverage check that always reports, unlike the external | |
| # codecov/patch status which can hang at "Expected" (fork PRs, merge queue | |
| # commits, Codecov processing stalls). The threshold and the ignored paths are | |
| # read from codecov.yml, so this enforces the same rules as codecov/patch | |
| # without a second copy of them here. The enforcement logic lives in | |
| # .github/actions/patch-coverage-gate. | |
| needs: [build, test-backend-unit, test-integration, test-frontend-console-app, test-frontend-gate-app, upload-frontend-coverage] | |
| if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'merge_group') && needs.build.result == 'success' && needs.test-backend-unit.result == 'success' && needs.test-integration.result == 'success' && needs.test-frontend-console-app.result == 'success' && needs.test-frontend-gate-app.result == 'success' && needs.upload-frontend-coverage.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| checks: read | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| # Codecov never processes merge queue commits, so a queue entry is compared | |
| # through the pull request it was created from: the queue commit carries | |
| # that same diff rebased onto main. The pull request number is embedded in | |
| # the queue ref, and its head commit already has a fully processed Codecov | |
| # report from the pull request run. Leaving the outputs empty makes the gate | |
| # compute patch coverage locally, which is what queue runs did before. | |
| # On merge queue runs the commit under test is not the commit Codecov | |
| # reported on, so the codecov/patch check run is read from the head of the | |
| # pull request the queue entry was created from. The queue commit carries | |
| # that same diff rebased onto main, which is the premise the | |
| # codecov-merge-queue-status job already relies on. | |
| - name: 🔢 Resolve the commit whose codecov/patch verdict applies | |
| id: codecov-target | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| # refs/heads/gh-readonly-queue/<base>/pr-<number>-<base sha> | |
| QUEUE_REF: ${{ github.event.merge_group.head_ref }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT_NAME" = "pull_request" ]; then | |
| echo "pr-number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| echo "head-sha=$PR_HEAD_SHA" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| PR=$(printf '%s' "$QUEUE_REF" | sed -nE 's|.*/pr-([0-9]+)-[0-9a-f]+$|\1|p') | |
| if [ -z "$PR" ]; then | |
| echo "⚠️ No pull request number in '$QUEUE_REF'; computing patch coverage locally." | |
| exit 0 | |
| fi | |
| SHA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}" --jq '.head.sha' 2>/dev/null || true) | |
| if [ -z "$SHA" ]; then | |
| echo "⚠️ Could not resolve the head commit of PR #${PR}; computing patch coverage locally." | |
| exit 0 | |
| fi | |
| echo "Comparing through PR #${PR} (head ${SHA})" | |
| echo "pr-number=$PR" >> "$GITHUB_OUTPUT" | |
| echo "head-sha=$SHA" >> "$GITHUB_OUTPUT" | |
| - name: 🛡️ Enforce Patch Coverage | |
| uses: ./.github/actions/patch-coverage-gate | |
| with: | |
| github-token: ${{ github.token }} | |
| pr-number: ${{ steps.codecov-target.outputs.pr-number }} | |
| head-sha: ${{ steps.codecov-target.outputs.head-sha || github.sha }} | |
| base-ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || format('origin/{0}', github.base_ref) }} | |
| build_samples: | |
| name: 🛠️ Build Sample Apps | |
| needs: [preflight] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Go Environment | |
| uses: ./.github/actions/setup-go | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: 📦 Package Sample Apps | |
| run: | | |
| make package_samples OS=$(go env GOOS) ARCH=$(go env GOARCH) | |
| - name: 📦 Upload Built Sample App | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: sample-app-react-sdk | |
| path: target/dist/sample-app-react-sdk-*.zip | |
| # Kept separately from sample-app-react-sdk, which other workflows consume by name. | |
| # The CLI installs this bundle for `try`, so cli-compatibility serves it with the product. | |
| - name: 📦 Upload Wayfinder Sample | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: sample-app-wayfinder | |
| path: target/dist/sample-app-wayfinder-*.zip | |
| if-no-files-found: error | |
| # Fast gate for the CLI: everything that needs neither the network nor a running server. | |
| # Kept out of the `lint` job above because the CLI lints with its own golangci-lint, built | |
| # against the toolchain in tools/cli/go.mod: a published prebuilt binary is compiled with an | |
| # older Go than the CLI targets and refuses to run at all in that case. | |
| cli-lint-unit: | |
| name: 🔍 CLI Lint and Unit Tests | |
| needs: [preflight] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: ⚙️ Set up Go Environment | |
| uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version-file: tools/cli/go.mod | |
| cache-dependency-path: tools/cli/go.sum | |
| - name: 🧹 Lint CLI | |
| run: make tools_lint_cli | |
| - name: 🧪 Run CLI Unit Tests | |
| run: make tools_test_cli | |
| # Lane 1 of the CLI e2e strategy: does a CLI change break the CLI? Drives the npx wrapper | |
| # against the currently published release, not the distribution built above: that pairing | |
| # is cli-compatibility's job, below. Split from cli-lint-unit so a lint or unit failure | |
| # reports without paying for this. | |
| cli-e2e: | |
| name: 🎭 CLI E2E (${{ matrix.os }}) | |
| needs: [cli-lint-unit] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 45 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest] | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| # global-setup.ts builds the CLI binary that the npx wrapper resolves. | |
| - name: ⚙️ Set up Go Environment | |
| uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version-file: tools/cli/go.mod | |
| cache-dependency-path: tools/cli/go.sum | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: 📦 Set up CLI E2E Suite | |
| uses: ./.github/actions/setup-cli-e2e | |
| - name: 🔌 Verify Required Ports Are Free | |
| uses: ./.github/actions/verify-cli-e2e-ports | |
| - name: 🎭 Run E2E Tests | |
| working-directory: tests/e2e-cli | |
| run: pnpm test | |
| - name: 📄 Upload Playwright Report on Failure | |
| if: failure() | |
| uses: ./.github/actions/upload-cli-e2e-report | |
| with: | |
| name: cli-e2e-report-${{ matrix.os }} | |
| cli-compatibility: | |
| name: 🔗 CLI Against This Build | |
| needs: [preflight, build, build_samples] | |
| if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder')) || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| # global-setup builds the CLI from this checkout; the product comes from the artifacts. | |
| - name: ⚙️ Set up Go Environment | |
| uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | |
| with: | |
| go-version-file: tools/cli/go.mod | |
| cache-dependency-path: tools/cli/go.sum | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| # Reusing what build-product and build-samples already produced, rather than building the | |
| # distribution a second time. | |
| - name: 📥 Download the Built Distribution | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: product-distribution | |
| path: target/dist | |
| - name: 📥 Download the Wayfinder Sample | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: sample-app-wayfinder | |
| path: target/dist | |
| - name: 🔍 Show What Will Be Served | |
| run: ls -lh target/dist/ | |
| - name: 📦 Set up CLI E2E Suite | |
| uses: ./.github/actions/setup-cli-e2e | |
| # 8899 is the local release server this run serves target/dist through, on top of the | |
| # suite's own fixed ports. | |
| - name: 🔌 Verify Required Ports Are Free | |
| uses: ./.github/actions/verify-cli-e2e-ports | |
| with: | |
| extra-ports: "8899" | |
| # E2E_PRODUCT_DIST makes the suite serve target/dist with a generated manifest and point the | |
| # CLI at it, so everything it installs was built by this pull request. | |
| - name: 🎭 Run the CLI Against This Build | |
| working-directory: tests/e2e-cli | |
| env: | |
| E2E_PRODUCT_DIST: ${{ github.workspace }}/target/dist | |
| run: pnpm test | |
| - name: 📄 Upload Playwright Report on Failure | |
| if: failure() | |
| uses: ./.github/actions/upload-cli-e2e-report | |
| with: | |
| name: cli-compatibility-report | |
| test-integration: | |
| name: 🧪 Integration Tests (${{ matrix.database }}) | |
| needs: [build, build_samples] | |
| if: ${{ always() && needs.build.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| permissions: | |
| id-token: write | |
| contents: read | |
| strategy: | |
| matrix: | |
| database: [sqlite, postgres, redis] | |
| fail-fast: false | |
| services: | |
| postgres: | |
| image: postgres:latest | |
| env: | |
| POSTGRES_USER: dbuser | |
| POSTGRES_PASSWORD: dbpassword | |
| POSTGRES_DB: postgredb | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| redis: | |
| image: redis:latest | |
| ports: | |
| - 6379:6379 | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Go Environment | |
| uses: ./.github/actions/setup-go | |
| - name: 🗄️ Cache Go Modules | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 | |
| id: cache-go-modules | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-modules-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-modules- | |
| - name: 📦 Install Dependencies | |
| run: | | |
| cd backend | |
| go mod download | |
| cd ../tests/integration | |
| go mod download | |
| - name: 📝 Configure Test Database | |
| run: | | |
| chmod +x tests/integration/resources/scripts/setup-test-config.sh | |
| ./tests/integration/resources/scripts/setup-test-config.sh | |
| env: | |
| DB_TYPE: ${{ matrix.database }} | |
| - name: 🧪 Run Integration Tests (${{ matrix.database }}) | |
| uses: ./.github/actions/run-integration-tests | |
| with: | |
| database-type: ${{ matrix.database }} | |
| coverage-enabled: true | |
| - name: 📦 Archive Integration Coverage Report | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| with: | |
| name: integration-coverage-${{ matrix.database }} | |
| path: target/coverage_integration.out | |
| if-no-files-found: error | |
| - name: 📊 Upload Integration Test Coverage Report to Codecov | |
| if: github.event_name != 'merge_group' | |
| uses: codecov/codecov-action@v5 | |
| timeout-minutes: 5 | |
| continue-on-error: true | |
| with: | |
| use_oidc: true | |
| files: target/coverage_integration.out | |
| disable_search: true | |
| flags: backend-integration-${{ matrix.database }} | |
| name: Backend Integration Tests (${{ matrix.database }}) | |
| fail_ci_if_error: false | |
| # - name: 🧩 Generate Combined Coverage Report | |
| # run: ./build.sh merge_coverage | |
| # - name: 📊 Upload Combined Coverage Report to Codecov | |
| # uses: codecov/codecov-action@v4 | |
| # with: | |
| # token: ${{ secrets.CODECOV_TOKEN }} | |
| # files: target/coverage_combined.out | |
| # disable_search: true | |
| # flags: backend-combined-${{ matrix.database }} | |
| # name: Backend-combined Coverage (${{ matrix.database }}) | |
| # fail_ci_if_error: false | |
| backend-integration-patch-coverage: | |
| name: 🛡️ Backend Integration Patch Coverage | |
| # Measures only what the integration suite executed. The combined | |
| # 🛡️ Patch Coverage Gate pools backend unit, backend integration, and both | |
| # frontend reports into one percentage, so a changed backend line covered | |
| # only by a unit test satisfies it. This job loads the three | |
| # integration-coverage-* artifacts and nothing else, which is the whole | |
| # reason it exists separately rather than as another threshold on that gate. | |
| # | |
| # Gated like the other checks: it needs only build and test-integration, so | |
| # unlike the combined gate it does not wait on the frontend or unit jobs, and | |
| # it is skipped when those two did not succeed rather than reporting on data | |
| # it does not have. | |
| needs: [build, test-integration] | |
| if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'merge_group') && needs.build.result == 'success' && needs.test-integration.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| # The base-to-head diff needs full history; a shallow clone makes the | |
| # merge base unreachable and every changed line look new. | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: 🛡️ Measure Backend Integration Patch Coverage | |
| uses: ./.github/actions/backend-integration-patch-coverage | |
| with: | |
| base-ref: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || format('origin/{0}', github.base_ref) }} | |
| # A merge_group payload carries no pull request labels, so queue runs | |
| # apply no tag group. Thresholds still come from the base packages map, | |
| # and only then the global default. | |
| pr-labels: ${{ github.event_name == 'pull_request' && toJSON(github.event.pull_request.labels.*.name) || '' }} | |
| test-integration-status: | |
| name: ✅ Integration Tests Status | |
| needs: [test-integration] | |
| if: ${{ always() && (github.event_name != 'pull_request' || (contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder'))) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check integration test results | |
| run: | | |
| if [[ "${{ needs.test-integration.result }}" == "failure" || "${{ needs.test-integration.result }}" == "cancelled" ]]; then | |
| echo "❌ Integration tests failed or were cancelled" | |
| exit 1 | |
| fi | |
| echo "✅ Integration tests passed or were not required" | |
| test-e2e: | |
| name: 🎭 Playwright E2E Tests | |
| needs: [build, build_samples] | |
| if: ${{ always() && needs.build.result == 'success' && needs.build_samples.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| container: | |
| image: mcr.microsoft.com/playwright:v1.60.0-noble | |
| options: --user root | |
| # HOME=/root: GitHub Actions overrides HOME=/github/home for container jobs, but that | |
| # directory is owned by uid 1001 (the default runner user). Since we run as root, Firefox | |
| # refuses to launch ("$HOME folder isn't owned by the current user"). Chromium and WebKit | |
| # tolerate it; Firefox does not. Setting HOME=/root aligns ownership with the running user. | |
| env: | |
| HOME: /root | |
| # GitHub Actions defaults container jobs to POSIX `sh`; force bash so brace expansion | |
| # (e.g. `for i in {1..60}`), arrays, and `[[ ... ]]` behave the same as on runners. | |
| defaults: | |
| run: | |
| shell: bash | |
| # Provisions and runs against two servers in sequence (core, then a fresh one for @wayfinder). | |
| timeout-minutes: 40 | |
| steps: | |
| - name: 🔧 Install Required OS Utilities | |
| # jq/lsof/unzip are used by the bash setup steps below. No build toolchain | |
| # is needed: no workspace dependency compiles from source. | |
| run: | | |
| apt-get update | |
| apt-get install -y --no-install-recommends jq lsof unzip | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| persist-credentials: false | |
| - name: 📦 Enable pnpm via corepack | |
| # The Playwright image ships Node with corepack; use it instead of setup-node/setup-pnpm, | |
| # both of which are noisy inside containers and add ~15-20s. | |
| run: | | |
| corepack enable | |
| corepack prepare pnpm@11.21.0 --activate | |
| pnpm --version | |
| - name: 📥 Download Built Distribution | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: product-distribution | |
| path: target/dist/ | |
| - name: 📥 Download Built Sample App | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: sample-app-react-sdk | |
| path: target/dist/ | |
| - name: 📂 Extract Server to E2E | |
| run: | | |
| mkdir -p tests/e2e/server | |
| unzip target/dist/$PRODUCT_NAME_LOWER-*.zip -d tests/e2e/server | |
| cd tests/e2e/server | |
| # Find the extracted folder (name includes version and arch) and move its contents to the root of tests/e2e/server | |
| EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "$PRODUCT_NAME_LOWER-*" | head -n 1) | |
| if [ -n "$EXTRACTED_DIR" ]; then | |
| mv "$EXTRACTED_DIR"/* . | |
| rmdir "$EXTRACTED_DIR" | |
| fi | |
| chmod +x setup.sh start.sh | |
| - name: 🔑 Resolve Admin Credentials | |
| env: | |
| SECRET_ADMIN_USERNAME: ${{ secrets.PLAYWRIGHT_ADMIN_USERNAME }} | |
| VAR_ADMIN_USERNAME: ${{ vars.PLAYWRIGHT_ADMIN_USERNAME }} | |
| SECRET_ADMIN_PASSWORD: ${{ secrets.PLAYWRIGHT_ADMIN_PASSWORD }} | |
| run: | | |
| # Resolve a single admin username/password pair (Secret > Variable > defaults.env) once, | |
| # up front, and reuse it for server bootstrap, token acquisition, and the Playwright run | |
| # below, instead of each resolving (or hardcoding) its own copy. | |
| DEFAULT_ADMIN_USERNAME=$(grep '^ADMIN_USERNAME=' tests/e2e/defaults.env | cut -d= -f2-) | |
| DEFAULT_ADMIN_PASSWORD=$(grep '^ADMIN_PASSWORD=' tests/e2e/defaults.env | cut -d= -f2-) | |
| ADMIN_USERNAME="${SECRET_ADMIN_USERNAME:-${VAR_ADMIN_USERNAME:-$DEFAULT_ADMIN_USERNAME}}" | |
| ADMIN_PASSWORD="${SECRET_ADMIN_PASSWORD:-$DEFAULT_ADMIN_PASSWORD}" | |
| echo "::add-mask::$ADMIN_PASSWORD" | |
| echo "ADMIN_USERNAME=$ADMIN_USERNAME" >> "$GITHUB_ENV" | |
| echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" >> "$GITHUB_ENV" | |
| echo "✅ Resolved admin credentials (Secret > Variable > defaults.env)" | |
| - name: 📋 Load E2E Default Configuration | |
| run: | | |
| # Loads tests/e2e/defaults.env (the canonical fixed dataset, shared with run-e2e.sh | |
| # for local runs) as DEFAULT_* so later steps can fall back to it. | |
| while IFS='=' read -r key value; do | |
| case "$key" in ''|'#'*) continue ;; esac | |
| echo "DEFAULT_${key}=${value}" >> "$GITHUB_ENV" | |
| done < tests/e2e/defaults.env | |
| - name: 🚀 Start the Server | |
| run: | | |
| # Redirect the backend's Google and GitHub endpoints to the mock servers the social login | |
| # specs start, mirroring what run-e2e.sh does locally. Else the specs would use the real | |
| # accounts.google.com/github.com. | |
| cat >> tests/e2e/server/deployment.yaml <<EOF | |
| identity_provider: | |
| google_base_url: "$DEFAULT_MOCK_GOOGLE_BASE_URL" | |
| github_base_url: "$DEFAULT_MOCK_GITHUB_BASE_URL" | |
| EOF | |
| echo "✅ Pointed identity provider endpoints at the mock servers" | |
| cd tests/e2e/server | |
| ./setup.sh | |
| # In container jobs, GitHub Actions reaps a step's descendant processes when the step's | |
| # shell exits. `setsid` runs start.sh in a new session detached from the shell so it | |
| # survives the step boundary and remains up when the Playwright test step runs later. | |
| setsid ./start.sh > "$GITHUB_WORKSPACE/server.log" 2>&1 < /dev/null & | |
| SERVER_PID=$! | |
| echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV | |
| disown $SERVER_PID 2>/dev/null || true | |
| # Wait for server to be ready | |
| echo "Waiting for server to be ready on https://localhost:8090..." | |
| for i in {1..60}; do | |
| if curl -s -k https://localhost:8090/health/liveness > /dev/null; then | |
| echo "Server is UP!" | |
| break | |
| fi | |
| echo "Still waiting ($i/60)..." | |
| sleep 2 | |
| done | |
| # Verify server is actually running | |
| if ! curl -s -k https://localhost:8090/health/liveness > /dev/null; then | |
| echo "Server failed to start within 2 minutes" | |
| exit 1 | |
| fi | |
| echo "Server started successfully!" | |
| - name: 🔑 Obtain Admin Access Token | |
| id: admin-token | |
| uses: ./.github/actions/obtain-admin-token | |
| with: | |
| admin-username: ${{ env.ADMIN_USERNAME }} | |
| admin-password: ${{ env.ADMIN_PASSWORD }} | |
| - name: 📦 Import Vanilla Sample Resources | |
| uses: ./.github/actions/import-declarative-config | |
| with: | |
| config-path: samples/apps/vanilla-sample/thunderid-config/basic/thunderid-config.yaml | |
| admin-token: ${{ steps.admin-token.outputs.token }} | |
| label: Vanilla Sample resources | |
| variables-json: | | |
| { | |
| "SAMPLE_APP_FLOW_SECRET": "sample-app-flow-secret", | |
| "SAMPLE_APP_GOOGLE_CLIENT_ID": "test-google-client-id", | |
| "SAMPLE_APP_GOOGLE_CLIENT_SECRET": "test-google-client-secret", | |
| "SAMPLE_APP_GOOGLE_REDIRECT_URI": "https://localhost:3000/", | |
| "SAMPLE_APP_GOOGLE_SCOPES": "openid,email,profile", | |
| "SAMPLE_APP_GITHUB_CLIENT_ID": "test-github-client-id", | |
| "SAMPLE_APP_GITHUB_CLIENT_SECRET": "test-github-client-secret", | |
| "SAMPLE_APP_GITHUB_REDIRECT_URI": "https://localhost:3000/", | |
| "SAMPLE_APP_SMS_SENDER_ID": "test-sms-sender-id" | |
| } | |
| - name: 📦 Import React SDK Sample Resources | |
| uses: ./.github/actions/import-declarative-config | |
| with: | |
| config-path: samples/apps/react-sdk-sample/thunderid-config/thunderid-config.yaml | |
| admin-token: ${{ steps.admin-token.outputs.token }} | |
| label: React SDK Sample resources | |
| variables-json: | | |
| { | |
| "REACT_SDK_SAMPLE_CLIENT_ID": "REACT_SDK_SAMPLE", | |
| "REACT_SDK_SAMPLE_REDIRECT_URIS": ["http://localhost:3000", "https://localhost:3000"], | |
| "REACT_SDK_SAMPLE_POST_LOGOUT_REDIRECT_URIS": ["http://localhost:3000", "https://localhost:3000"] | |
| } | |
| - name: 📦 Import E2E Admin App | |
| uses: ./.github/actions/import-declarative-config | |
| with: | |
| config-path: tests/e2e/thunderid-config.yaml | |
| admin-token: ${{ steps.admin-token.outputs.token }} | |
| label: E2E admin app | |
| - name: 📦 Import E2E Sample-App Infrastructure | |
| uses: ./.github/actions/import-declarative-config | |
| with: | |
| config-path: tests/e2e/thunderid-config-sample-apps.yaml | |
| admin-token: ${{ steps.admin-token.outputs.token }} | |
| label: E2E sample-app infrastructure | |
| - name: 🔄 Restart the Server with Security Enabled | |
| run: | | |
| cd tests/e2e/server | |
| echo "Stopping the server..." | |
| # Kill the start.sh wrapper process | |
| kill $SERVER_PID 2>/dev/null || true | |
| # Also kill any process still listening on port 8090 | |
| # (start.sh backgrounds the server binary, so killing start.sh | |
| # may leave the server process running as an orphan) | |
| sleep 2 | |
| if lsof -ti tcp:8090 >/dev/null 2>&1; then | |
| echo "Port 8090 still in use, killing remaining processes..." | |
| kill -9 $(lsof -ti tcp:8090) 2>/dev/null || true | |
| fi | |
| # Wait for port to be fully released | |
| sleep 3 | |
| echo "Starting the server with security enabled..." | |
| # See "Start the Server" step for why setsid is needed inside container jobs. | |
| setsid ./start.sh > "$GITHUB_WORKSPACE/server.log" 2>&1 < /dev/null & | |
| SERVER_PID=$! | |
| echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV | |
| disown $SERVER_PID 2>/dev/null || true | |
| # Wait for server to be ready | |
| echo "Waiting for server to be ready on https://localhost:8090..." | |
| for i in {1..60}; do | |
| if curl -s -k https://localhost:8090/health/liveness > /dev/null; then | |
| echo "Server is UP with security enabled!" | |
| break | |
| fi | |
| echo "Still waiting ($i/60)..." | |
| sleep 2 | |
| done | |
| # Verify server is actually running | |
| if ! curl -s -k https://localhost:8090/health/liveness > /dev/null; then | |
| echo "Server failed to restart within 2 minutes" | |
| exit 1 | |
| fi | |
| echo "Server restarted successfully with security enabled!" | |
| - name: 📱 Extract and Start Sample App | |
| run: | | |
| echo "Extracting Sample App..." | |
| mkdir -p tests/e2e/sample-app | |
| # Find the sample app zip file (platform-specific) | |
| SAMPLE_APP_ZIP=$(find target/dist -name "sample-app-react-sdk-*.zip" | head -n 1) | |
| if [ -z "$SAMPLE_APP_ZIP" ]; then | |
| echo "⚠️ Warning: Sample app zip not found in target/dist" | |
| echo "Available files:" | |
| ls -la target/dist/ | |
| exit 1 | |
| fi | |
| echo "Found sample app: $SAMPLE_APP_ZIP" | |
| unzip -q "$SAMPLE_APP_ZIP" -d tests/e2e/sample-app | |
| cd tests/e2e/sample-app | |
| # Find the extracted folder and move its contents | |
| EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "sample-app-*" | head -n 1) | |
| if [ -n "$EXTRACTED_DIR" ]; then | |
| mv "$EXTRACTED_DIR"/* . | |
| rmdir "$EXTRACTED_DIR" | |
| fi | |
| echo "Installing dependencies..." | |
| npm install | |
| echo "Generating SSL certificates..." | |
| openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ | |
| -keyout server.key \ | |
| -out server.cert \ | |
| -subj "/O=ThunderID/OU=${{ env.PRODUCT_NAME }}/CN=localhost" \ | |
| > /dev/null 2>&1 | |
| echo "Starting Sample App..." | |
| # See "Start the Server" step for why setsid is needed inside container jobs. | |
| SAMPLE_PID=$(setsid bash -c 'npm run dev > "$GITHUB_WORKSPACE/sample-app.log" 2>&1 < /dev/null & echo $!' | tail -1) | |
| echo "SAMPLE_APP_PID=$SAMPLE_PID" >> $GITHUB_ENV | |
| disown $SAMPLE_PID 2>/dev/null || true | |
| # Wait for sample app to be ready | |
| echo "Waiting for sample app to be ready on https://localhost:3000..." | |
| for i in {1..60}; do | |
| if curl -s -k https://localhost:3000 > /dev/null 2>&1; then | |
| echo "Sample App is UP!" | |
| break | |
| fi | |
| echo "Still waiting ($i/60)..." | |
| sleep 2 | |
| done | |
| # Verify sample app is running | |
| if ! curl -s -k https://localhost:3000 > /dev/null 2>&1; then | |
| echo "Sample app failed to start within 2 minutes" | |
| exit 1 | |
| fi | |
| echo "Sample App started successfully!" | |
| # Scoped to the E2E package: it has no workspace dependencies, so installing | |
| # the whole monorepo here only added time and dragged in native builds from | |
| # unrelated sample apps. | |
| - name: 📦 Install E2E Dependencies | |
| run: pnpm install --frozen-lockfile --filter @thunderid/e2e | |
| # Pull requests run the Chromium project only, and the merge queue runs all | |
| # three browsers, so nothing reaches main without the full matrix. On a pull | |
| # request that is 74 of the 187 tests. | |
| # The Wayfinder specs (tagged @wayfinder) import a config bundle that replaces the | |
| # server-wide CORS allowlist and default resource server (full-replace import semantics), | |
| # so they must not share a server with anything that depends on either setting; run them | |
| # separately, below, against their own freshly provisioned server. | |
| - name: 🎭 Run Playwright E2E Tests (core) | |
| run: | | |
| if [ -n "$PLAYWRIGHT_PROJECT_FILTER" ]; then | |
| echo "Running Playwright projects matching '$PLAYWRIGHT_PROJECT_FILTER'" | |
| pnpm test --project="$PLAYWRIGHT_PROJECT_FILTER" --grep-invert @wayfinder --pass-with-no-tests | |
| else | |
| echo "Running every Playwright project" | |
| pnpm test --grep-invert @wayfinder --pass-with-no-tests | |
| fi | |
| working-directory: tests/e2e | |
| env: | |
| # setup comes along as the chromium project's dependency. Empty on | |
| # merge_group and workflow_dispatch, which run every project. | |
| PLAYWRIGHT_PROJECT_FILTER: ${{ github.event_name == 'pull_request' && 'chromium' || '' }} | |
| # Configuration Priority: Secret > Variable > tests/e2e/defaults.env (loaded above as DEFAULT_*) | |
| # ADMIN_USERNAME/ADMIN_PASSWORD were already resolved once by the "Resolve Admin | |
| # Credentials" step above and reused for server bootstrap and token acquisition; reuse | |
| # that same pair here instead of resolving it again. | |
| BASE_URL: ${{ secrets.PLAYWRIGHT_BASE_URL || vars.PLAYWRIGHT_BASE_URL || env.DEFAULT_BASE_URL }} | |
| ADMIN_USERNAME: ${{ env.ADMIN_USERNAME }} | |
| ADMIN_PASSWORD: ${{ env.ADMIN_PASSWORD }} | |
| TEST_USER_USERNAME: ${{ secrets.PLAYWRIGHT_TEST_USER_USERNAME || vars.PLAYWRIGHT_TEST_USER_USERNAME || env.DEFAULT_TEST_USER_USERNAME }} | |
| TEST_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_TEST_USER_PASSWORD || env.DEFAULT_TEST_USER_PASSWORD }} | |
| PLAYWRIGHT_WORKERS: ${{ vars.PLAYWRIGHT_WORKERS || env.DEFAULT_PLAYWRIGHT_WORKERS }} | |
| DEBUG_AUTH: ${{ vars.PLAYWRIGHT_DEBUG_AUTH || env.DEFAULT_DEBUG_AUTH }} | |
| # MFA Test Configuration | |
| SAMPLE_APP_URL: ${{ env.DEFAULT_SAMPLE_APP_URL }} | |
| SERVER_URL: ${{ env.DEFAULT_SERVER_URL }} | |
| AUTO_SETUP_MFA: ${{ vars.AUTO_SETUP_MFA || env.DEFAULT_AUTO_SETUP_MFA }} | |
| MOCK_SMS_SERVER_PORT: ${{ vars.MOCK_SMS_SERVER_PORT || env.DEFAULT_MOCK_SMS_SERVER_PORT }} | |
| # Social Login Test Configuration. The specs bind their mock servers to these URLs and | |
| # skip entirely when unset, so they must match what "Start the Server" wrote into | |
| # deployment.yaml as identity_provider. | |
| MOCK_GOOGLE_BASE_URL: ${{ env.DEFAULT_MOCK_GOOGLE_BASE_URL }} | |
| MOCK_GITHUB_BASE_URL: ${{ env.DEFAULT_MOCK_GITHUB_BASE_URL }} | |
| SAMPLE_APP_USERNAME: ${{ secrets.SAMPLE_APP_USERNAME || vars.SAMPLE_APP_USERNAME || env.DEFAULT_SAMPLE_APP_USERNAME }} | |
| SAMPLE_APP_PASSWORD: ${{ secrets.SAMPLE_APP_PASSWORD || env.DEFAULT_SAMPLE_APP_PASSWORD }} | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: ${{ !cancelled() }} | |
| with: | |
| name: playwright-report | |
| path: tests/e2e/playwright-report/ | |
| retention-days: 30 | |
| - name: 🛑 Stop the Server | |
| if: ${{ !cancelled() }} | |
| run: | | |
| kill $SERVER_PID 2>/dev/null || true | |
| sleep 2 | |
| if lsof -ti tcp:8090 >/dev/null 2>&1; then | |
| kill -9 $(lsof -ti tcp:8090) 2>/dev/null || true | |
| fi | |
| sleep 3 | |
| rm -rf tests/e2e/server | |
| # A fresh server for the Wayfinder specs: same distribution zip, re-extracted so the CORS | |
| # allowlist and default resource server the previous phase set are gone, and importing only | |
| # the E2E admin app so there is no `Customer` user type for the Wayfinder bundle's own | |
| # `Customer` user type to collide with. Runs regardless of the core phase's result - a | |
| # different server, so the core phase's failure tells us nothing about this one (mirrors | |
| # run-e2e.sh's run_phase, which does the same for the same reason). | |
| - name: 📂 Extract Fresh Server for Wayfinder Tests | |
| if: ${{ !cancelled() }} | |
| run: | | |
| mkdir -p tests/e2e/server | |
| unzip target/dist/$PRODUCT_NAME_LOWER-*.zip -d tests/e2e/server | |
| cd tests/e2e/server | |
| EXTRACTED_DIR=$(find . -maxdepth 1 -type d -name "$PRODUCT_NAME_LOWER-*" | head -n 1) | |
| if [ -n "$EXTRACTED_DIR" ]; then | |
| mv "$EXTRACTED_DIR"/* . | |
| rmdir "$EXTRACTED_DIR" | |
| fi | |
| chmod +x setup.sh start.sh | |
| - name: 🚀 Start the Server (Wayfinder) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| cd tests/e2e/server | |
| ./setup.sh | |
| setsid ./start.sh > "$GITHUB_WORKSPACE/server-wayfinder.log" 2>&1 < /dev/null & | |
| SERVER_PID=$! | |
| echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV | |
| disown $SERVER_PID 2>/dev/null || true | |
| echo "Waiting for server to be ready on https://localhost:8090..." | |
| for i in {1..60}; do | |
| if curl -s -k https://localhost:8090/health/liveness > /dev/null; then | |
| echo "Server is UP!" | |
| break | |
| fi | |
| echo "Still waiting ($i/60)..." | |
| sleep 2 | |
| done | |
| if ! curl -s -k https://localhost:8090/health/liveness > /dev/null; then | |
| echo "Server failed to start within 2 minutes" | |
| exit 1 | |
| fi | |
| echo "Server started successfully!" | |
| - name: 🔑 Obtain Admin Access Token (Wayfinder) | |
| if: ${{ !cancelled() }} | |
| id: admin-token-wayfinder | |
| uses: ./.github/actions/obtain-admin-token | |
| with: | |
| admin-username: ${{ env.ADMIN_USERNAME }} | |
| admin-password: ${{ env.ADMIN_PASSWORD }} | |
| - name: 📦 Import E2E Admin App (Wayfinder) | |
| if: ${{ !cancelled() }} | |
| uses: ./.github/actions/import-declarative-config | |
| with: | |
| config-path: tests/e2e/thunderid-config.yaml | |
| admin-token: ${{ steps.admin-token-wayfinder.outputs.token }} | |
| label: E2E admin app | |
| - name: 🚀 Start Wayfinder Sample App | |
| if: ${{ !cancelled() }} | |
| run: | | |
| cd samples/apps/wayfinder-sample/frontend | |
| cp .env.example .env | |
| npm install | |
| setsid npm run dev > "$GITHUB_WORKSPACE/wayfinder-app.log" 2>&1 < /dev/null & | |
| disown $! 2>/dev/null || true | |
| echo "Waiting for Wayfinder sample app on http://localhost:5173..." | |
| for i in {1..60}; do | |
| if curl -s http://localhost:5173 > /dev/null; then | |
| echo "Wayfinder sample app is up!" | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| if ! curl -s http://localhost:5173 > /dev/null; then | |
| echo "❌ Wayfinder sample app failed to start within 2 minutes" | |
| echo "--- wayfinder-app.log ---" | |
| cat "$GITHUB_WORKSPACE/wayfinder-app.log" || true | |
| exit 1 | |
| fi | |
| - name: 🗂️ Cache Wayfinder Mock SMTP Server Dependencies | |
| if: ${{ !cancelled() }} | |
| id: cache-wayfinder-smtp | |
| uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 | |
| with: | |
| path: samples/apps/wayfinder-sample/smtp-server/node_modules | |
| key: wayfinder-smtp-server-${{ runner.os }}-${{ hashFiles('samples/apps/wayfinder-sample/smtp-server/package.json') }} | |
| - name: 🚀 Start Wayfinder Mock SMTP Server | |
| if: ${{ !cancelled() }} | |
| run: | | |
| cd samples/apps/wayfinder-sample/smtp-server | |
| if [ "${{ steps.cache-wayfinder-smtp.outputs.cache-hit }}" != "true" ]; then | |
| npm install | |
| fi | |
| npm run build | |
| setsid node src/index.js > "$GITHUB_WORKSPACE/wayfinder-smtp-server.log" 2>&1 < /dev/null & | |
| disown $! 2>/dev/null || true | |
| # Defaults (127.0.0.1:2525 SMTP, 127.0.0.1:8788 inbox) match the server distribution's | |
| # deployment.yaml email.smtp settings exactly, so no .env or config changes are needed. | |
| echo "Waiting for Wayfinder mock SMTP inbox on http://localhost:8788..." | |
| for i in {1..60}; do | |
| if curl -s http://localhost:8788/health > /dev/null; then | |
| echo "Wayfinder mock SMTP inbox is up!" | |
| break | |
| fi | |
| sleep 2 | |
| done | |
| if ! curl -s http://localhost:8788/health > /dev/null; then | |
| echo "❌ Wayfinder mock SMTP inbox failed to start within 2 minutes" | |
| echo "--- wayfinder-smtp-server.log ---" | |
| cat "$GITHUB_WORKSPACE/wayfinder-smtp-server.log" || true | |
| exit 1 | |
| fi | |
| - name: 🎭 Run Playwright E2E Tests (Wayfinder) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| if [ -n "$PLAYWRIGHT_PROJECT_FILTER" ]; then | |
| echo "Running Playwright projects matching '$PLAYWRIGHT_PROJECT_FILTER'" | |
| pnpm test --project $PLAYWRIGHT_PROJECT_FILTER --output=test-results-wayfinder --grep @wayfinder --pass-with-no-tests | |
| else | |
| echo "Running every Playwright project" | |
| pnpm test --output=test-results-wayfinder --grep @wayfinder --pass-with-no-tests | |
| fi | |
| working-directory: tests/e2e | |
| env: | |
| # wayfinder-sample-setup.spec.ts and tests/wayfinder/** run through their own dedicated | |
| # projects (see playwright.config.ts), not the plain chromium/firefox/webkit ones, so PRs | |
| # name the Wayfinder projects explicitly (chromium's tryout project plus the two | |
| # browser-agnostic ones) instead of a 'chromium' or '*wayfinder-*' filter - the former | |
| # would match none of them, the latter would also pull in firefox/webkit tryout runs. | |
| PLAYWRIGHT_PROJECT_FILTER: ${{ github.event_name == 'pull_request' && 'wayfinder-setup chromium-wayfinder-tryout wayfinder-mock-email' || '' }} | |
| PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report-wayfinder | |
| PLAYWRIGHT_JSON_OUTPUT_FILE: test-results-wayfinder/test-results.json | |
| PLAYWRIGHT_JUNIT_OUTPUT_FILE: test-results-wayfinder/junit.xml | |
| BASE_URL: ${{ secrets.PLAYWRIGHT_BASE_URL || vars.PLAYWRIGHT_BASE_URL || env.DEFAULT_BASE_URL }} | |
| ADMIN_USERNAME: ${{ env.ADMIN_USERNAME }} | |
| ADMIN_PASSWORD: ${{ env.ADMIN_PASSWORD }} | |
| WAYFINDER_APP_URL: http://localhost:5173 | |
| PLAYWRIGHT_WORKERS: ${{ vars.PLAYWRIGHT_WORKERS || env.DEFAULT_PLAYWRIGHT_WORKERS }} | |
| DEBUG_AUTH: ${{ vars.PLAYWRIGHT_DEBUG_AUTH || env.DEFAULT_DEBUG_AUTH }} | |
| SERVER_URL: ${{ env.DEFAULT_SERVER_URL }} | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: ${{ !cancelled() }} | |
| with: | |
| name: playwright-report-wayfinder | |
| path: tests/e2e/playwright-report-wayfinder/ | |
| retention-days: 30 | |
| build-docs: | |
| name: 📚 Build Documentation | |
| needs: [preflight] | |
| if: ${{ needs.preflight.outputs.docs-changed == 'true' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: 📚 Build docs | |
| run: make build_docs | |
| validate-windows: | |
| name: 🪟 Validate Windows PowerShell | |
| needs: preflight | |
| if: needs.preflight.outputs.powershell-changed == 'true' | |
| uses: ./.github/workflows/windows-powershell-validation.yml | |
| cleanup-turbo-cache: | |
| name: 🗑️ Cleanup Stale Turbo Cache | |
| runs-on: ubuntu-latest | |
| # Skip on fork PRs — GITHUB_TOKEN is always read-only there regardless of permissions declared here. | |
| if: always() && github.event.pull_request.head.repo.full_name == github.repository && contains(github.event.pull_request.labels.*.name, 'trigger-pr-builder') && (github.event.action != 'labeled' || github.event.label.name == 'trigger-pr-builder') | |
| needs: | |
| - build | |
| - test-frontend-packages | |
| - test-frontend-gate-app | |
| - test-frontend-console-app | |
| - test-integration | |
| timeout-minutes: 5 | |
| permissions: | |
| actions: write | |
| steps: | |
| - name: 🗑️ Delete superseded Turbo and pnpm store caches | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CACHE_REF: ${{ github.ref }} | |
| run: | | |
| # Both cache families end in "--<run id>-<attempt>", so each run and each | |
| # re-run publishes a fresh entry, and the older ones for the same lineage | |
| # (the key without that suffix) can never be reached by a restore-key | |
| # again. The separator is a | |
| # double dash because a hex lockfile hash cannot contain one, so trimming | |
| # it can never mangle a key that predates this scheme; those are left | |
| # alone and expire on their own. | |
| # | |
| # Scoped to this run's ref on purpose: GitHub stores caches per branch, | |
| # and the previous version of this step matched every ref, so it deleted | |
| # other pull requests' warm caches along with this one's stale copies. | |
| gh api --paginate \ | |
| "repos/${{ github.repository }}/actions/caches?per_page=100&ref=${CACHE_REF}" \ | |
| --jq '.actions_caches[] | |
| | select(.key | test("^(turbo|pnpm-store)-")) | |
| | select(.key | test("--[0-9]+-[0-9]+$")) | |
| | {id, created_at, lineage: (.key | sub("--[0-9]+-[0-9]+$"; ""))}' \ | |
| | jq -s 'group_by(.lineage)[] | |
| | sort_by(.created_at) | reverse | .[1:] | .[].id' \ | |
| | xargs -r -I{} gh api --method DELETE "repos/${{ github.repository }}/actions/caches/{}" |