# Copyright 2025 Collate # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This workflow executes SSO-specific end-to-end (e2e) tests using Playwright with PostgreSQL as the database. # # Purpose: # - Run SSO token renewal E2E tests using a mock OIDC provider on PRs # - Run SSO configuration tests for various providers (Google, Azure AD, Okta, etc.) via manual trigger # - Tests run in isolation from the regular sharded Playwright E2E tests # # Triggers: # - Pull requests with "safe to test" label (mock-oidc only, Chromium + WebKit) # - Manual trigger via workflow_dispatch (any SSO provider, Chromium + WebKit) # # Test Location: # - openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/SSOAuthentication.spec.ts (mock-oidc) name: SSO Playwright E2E Tests on: pull_request_target: types: - labeled - opened - synchronize - reopened - ready_for_review paths: - "openmetadata-ui/src/main/resources/ui/src/components/Auth/**" - "openmetadata-ui/src/main/resources/ui/src/rest/auth-API*" - "openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider*" - "openmetadata-ui/src/main/resources/ui/src/utils/TokenServiceUtil*" - "openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils*" - "openmetadata-ui/src/main/resources/ui/src/hooks/useSignIn*" - "openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/**" - "openmetadata-ui/src/main/resources/ui/playwright/utils/mockOidc*" - "openmetadata-ui/src/main/resources/ui/playwright.sso.config.ts" - "docker/development/mock-oidc-provider/**" - "docker/development/docker-compose.yml" - "docker/development/.env.sso-test" - ".github/workflows/playwright-sso-tests.yml" workflow_dispatch: inputs: sso_provider: description: "SSO Provider to test" required: true default: "mock-oidc" type: choice options: - mock-oidc - google - okta - azure - auth0 - saml - cognito permissions: contents: read pull-requests: write concurrency: group: sso-playwright-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: ${{ github.event_name != 'pull_request_target' || github.event.action != 'labeled' || github.event.label.name == 'safe to test' }} jobs: sso-auth-tests: runs-on: ubuntu-latest if: ${{ !github.event.pull_request.draft && (github.event_name != 'pull_request_target' || github.event.action != 'labeled' || github.event.label.name == 'safe to test') }} environment: test strategy: fail-fast: false matrix: provider: ${{ github.event_name == 'pull_request_target' && fromJSON('["mock-oidc"]') || github.event.inputs.sso_provider && fromJSON(format('["{0}"]', github.event.inputs.sso_provider)) || fromJSON('["mock-oidc"]') }} steps: - name: Free Disk Space (Ubuntu) uses: jlumbroso/free-disk-space@main with: tool-cache: false android: true dotnet: true haskell: true large-packages: false swap-storage: true docker-images: false - name: Wait for the labeler uses: lewagon/wait-on-check-action@v1.7.0 if: ${{ github.event_name == 'pull_request_target' }} with: ref: ${{ github.event.pull_request.head.sha }} check-name: Team Label repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 90 - name: Verify PR labels uses: jesusvasquez333/verify-pr-label-action@v1.4.0 if: ${{ github.event_name == 'pull_request_target' }} with: github-token: "${{ secrets.GITHUB_TOKEN }}" valid-labels: "safe to test" pull-request-number: "${{ github.event.pull_request.number }}" disable-reviews: true - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Cache Maven Dependencies id: cache-output uses: actions/cache@v4 with: path: ~/.m2 key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - name: Start Mock OIDC Provider if: ${{ matrix.provider == 'mock-oidc' }} run: | cd docker/development docker compose --profile sso-test build mock-oidc-provider docker compose --profile sso-test up -d mock-oidc-provider echo "Waiting for mock OIDC provider to be healthy..." for i in $(seq 1 30); do if curl -sf http://localhost:9090/health > /dev/null 2>&1; then echo "Mock OIDC provider is ready" break fi echo "Waiting... ($i/30)" sleep 2 done curl -sf http://localhost:9090/.well-known/openid-configuration || echo "WARNING: Discovery endpoint not ready" - name: Setup Openmetadata Test Environment uses: ./.github/actions/setup-openmetadata-test-environment with: python-version: "3.10" args: "-d postgresql -i false" ingestion_dependency: "all" env: AUTHENTICATION_PROVIDER: ${{ matrix.provider == 'mock-oidc' && 'custom-oidc' || 'basic' }} CUSTOM_OIDC_AUTHENTICATION_PROVIDER_NAME: ${{ matrix.provider == 'mock-oidc' && 'mock-oidc' || '' }} AUTHENTICATION_PUBLIC_KEYS: ${{ matrix.provider == 'mock-oidc' && '[http://localhost:9090/jwks,http://localhost:8585/api/v1/system/config/jwks]' || '[http://localhost:8585/api/v1/system/config/jwks]' }} AUTHENTICATION_AUTHORITY: ${{ matrix.provider == 'mock-oidc' && 'http://localhost:9090' || 'https://accounts.google.com' }} AUTHENTICATION_CLIENT_ID: ${{ matrix.provider == 'mock-oidc' && 'openmetadata-test' || '' }} AUTHENTICATION_CALLBACK_URL: ${{ matrix.provider == 'mock-oidc' && 'http://localhost:8585/callback' || '' }} AUTHENTICATION_JWT_PRINCIPAL_CLAIMS_MAPPING: ${{ matrix.provider == 'mock-oidc' && '[username:sub,email:email]' || '' }} OIDC_CLIENT_ID: ${{ matrix.provider == 'mock-oidc' && 'openmetadata-test' || '' }} OIDC_TYPE: ${{ matrix.provider == 'mock-oidc' && 'custom-oidc' || '' }} OIDC_CLIENT_SECRET: ${{ matrix.provider == 'mock-oidc' && 'openmetadata-test-secret' || '' }} OIDC_DISCOVERY_URI: ${{ matrix.provider == 'mock-oidc' && 'http://localhost:9090/.well-known/openid-configuration' || '' }} OIDC_CALLBACK: ${{ matrix.provider == 'mock-oidc' && 'http://localhost:8585/callback' || 'http://localhost:8585/callback' }} OIDC_SERVER_URL: ${{ matrix.provider == 'mock-oidc' && 'http://localhost:8585' || 'http://localhost:8585' }} AUTHENTICATION_CLIENT_TYPE: ${{ matrix.provider == 'mock-oidc' && 'confidential' || 'public' }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version-file: 'openmetadata-ui/src/main/resources/ui/.nvmrc' - name: Install dependencies working-directory: openmetadata-ui/src/main/resources/ui/ run: yarn --ignore-scripts --frozen-lockfile - name: Install Playwright Browsers run: npx playwright@1.51.1 install chromium webkit --with-deps - name: Run Mock OIDC Authentication Tests if: ${{ matrix.provider == 'mock-oidc' }} working-directory: openmetadata-ui/src/main/resources/ui run: npx playwright test --config=playwright.sso.config.ts --workers=1 env: MOCK_OIDC_URL: http://localhost:9090 PLAYWRIGHT_IS_OSS: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 - name: Run SSO Authentication Tests if: ${{ matrix.provider != 'mock-oidc' }} working-directory: openmetadata-ui/src/main/resources/ui run: npx playwright test --config=playwright.sso.config.ts --workers=1 env: SSO_PROVIDER_TYPE: ${{ matrix.provider }} SSO_USERNAME: ${{ secrets[format('{0}_SSO_USERNAME', upper(matrix.provider))] }} SSO_PASSWORD: ${{ secrets[format('{0}_SSO_PASSWORD', upper(matrix.provider))] }} PLAYWRIGHT_IS_OSS: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} timeout-minutes: 60 - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: sso-auth-test-results-${{ matrix.provider }} path: | openmetadata-ui/src/main/resources/ui/playwright/output/sso-playwright-report openmetadata-ui/src/main/resources/ui/playwright/output/sso-test-results retention-days: 5 - name: Upload results JSON for summary uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: sso-results-json-${{ matrix.provider }} path: openmetadata-ui/src/main/resources/ui/playwright/output/sso-results.json retention-days: 1 if-no-files-found: ignore - name: Clean Up run: | cd ./docker/development docker compose --profile sso-test down --remove-orphans 2>/dev/null || true docker compose down --remove-orphans sudo rm -rf ${PWD}/docker-volume sso-summary: if: ${{ !cancelled() && github.event_name == 'pull_request_target' && (github.event.action != 'labeled' || github.event.label.name == 'safe to test') }} needs: sso-auth-tests runs-on: ubuntu-latest steps: - name: Download results JSON uses: actions/download-artifact@v4 with: pattern: sso-results-json-* path: results - name: Post SSO test summary as PR comment uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const path = require('path'); const runId = '${{ github.run_id }}'; const repo = context.repo; const prNumber = context.payload.pull_request?.number; if (!prNumber) return; const artifactUrl = `https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId}`; const commentMarker = ''; const allTests = []; const resultsDir = 'results'; if (fs.existsSync(resultsDir)) { for (const dir of fs.readdirSync(resultsDir).sort()) { const jsonPath = path.join(resultsDir, dir, 'sso-results.json'); if (!fs.existsSync(jsonPath)) continue; const report = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); function collectTests(suite, filePath) { const file = suite.file || filePath || ''; for (const spec of (suite.specs || [])) { for (const test of (spec.tests || [])) { const results = test.results || []; const lastResult = results[results.length - 1] || {}; const firstResult = results[0] || {}; allTests.push({ title: spec.title, project: test.projectName || '', file: file, status: test.status, retries: results.length - 1, error: lastResult.error?.message || firstResult.error?.message || '', }); } } for (const child of (suite.suites || [])) { collectTests(child, file); } } for (const suite of (report.suites || [])) { collectTests(suite, ''); } } } if (allTests.length === 0) { console.log('No SSO test results found'); return; } const passed = allTests.filter(t => t.status === 'expected'); const failed = allTests.filter(t => t.status === 'unexpected'); const flaky = allTests.filter(t => t.status === 'flaky'); const skipped = allTests.filter(t => t.status === 'skipped'); const lines = [commentMarker]; if (failed.length > 0) { lines.push(`## ๐Ÿ”ด SSO Playwright Results โ€” ${failed.length} failure(s)${flaky.length > 0 ? `, ${flaky.length} flaky` : ''}`); } else if (flaky.length > 0) { lines.push(`## ๐ŸŸก SSO Playwright Results โ€” all passed (${flaky.length} flaky)`); } else { lines.push(`## โœ… SSO Playwright Results โ€” all ${passed.length} tests passed`); } lines.push(''); lines.push(`โœ… ${passed.length} passed ยท โŒ ${failed.length} failed ยท ๐ŸŸก ${flaky.length} flaky ยท โญ๏ธ ${skipped.length} skipped`); lines.push(''); if (failed.length > 0) { lines.push('### Failures'); lines.push(''); for (const t of failed.slice(0, 20)) { lines.push(`
โŒ ${t.project} โ€บ ${t.title}`); lines.push(''); lines.push('```'); lines.push(t.error.substring(0, 1000)); lines.push('```'); lines.push('
'); lines.push(''); } } if (flaky.length > 0) { lines.push(`
๐ŸŸก ${flaky.length} flaky test(s)`); lines.push(''); for (const t of flaky.slice(0, 20)) { lines.push(`- ${t.project} โ€บ ${t.title} (${t.retries} ${t.retries === 1 ? 'retry' : 'retries'})`); } lines.push(''); lines.push('
'); lines.push(''); } lines.push(`๐Ÿ“ฆ [View run details](${artifactUrl})`); const body = lines.join('\n'); let existingComment = null; for await (const response of github.paginate.iterator( github.rest.issues.listComments, { ...repo, issue_number: prNumber, per_page: 100 } )) { const found = response.data.find(c => c.user?.login === 'github-actions[bot]' && c.body?.includes(commentMarker) ); if (found) { existingComment = found; break; } } if (existingComment) { await github.rest.issues.updateComment({ ...repo, comment_id: existingComment.id, body }); } else { await github.rest.issues.createComment({ ...repo, issue_number: prNumber, body }); }