99# See the License for the specific language governing permissions and
1010# limitations under the License.
1111
12- # This workflow executes SSO-specific end-to-end (e2e) tests using Playwright with MySQL as the database.
13- #
12+ # This workflow executes SSO-specific end-to-end (e2e) tests using Playwright with PostgreSQL as the database.
13+ #
1414# Purpose:
15- # - Run SSO configuration tests for various authentication providers (Google, Azure AD, Okta, SAML, LDAP, etc.)
16- # - Validate SSO provider selection, field visibility, and configuration workflows
17- # - Tests are tagged with @sso and run in isolation from other Playwright tests
15+ # - Run SSO token renewal E2E tests using a mock OIDC provider on PRs
16+ # - Run SSO configuration tests for various providers (Google, Azure AD, Okta, etc.) via manual trigger
17+ # - Tests run in isolation from the regular sharded Playwright E2E tests
1818#
1919# Triggers:
20- # - Manual trigger via workflow_dispatch
21- # - Pull requests with "safe to test" label
22- # - Excludes draft PRs
20+ # - Pull requests with "safe to test" label (mock-oidc only, Chromium + WebKit)
21+ # - Manual trigger via workflow_dispatch (any SSO provider, Chromium + WebKit)
2322#
2423# Test Location:
25- # - openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/SSOConfiguration.spec.ts
26- #
27- # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path
24+ # - openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/SSOAuthentication.spec.ts (mock-oidc)
2825
29- name : SSO Authentication Tests (Nightly)
26+ name : SSO Playwright E2E Tests
3027
3128on :
32- # schedule:
33- # # Run every night at 2 AM UTC
34- # - cron: '0 2 * * *'
35- workflow_dispatch : # Allow manual trigger
29+ pull_request_target :
30+ types :
31+ - labeled
32+ - opened
33+ - synchronize
34+ - reopened
35+ - ready_for_review
36+ paths :
37+ - " openmetadata-ui/src/main/resources/ui/src/components/Auth/**"
38+ - " openmetadata-ui/src/main/resources/ui/src/rest/auth-API*"
39+ - " openmetadata-ui/src/main/resources/ui/src/utils/AuthProvider*"
40+ - " openmetadata-ui/src/main/resources/ui/src/utils/TokenServiceUtil*"
41+ - " openmetadata-ui/src/main/resources/ui/src/utils/UserDataUtils*"
42+ - " openmetadata-ui/src/main/resources/ui/src/hooks/useSignIn*"
43+ - " openmetadata-ui/src/main/resources/ui/playwright/e2e/Auth/**"
44+ - " openmetadata-ui/src/main/resources/ui/playwright/utils/mockOidc*"
45+ - " openmetadata-ui/src/main/resources/ui/playwright.sso.config.ts"
46+ - " docker/development/mock-oidc-provider/**"
47+ - " docker/development/docker-compose.yml"
48+ - " docker/development/.env.sso-test"
49+ - " .github/workflows/playwright-sso-tests.yml"
50+ workflow_dispatch :
3651 inputs :
3752 sso_provider :
3853 description : " SSO Provider to test"
3954 required : true
40- default : " google "
55+ default : " mock-oidc "
4156 type : choice
4257 options :
58+ - mock-oidc
4359 - google
4460 - okta
4561 - azure
4965
5066permissions :
5167 contents : read
68+ pull-requests : write
5269
5370concurrency :
54- group : sso-auth-tests- ${{ github.workflow }}-${{ github. event.inputs.sso_provider || 'scheduled' }}
71+ group : sso-playwright- ${{ github.event.pull_request.number || github.run_id }}
5572 cancel-in-progress : true
5673
5774jobs :
6380 strategy :
6481 fail-fast : false
6582 matrix :
66- provider : ${{ github.event.inputs.sso_provider == 'all ' && fromJSON('["google", "okta", "azure", "auth0" ]') || github.event.inputs.sso_provider && fromJSON(format('["{0}"]', github.event.inputs.sso_provider)) || fromJSON('["google "]') }}
83+ 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 "]') }}
6784
6885 steps :
6986 - name : Free Disk Space (Ubuntu)
93110 github-token : " ${{ secrets.GITHUB_TOKEN }}"
94111 valid-labels : " safe to test"
95112 pull-request-number : " ${{ github.event.pull_request.number }}"
96- disable-reviews : true # To not auto approve changes
113+ disable-reviews : true
97114
98115 - name : Checkout
99116 uses : actions/checkout@v4
@@ -109,13 +126,43 @@ jobs:
109126 restore-keys : |
110127 ${{ runner.os }}-maven-
111128
129+ - name : Start Mock OIDC Provider
130+ if : ${{ matrix.provider == 'mock-oidc' }}
131+ run : |
132+ cd docker/development
133+ docker compose --profile sso-test build mock-oidc-provider
134+ docker compose --profile sso-test up -d mock-oidc-provider
135+ echo "Waiting for mock OIDC provider to be healthy..."
136+ for i in $(seq 1 30); do
137+ if curl -sf http://localhost:9090/health > /dev/null 2>&1; then
138+ echo "Mock OIDC provider is ready"
139+ break
140+ fi
141+ echo "Waiting... ($i/30)"
142+ sleep 2
143+ done
144+ curl -sf http://localhost:9090/.well-known/openid-configuration || echo "WARNING: Discovery endpoint not ready"
145+
112146 - name : Setup Openmetadata Test Environment
113147 uses : ./.github/actions/setup-openmetadata-test-environment
114148 with :
115149 python-version : " 3.10"
116- # Skip ingestion setup for SSO tests
117150 args : " -d postgresql -i false"
118151 ingestion_dependency : " all"
152+ env :
153+ AUTHENTICATION_PROVIDER : ${{ matrix.provider == 'mock-oidc' && 'custom-oidc' || 'basic' }}
154+ CUSTOM_OIDC_AUTHENTICATION_PROVIDER_NAME : ${{ matrix.provider == 'mock-oidc' && 'mock-oidc' || '' }}
155+ 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]' }}
156+ AUTHENTICATION_AUTHORITY : ${{ matrix.provider == 'mock-oidc' && 'http://localhost:9090' || 'https://accounts.google.com' }}
157+ AUTHENTICATION_CLIENT_ID : ${{ matrix.provider == 'mock-oidc' && 'openmetadata-test' || '' }}
158+ AUTHENTICATION_CALLBACK_URL : ${{ matrix.provider == 'mock-oidc' && 'http://localhost:8585/callback' || '' }}
159+ OIDC_CLIENT_ID : ${{ matrix.provider == 'mock-oidc' && 'openmetadata-test' || '' }}
160+ OIDC_TYPE : ${{ matrix.provider == 'mock-oidc' && 'custom-oidc' || '' }}
161+ OIDC_CLIENT_SECRET : ${{ matrix.provider == 'mock-oidc' && 'openmetadata-test-secret' || '' }}
162+ OIDC_DISCOVERY_URI : ${{ matrix.provider == 'mock-oidc' && 'http://localhost:9090/.well-known/openid-configuration' || '' }}
163+ OIDC_CALLBACK : ${{ matrix.provider == 'mock-oidc' && 'http://localhost:8585/callback' || 'http://localhost:8585/callback' }}
164+ OIDC_SERVER_URL : ${{ matrix.provider == 'mock-oidc' && 'http://localhost:8585' || 'http://localhost:8585' }}
165+ AUTHENTICATION_CLIENT_TYPE : ${{ matrix.provider == 'mock-oidc' && 'confidential' || 'public' }}
119166
120167 - name : Setup Node.js
121168 uses : actions/setup-node@v4
@@ -127,11 +174,22 @@ jobs:
127174 run : yarn --ignore-scripts --frozen-lockfile
128175
129176 - name : Install Playwright Browsers
130- run : npx playwright@1.51.1 install chromium --with-deps
177+ run : npx playwright@1.51.1 install chromium webkit --with-deps
178+
179+ - name : Run Mock OIDC Authentication Tests
180+ if : ${{ matrix.provider == 'mock-oidc' }}
181+ working-directory : openmetadata-ui/src/main/resources/ui
182+ run : npx playwright test --config=playwright.sso.config.ts --workers=1
183+ env :
184+ MOCK_OIDC_URL : http://localhost:9090
185+ PLAYWRIGHT_IS_OSS : true
186+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
187+ timeout-minutes : 60
131188
132189 - name : Run SSO Authentication Tests
190+ if : ${{ matrix.provider != 'mock-oidc' }}
133191 working-directory : openmetadata-ui/src/main/resources/ui
134- run : npx playwright test playwright/e2e/Auth/SSOAuthentication.spec .ts --workers=1
192+ run : npx playwright test --config= playwright.sso.config .ts --workers=1
135193 env :
136194 SSO_PROVIDER_TYPE : ${{ matrix.provider }}
137195 SSO_USERNAME : ${{ secrets[format('{0}_SSO_USERNAME', upper(matrix.provider))] }}
@@ -145,21 +203,154 @@ jobs:
145203 uses : actions/upload-artifact@v4
146204 with :
147205 name : sso-auth-test-results-${{ matrix.provider }}
148- path : openmetadata-ui/src/main/resources/ui/playwright/output/playwright-report
206+ path : |
207+ openmetadata-ui/src/main/resources/ui/playwright/output/sso-playwright-report
208+ openmetadata-ui/src/main/resources/ui/playwright/output/sso-test-results
149209 retention-days : 5
150210
211+ - name : Upload results JSON for summary
212+ uses : actions/upload-artifact@v4
213+ if : ${{ !cancelled() }}
214+ with :
215+ name : sso-results-json-${{ matrix.provider }}
216+ path : openmetadata-ui/src/main/resources/ui/playwright/output/sso-results.json
217+ retention-days : 1
218+ if-no-files-found : ignore
219+
151220 - name : Clean Up
152221 run : |
153222 cd ./docker/development
223+ docker compose --profile sso-test down --remove-orphans 2>/dev/null || true
154224 docker compose down --remove-orphans
155225 sudo rm -rf ${PWD}/docker-volume
156-
157- notify :
226+
227+ sso-summary :
228+ if : ${{ !cancelled() && github.event_name == 'pull_request_target' }}
158229 needs : sso-auth-tests
159230 runs-on : ubuntu-latest
160- if : failure()
161231 steps :
162- - name : Send notification on failure
163- run : |
164- echo "SSO Authentication tests failed for one or more providers"
165- # Add your notification logic here (Slack, email, etc.)
232+ - name : Download results JSON
233+ uses : actions/download-artifact@v4
234+ with :
235+ pattern : sso-results-json-*
236+ path : results
237+
238+ - name : Post SSO test summary as PR comment
239+ uses : actions/github-script@v7
240+ with :
241+ github-token : ${{ secrets.GITHUB_TOKEN }}
242+ script : |
243+ const fs = require('fs');
244+ const path = require('path');
245+
246+ const runId = '${{ github.run_id }}';
247+ const repo = context.repo;
248+ const prNumber = context.payload.pull_request?.number;
249+ if (!prNumber) return;
250+
251+ const artifactUrl = `https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId}`;
252+ const commentMarker = '<!-- sso-playwright-summary -->';
253+
254+ const allTests = [];
255+ const resultsDir = 'results';
256+ if (fs.existsSync(resultsDir)) {
257+ for (const dir of fs.readdirSync(resultsDir).sort()) {
258+ const jsonPath = path.join(resultsDir, dir, 'sso-results.json');
259+ if (!fs.existsSync(jsonPath)) continue;
260+
261+ const report = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
262+
263+ function collectTests(suite, filePath) {
264+ const file = suite.file || filePath || '';
265+ for (const spec of (suite.specs || [])) {
266+ for (const test of (spec.tests || [])) {
267+ const results = test.results || [];
268+ const lastResult = results[results.length - 1] || {};
269+ const firstResult = results[0] || {};
270+ allTests.push({
271+ title: spec.title,
272+ project: test.projectName || '',
273+ file: file,
274+ status: test.status,
275+ retries: results.length - 1,
276+ error: lastResult.error?.message || firstResult.error?.message || '',
277+ });
278+ }
279+ }
280+ for (const child of (suite.suites || [])) {
281+ collectTests(child, file);
282+ }
283+ }
284+ for (const suite of (report.suites || [])) {
285+ collectTests(suite, '');
286+ }
287+ }
288+ }
289+
290+ if (allTests.length === 0) {
291+ console.log('No SSO test results found');
292+ return;
293+ }
294+
295+ const passed = allTests.filter(t => t.status === 'expected');
296+ const failed = allTests.filter(t => t.status === 'unexpected');
297+ const flaky = allTests.filter(t => t.status === 'flaky');
298+ const skipped = allTests.filter(t => t.status === 'skipped');
299+
300+ const lines = [commentMarker];
301+
302+ if (failed.length > 0) {
303+ lines.push(`## 🔴 SSO Playwright Results — ${failed.length} failure(s)${flaky.length > 0 ? `, ${flaky.length} flaky` : ''}`);
304+ } else if (flaky.length > 0) {
305+ lines.push(`## 🟡 SSO Playwright Results — all passed (${flaky.length} flaky)`);
306+ } else {
307+ lines.push(`## ✅ SSO Playwright Results — all ${passed.length} tests passed`);
308+ }
309+ lines.push('');
310+ lines.push(`✅ ${passed.length} passed · ❌ ${failed.length} failed · 🟡 ${flaky.length} flaky · ⏭️ ${skipped.length} skipped`);
311+ lines.push('');
312+
313+ if (failed.length > 0) {
314+ lines.push('### Failures');
315+ lines.push('');
316+ for (const t of failed.slice(0, 20)) {
317+ lines.push(`<details><summary>❌ ${t.project} › ${t.title}</summary>`);
318+ lines.push('');
319+ lines.push('```');
320+ lines.push(t.error.substring(0, 1000));
321+ lines.push('```');
322+ lines.push('</details>');
323+ lines.push('');
324+ }
325+ }
326+
327+ if (flaky.length > 0) {
328+ lines.push(`<details><summary>🟡 ${flaky.length} flaky test(s)</summary>`);
329+ lines.push('');
330+ for (const t of flaky.slice(0, 20)) {
331+ lines.push(`- ${t.project} › ${t.title} (${t.retries} ${t.retries === 1 ? 'retry' : 'retries'})`);
332+ }
333+ lines.push('');
334+ lines.push('</details>');
335+ lines.push('');
336+ }
337+
338+ lines.push(`📦 [View run details](${artifactUrl})`);
339+
340+ const body = lines.join('\n');
341+
342+ let existingComment = null;
343+ for await (const response of github.paginate.iterator(
344+ github.rest.issues.listComments, { ...repo, issue_number: prNumber, per_page: 100 }
345+ )) {
346+ const found = response.data.find(c =>
347+ c.user?.login === 'github-actions[bot]' && c.body?.includes(commentMarker)
348+ );
349+ if (found) { existingComment = found; break; }
350+ }
351+
352+ if (existingComment) {
353+ await github.rest.issues.updateComment({ ...repo, comment_id: existingComment.id, body });
354+ } else {
355+ await github.rest.issues.createComment({ ...repo, issue_number: prNumber, body });
356+ }
0 commit comments