Improve handling default certs and keys #15316
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 | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| merge_group: | |
| workflow_dispatch: | |
| # Avoid running multiple PR builders for the same PR on subsequent pushes. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ 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/*" | |
| PNPM_VERSION: "latest" | |
| jobs: | |
| security-audit: | |
| name: 🔒 Security Audit | |
| if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| 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: 🔍 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 | |
| dependency-review: | |
| name: 🔎 Dependency Review | |
| if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: 🔎 Dependency Review | |
| 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 }} | |
| verify-mocks: | |
| name: 🔍 Verify Mock Files | |
| if: ${{ github.event_name == 'pull_request' || 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 | |
| if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| with: | |
| # We need to fetch all branches and commits so that Turbo's --affected has a base to compare against. | |
| fetch-depth: 0 | |
| - name: ⚙️ Set up Node.js and pnpm | |
| uses: ./.github/actions/setup-pnpm | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: 🔄 Fetch main branch for Turbo affected base | |
| run: git fetch origin main:main --update-head-ok || true | |
| - 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 | |
| - 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: main | |
| - name: 🎨 Check Frontend Formatting | |
| run: | | |
| pnpm format:check | |
| build: | |
| name: 🛠️ Build Product | |
| needs: [detect-code-changes] | |
| if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'merge_group') && (needs.detect-code-changes.result == 'skipped' || needs.detect-code-changes.outputs.should-run == 'true') }} | |
| 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 Product with Coverage | |
| run: | | |
| set -e | |
| export LOG_LEVEL=debug | |
| make build_with_coverage_only OS=$(go env GOOS) ARCH=$(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 | |
| - name: 📊 Upload Unit Test Coverage Report to Codecov | |
| 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 | |
| detect-code-changes: | |
| name: 🔍 Detect Code Changes | |
| if: github.event_name == 'pull_request' || github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| should-run: ${{ steps.filter.outputs.code }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: 🔍 Detect non-docs file changes | |
| id: filter | |
| # TODO: Temporarily always treat as code changes to unblock docs-only PRs hanging on the | |
| # codecov/patch status check. Revisit once the workflow_run-based fix is in place. | |
| # env: | |
| # BASE_SHA: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event.pull_request.base.sha }} | |
| run: | | |
| # git fetch --depth=1 origin "$BASE_SHA" | |
| # CHANGED=$(git diff --name-only "$BASE_SHA" HEAD) | |
| # echo "Changed files:" | |
| # echo "$CHANGED" | |
| # NON_DOCS=$(echo "$CHANGED" | grep -v '^docs/' | grep -v -i 'readme\.md' | grep -c . || true) | |
| # echo "Non-docs changed file count: $NON_DOCS" | |
| # if [ "$NON_DOCS" -gt 0 ]; then | |
| # echo "code=true" >> $GITHUB_OUTPUT | |
| # else | |
| # echo "code=false" >> $GITHUB_OUTPUT | |
| # fi | |
| echo "code=true" >> $GITHUB_OUTPUT | |
| # - name: ✅ Code changes detected | |
| # if: steps.filter.outputs.code == 'true' | |
| # run: echo "Non-documentation files changed - frontend and E2E tests will run" | |
| # - name: ⏭️ Docs-only changes | |
| # if: steps.filter.outputs.code != 'true' | |
| # run: echo "Only documentation changes detected - skipping frontend and E2E tests" | |
| test-frontend-packages: | |
| name: 🧪 Frontend Packages Tests | |
| needs: [detect-code-changes] | |
| if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'merge_group') && (needs.detect-code-changes.result == 'skipped' || needs.detect-code-changes.outputs.should-run == '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: 📦 Install Frontend Dependencies | |
| run: | | |
| pnpm install --frozen-lockfile | |
| - name: 🗂️ Set up Turbo Cache | |
| uses: ./.github/actions/setup-turbo-cache | |
| - name: 🎭 Set up Playwright Browsers | |
| uses: ./.github/actions/setup-playwright | |
| with: | |
| working-directory: frontend | |
| scope: packages | |
| - name: 🔨 Build Frontend Packages | |
| working-directory: frontend | |
| run: | | |
| pnpm build:packages | |
| - name: 🧪 Run Tests for Frontend Packages | |
| working-directory: frontend | |
| run: | | |
| pnpm test:packages | |
| test-frontend-gate-app: | |
| name: 🧪 Gate App Tests with Coverage | |
| needs: [detect-code-changes] | |
| if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'merge_group') && (needs.detect-code-changes.result == 'skipped' || needs.detect-code-changes.outputs.should-run == '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: 📦 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 | |
| test-frontend-console-app: | |
| name: 🧪 Console App Tests with Coverage | |
| needs: [detect-code-changes] | |
| if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'merge_group') && (needs.detect-code-changes.result == 'skipped' || needs.detect-code-changes.outputs.should-run == 'true') }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| 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 | |
| run: | | |
| cd frontend/apps/console | |
| 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 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 | |
| - 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] | |
| if: always() && (needs.test-frontend-gate-app.result == 'success' && needs.test-frontend-console-app.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: 📊 Upload `@thunderid/console` Unit Test Coverage Report to Codecov | |
| 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 | |
| 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 | |
| build_samples: | |
| name: 🛠️ Build Sample Apps | |
| needs: [detect-code-changes] | |
| if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'merge_group') && (needs.detect-code-changes.result == 'skipped' || needs.detect-code-changes.outputs.should-run == 'true') }} | |
| 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 | |
| test-integration: | |
| name: 🧪 Integration Tests (${{ matrix.database }}) | |
| needs: [build, build_samples, detect-code-changes] | |
| 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: 📥 Download Unit Coverage Report | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| name: unit-coverage-report | |
| path: backend/ | |
| - 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: 📊 Upload Integration Test Coverage Report to Codecov | |
| 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 | |
| test-integration-status: | |
| name: ✅ Integration Tests Status | |
| needs: [test-integration] | |
| if: always() | |
| 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, detect-code-changes] | |
| if: ${{ always() && (needs.build.result == 'success' && needs.build_samples.result == 'success' && (needs.detect-code-changes.result == 'skipped' || needs.detect-code-changes.outputs.should-run == 'true')) }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: 📥 Checkout Code | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - name: ⚙️ Set up Node.js | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 | |
| with: | |
| node-version: 'lts/*' | |
| - name: 📦 Install pnpm | |
| uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 | |
| with: | |
| run_install: false | |
| cache_dependency_path: pnpm-lock.yaml | |
| version: ${{ env.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: 🚀 Start the Server | |
| run: | | |
| cd tests/e2e/server | |
| ./setup.sh | |
| ./start.sh & | |
| SERVER_PID=$! | |
| echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV | |
| # 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 | |
| run: | | |
| # The server runs with security enabled, so the management /import and | |
| # /applications calls below need an admin token. | |
| # Obtain one via OAuth2 authorization code + PKCE through the CONSOLE app, | |
| # authenticating the bootstrapped admin user. | |
| SERVER_URL="https://localhost:8090" | |
| CONSOLE_REDIRECT_URI="https://localhost:8090/console" | |
| CODE_VERIFIER=$(openssl rand -hex 32 | cut -c1-43) | |
| CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=') | |
| curl -sk -o /dev/null -D /tmp/authz_headers.txt -G "$SERVER_URL/oauth2/authorize" \ | |
| --data-urlencode "client_id=CONSOLE" \ | |
| --data-urlencode "redirect_uri=$CONSOLE_REDIRECT_URI" \ | |
| --data-urlencode "scope=system" \ | |
| --data-urlencode "response_type=code" \ | |
| --data-urlencode "code_challenge=$CODE_CHALLENGE" \ | |
| --data-urlencode "code_challenge_method=S256" | |
| LOCATION=$(grep -i "^location:" /tmp/authz_headers.txt | tr -d '\r' | sed 's/^[Ll]ocation: //') | |
| AUTH_ID=$(echo "$LOCATION" | sed 's/.*[?&]authId=\([^&]*\).*/\1/') | |
| EXEC_ID=$(echo "$LOCATION" | sed 's/.*[?&]executionId=\([^&]*\).*/\1/') | |
| if [ -z "$AUTH_ID" ] || [ -z "$EXEC_ID" ]; then | |
| echo "❌ Failed to parse authId/executionId from authorize redirect: $LOCATION" | |
| exit 1 | |
| fi | |
| FLOW_RESP=$(curl -sk -X POST "$SERVER_URL/flow/execute" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"executionId\": \"$EXEC_ID\", \"inputs\": {\"username\": \"admin\", \"password\": \"admin\"}, \"action\": \"action_001\"}") | |
| ASSERTION=$(echo "$FLOW_RESP" | jq -r '.assertion // empty') | |
| if [ -z "$ASSERTION" ]; then | |
| echo "❌ Flow execution did not return an assertion: $FLOW_RESP" | |
| exit 1 | |
| fi | |
| CALLBACK_RESP=$(curl -sk -X POST "$SERVER_URL/oauth2/auth/callback" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"authId\": \"$AUTH_ID\", \"assertion\": \"$ASSERTION\"}") | |
| AUTH_CODE=$(echo "$CALLBACK_RESP" | jq -r '.redirect_uri // empty' | sed 's/.*[?&]code=\([^&]*\).*/\1/') | |
| if [ -z "$AUTH_CODE" ]; then | |
| echo "❌ Callback did not return an authorization code: $CALLBACK_RESP" | |
| exit 1 | |
| fi | |
| TOKEN_RESP=$(curl -sk -X POST "$SERVER_URL/oauth2/token" \ | |
| -H "Content-Type: application/x-www-form-urlencoded" \ | |
| --data-urlencode "grant_type=authorization_code" \ | |
| --data-urlencode "code=$AUTH_CODE" \ | |
| --data-urlencode "redirect_uri=$CONSOLE_REDIRECT_URI" \ | |
| --data-urlencode "client_id=CONSOLE" \ | |
| --data-urlencode "code_verifier=$CODE_VERIFIER") | |
| ADMIN_TOKEN=$(echo "$TOKEN_RESP" | jq -r '.access_token // empty') | |
| if [ -z "$ADMIN_TOKEN" ]; then | |
| echo "❌ Failed to obtain admin access token: $TOKEN_RESP" | |
| exit 1 | |
| fi | |
| echo "::add-mask::$ADMIN_TOKEN" | |
| echo "ADMIN_TOKEN=$ADMIN_TOKEN" >> "$GITHUB_ENV" | |
| echo "✅ Obtained admin access token." | |
| - name: 📦 Import Sample App Resources | |
| run: | | |
| echo "Importing React Vanilla Sample declarative resources..." | |
| YAML_CONTENT=$(cat samples/apps/react-vanilla-sample/thunderid-config/basic/thunderid-config.yaml) | |
| HTTP_STATUS=$(curl -k -s -o /tmp/import_response.json -w "%{http_code}" \ | |
| -X POST https://localhost:8090/import \ | |
| -H 'Content-Type: application/json' \ | |
| -H "Authorization: Bearer $ADMIN_TOKEN" \ | |
| -d "{ | |
| \"content\": $(echo "$YAML_CONTENT" | jq -Rs .), | |
| \"variables\": { | |
| \"SAMPLE_APP_CLIENT_ID\": \"sample_app_client\", | |
| \"SAMPLE_APP_REDIRECT_URIS\": [\"https://localhost:3000\"], | |
| \"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\" | |
| }, | |
| \"options\": {\"upsert\": true} | |
| }") | |
| echo "Import HTTP status: $HTTP_STATUS" | |
| cat /tmp/import_response.json | jq . || cat /tmp/import_response.json | |
| if [ "$HTTP_STATUS" != "200" ]; then | |
| echo "❌ Failed to import React Vanilla Sample resources (HTTP $HTTP_STATUS)" | |
| exit 1 | |
| fi | |
| echo "✅ React Vanilla Sample resources imported successfully" | |
| echo "Importing React SDK Sample declarative resources..." | |
| YAML_CONTENT=$(cat samples/apps/react-sdk-sample/thunderid-config/thunderid-config.yaml) | |
| HTTP_STATUS=$(curl -k -s -o /tmp/import_response.json -w "%{http_code}" \ | |
| -X POST https://localhost:8090/import \ | |
| -H 'Content-Type: application/json' \ | |
| -H "Authorization: Bearer $ADMIN_TOKEN" \ | |
| -d "{ | |
| \"content\": $(echo "$YAML_CONTENT" | jq -Rs .), | |
| \"variables\": { | |
| \"REACT_SDK_SAMPLE_CLIENT_ID\": \"REACT_SDK_SAMPLE\", | |
| \"REACT_SDK_SAMPLE_REDIRECT_URIS\": [\"https://localhost:3000\"] | |
| }, | |
| \"options\": {\"upsert\": true} | |
| }") | |
| echo "Import HTTP status: $HTTP_STATUS" | |
| cat /tmp/import_response.json | jq . || cat /tmp/import_response.json | |
| if [ "$HTTP_STATUS" != "200" ]; then | |
| echo "❌ Failed to import React SDK Sample resources (HTTP $HTTP_STATUS)" | |
| exit 1 | |
| fi | |
| echo "✅ React SDK Sample resources imported successfully" | |
| echo "Importing E2E test infrastructure resources..." | |
| YAML_CONTENT=$(cat tests/e2e/thunderid-config.yaml) | |
| HTTP_STATUS=$(curl -k -s -o /tmp/import_response.json -w "%{http_code}" \ | |
| -X POST https://localhost:8090/import \ | |
| -H 'Content-Type: application/json' \ | |
| -H "Authorization: Bearer $ADMIN_TOKEN" \ | |
| -d "{ | |
| \"content\": $(echo "$YAML_CONTENT" | jq -Rs .), | |
| \"options\": {\"upsert\": true} | |
| }") | |
| echo "Import HTTP status: $HTTP_STATUS" | |
| cat /tmp/import_response.json | jq . || cat /tmp/import_response.json | |
| if [ "$HTTP_STATUS" != "200" ]; then | |
| echo "❌ Failed to import E2E test infrastructure resources (HTTP $HTTP_STATUS)" | |
| exit 1 | |
| fi | |
| echo "✅ E2E test infrastructure resources imported successfully" | |
| - name: 🔍 Extract Sample App ID | |
| id: extract-app-id | |
| run: | | |
| echo "Fetching applications from the server..." | |
| # Get applications list | |
| APPS_RESPONSE=$(curl -s -k -H "Authorization: Bearer $ADMIN_TOKEN" https://localhost:8090/applications) | |
| # Extract Sample App ID using jq (uses "Sample App" from vanilla sample, | |
| # which is unaffected by MFA test setup/teardown) | |
| SAMPLE_APP_ID=$(echo "$APPS_RESPONSE" | jq -r '(.applications // [])[] | select(.name == "Sample App") | .id') | |
| if [ -z "$SAMPLE_APP_ID" ] || [ "$SAMPLE_APP_ID" == "null" ]; then | |
| echo "⚠️ Warning: Sample App not found in applications list" | |
| echo "Available applications:" | |
| echo "$APPS_RESPONSE" | jq -r '(.applications // [])[] | " - \(.name) (\(.id))"' | |
| echo "sample_app_id=" >> $GITHUB_OUTPUT | |
| else | |
| echo "✓ Sample App ID extracted: $SAMPLE_APP_ID" | |
| echo "sample_app_id=$SAMPLE_APP_ID" >> $GITHUB_OUTPUT | |
| fi | |
| - 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..." | |
| ./start.sh & | |
| SERVER_PID=$! | |
| echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV | |
| # 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=WSO2/OU=${{ env.PRODUCT_NAME }}/CN=localhost" \ | |
| > /dev/null 2>&1 | |
| echo "Starting Sample App..." | |
| npm run dev & | |
| # 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!" | |
| - name: 📦 Install E2E Dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: 🎭 Set up Playwright Browsers | |
| uses: ./.github/actions/setup-playwright | |
| with: | |
| working-directory: tests/e2e | |
| scope: e2e | |
| timeout-minutes: 5 | |
| - name: 🎭 Run Playwright E2E Tests | |
| run: pnpm test | |
| working-directory: tests/e2e | |
| env: | |
| # Configuration Priority: Secret > Variable > Hardcoded Default | |
| BASE_URL: ${{ secrets.PLAYWRIGHT_BASE_URL || vars.PLAYWRIGHT_BASE_URL || 'https://localhost:8090' }} | |
| ADMIN_USERNAME: ${{ secrets.PLAYWRIGHT_ADMIN_USERNAME || 'admin' }} | |
| ADMIN_PASSWORD: ${{ secrets.PLAYWRIGHT_ADMIN_PASSWORD || 'admin' }} | |
| TEST_USER_USERNAME: ${{ secrets.PLAYWRIGHT_TEST_USER_USERNAME || 'testuser' }} | |
| TEST_USER_PASSWORD: ${{ secrets.PLAYWRIGHT_TEST_USER_PASSWORD || 'admin' }} | |
| PLAYWRIGHT_WORKERS: ${{ vars.PLAYWRIGHT_WORKERS || 1 }} | |
| DEBUG_AUTH: ${{ vars.PLAYWRIGHT_DEBUG_AUTH || 'false' }} | |
| # MFA Test Configuration | |
| SAMPLE_APP_ID: ${{ steps.extract-app-id.outputs.sample_app_id }} | |
| SAMPLE_APP_URL: 'https://localhost:3000' | |
| SERVER_URL: 'https://localhost:8090' | |
| AUTO_SETUP_MFA: ${{ vars.AUTO_SETUP_MFA || 'true' }} | |
| MOCK_SMS_SERVER_PORT: ${{ vars.MOCK_SMS_SERVER_PORT || 8098 }} | |
| SAMPLE_APP_USERNAME: ${{ secrets.SAMPLE_APP_USERNAME || 'e2e-test-user' }} | |
| SAMPLE_APP_PASSWORD: ${{ secrets.SAMPLE_APP_PASSWORD || 'e2e-test-password' }} | |
| - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 | |
| if: ${{ !cancelled() }} | |
| with: | |
| name: playwright-report | |
| path: tests/e2e/playwright-report/ | |
| retention-days: 30 | |
| detect-powershell-changes: | |
| name: 🔍 Detect PowerShell Changes | |
| if: github.event_name == 'pull_request' || github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| outputs: | |
| should-run: ${{ steps.filter.outputs.powershell }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - 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: | | |
| powershell: | |
| - '**.ps1' | |
| - '.github/workflows/windows-powershell-validation.yml' | |
| - name: ✅ PowerShell files detected | |
| if: steps.filter.outputs.powershell == 'true' | |
| run: echo "PowerShell files changed - validation will run" | |
| - name: ⏭️ No PowerShell changes | |
| if: steps.filter.outputs.powershell != 'true' | |
| run: echo "No PowerShell changes - skipping validation" | |
| detect-docs-changes: | |
| name: 🔍 Detect Documentation Changes | |
| if: github.event_name == 'pull_request' || github.event_name == 'merge_group' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| should-run: ${{ steps.filter.outputs.docs }} | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | |
| - 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' | |
| - name: ✅ Documentation changes detected | |
| if: steps.filter.outputs.docs == 'true' | |
| run: echo "Documentation files changed - docs build will run" | |
| - name: ⏭️ No documentation changes | |
| if: steps.filter.outputs.docs != 'true' | |
| run: echo "No documentation changes - skipping docs build" | |
| build-docs: | |
| name: 📚 Build Documentation | |
| needs: [detect-docs-changes] | |
| if: ${{ always() && needs.detect-docs-changes.outputs.should-run == '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: detect-powershell-changes | |
| if: needs.detect-powershell-changes.outputs.should-run == '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 | |
| needs: | |
| - build | |
| - test-frontend-packages | |
| - test-frontend-gate-app | |
| - test-frontend-console-app | |
| - test-integration | |
| timeout-minutes: 5 | |
| permissions: | |
| actions: write | |
| steps: | |
| - name: 🗑️ Delete stale Turbo caches | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| # Keep the single most-recently-created Turbo cache per OS; delete the rest. | |
| gh api "repos/${{ github.repository }}/actions/caches?per_page=100&key=turbo-Linux-" \ | |
| --jq '.actions_caches | sort_by(.created_at) | reverse | .[1:] | .[].id' \ | |
| | xargs -r -I{} gh api --method DELETE "repos/${{ github.repository }}/actions/caches/{}" | |