OpenMetadata/.github/workflows/playwright-postgresql-e2e.yml
Sriharsha Chintalapani c02ddd7796
Improve Playwright CI: reduce shard contention and fix STEP_SUMMARY size (#26417)
* Improve Playwright CI: reduce shard contention and fix STEP_SUMMARY size

- Split 6 shards into 8 with dedicated projects for stateful (serial),
  graph (2 workers), and landing (2 workers) tests to reduce contention
- Replace inline base64 screenshots in GITHUB_STEP_SUMMARY with a
  lightweight table + artifact download link to stay under 1024k limit
- Add expect timeout of 15s for CI reliability
- Exclude landing page tests from the general chromium project to avoid
  duplicate runs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Exclude stateful/graph/landing tests from chromium project

Prevent these tests from running twice — once in the dedicated
project shard and again in the general chromium shards.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 19:10:04 -07:00

411 lines
19 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright 2021 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 end-to-end (e2e) tests using Playwright with PostgreSQL as the database.
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path
name: Postgresql PR Playwright E2E Tests
on:
workflow_dispatch:
pull_request_target:
types:
- labeled
- opened
- synchronize
- reopened
- ready_for_review
paths-ignore:
- ".github/**"
- "openmetadata-dist/**"
- "docker/**"
- "!docker/development/docker-compose.yml"
- "!docker/development/docker-compose-postgres.yml"
- "openmetadata-ui/src/main/resources/ui/playwright/doc-generator/**"
- "openmetadata-ui/src/main/resources/ui/playwright/docs/**"
permissions:
contents: read
pull-requests: write
concurrency:
group: playwright-ci-pr-postgresql-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
playwright-ci-postgresql:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
environment: test
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
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.3.4
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 # To not auto approve changes
- 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: Setup Openmetadata Test Environment
uses: ./.github/actions/setup-openmetadata-test-environment
with:
python-version: "3.10"
# Pass different args depending on the shardIndex:
# - shard 2: run with ingestion setup
# - other shards: skip ingestion setup
args: ${{ matrix.shardIndex == 2 && '-d postgresql' || '-d postgresql -i false' }}
ingestion_dependency: "all"
- 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.57.0 install --with-deps
- name: Run Playwright tests
id: run-tests
working-directory: openmetadata-ui/src/main/resources/ui/
run: |
if [ "${{ matrix.shardIndex }}" -eq "1" ]; then
echo "🔹 Running DataAssetRules-only tests on shard 1"
# The testMatch pattern ensures only DataAssetRules*.spec.ts files run
npx playwright test \
--project=setup \
--project=DataAssetRulesEnabled \
--project=DataAssetRulesDisabled \
--project=Basic \
elif [ "${{ matrix.shardIndex }}" -eq "2" ]; then
echo "🔹 Running ingestion tests on shard 2"
# run ingestion framework tests
npx playwright test \
--project=ingestion \
elif [ "${{ matrix.shardIndex }}" -eq "6" ]; then
echo "🔹 Running stateful Playwright tests serially on shard 6"
npx playwright test \
--project=stateful \
--workers=1
elif [ "${{ matrix.shardIndex }}" -eq "7" ]; then
echo "🔹 Running graph/right-panel Playwright tests with reduced concurrency on shard 7"
npx playwright test \
--project=graph \
--workers=2
elif [ "${{ matrix.shardIndex }}" -eq "8" ]; then
echo "🔹 Running landing/widget Playwright tests with reduced concurrency on shard 8"
npx playwright test \
--project=landing \
--workers=2
else
# Shards 3-5 handle chromium tests (3 shards total)
CHROMIUM_SHARD=$(( ${{ matrix.shardIndex }} - 2 ))
echo "🔹 Running all other tests (excluding DataAssetRules/stateful) on chromium shard ${CHROMIUM_SHARD}/3"
npx playwright test \
--project=chromium \
--grep-invert @dataAssetRules \
--shard=${CHROMIUM_SHARD}/3
fi
env:
PLAYWRIGHT_IS_OSS: true
PLAYWRIGHT_SNOWFLAKE_USERNAME: ${{ secrets.TEST_SNOWFLAKE_USERNAME }}
PLAYWRIGHT_SNOWFLAKE_PASSWORD: ${{ secrets.TEST_SNOWFLAKE_PASSWORD }}
PLAYWRIGHT_SNOWFLAKE_ACCOUNT: ${{ secrets.TEST_SNOWFLAKE_ACCOUNT }}
PLAYWRIGHT_SNOWFLAKE_DATABASE: ${{ secrets.TEST_SNOWFLAKE_DATABASE }}
PLAYWRIGHT_SNOWFLAKE_WAREHOUSE: ${{ secrets.TEST_SNOWFLAKE_WAREHOUSE }}
PLAYWRIGHT_SNOWFLAKE_PASSPHRASE: ${{ secrets.TEST_SNOWFLAKE_PASSPHRASE }}
PLAYWRIGHT_PROJECT_ID: ${{ steps.cypress-project-id.outputs.CYPRESS_PROJECT_ID }}
PLAYWRIGHT_BQ_PRIVATE_KEY: ${{ secrets.TEST_BQ_PRIVATE_KEY }}
PLAYWRIGHT_BQ_PROJECT_ID: ${{ secrets.PLAYWRIGHT_BQ_PROJECT_ID }}
PLAYWRIGHT_BQ_PRIVATE_KEY_ID: ${{ secrets.TEST_BQ_PRIVATE_KEY_ID }}
PLAYWRIGHT_BQ_PROJECT_ID_TAXONOMY: ${{ secrets.TEST_BQ_PROJECT_ID_TAXONOMY }}
PLAYWRIGHT_BQ_CLIENT_EMAIL: ${{ secrets.TEST_BQ_CLIENT_EMAIL }}
PLAYWRIGHT_BQ_CLIENT_ID: ${{ secrets.TEST_BQ_CLIENT_ID }}
PLAYWRIGHT_REDSHIFT_HOST: ${{ secrets.E2E_REDSHIFT_HOST_PORT }}
PLAYWRIGHT_REDSHIFT_USERNAME: ${{ secrets.E2E_REDSHIFT_USERNAME }}
PLAYWRIGHT_REDSHIFT_PASSWORD: ${{ secrets.E2E_REDSHIFT_PASSWORD }}
PLAYWRIGHT_REDSHIFT_DATABASE: ${{ secrets.TEST_REDSHIFT_DATABASE }}
PLAYWRIGHT_METABASE_USERNAME: ${{ secrets.TEST_METABASE_USERNAME }}
PLAYWRIGHT_METABASE_PASSWORD: ${{ secrets.TEST_METABASE_PASSWORD }}
PLAYWRIGHT_METABASE_DB_SERVICE_NAME: ${{ secrets.TEST_METABASE_DB_SERVICE_NAME }}
PLAYWRIGHT_METABASE_HOST_PORT: ${{ secrets.TEST_METABASE_HOST_PORT }}
PLAYWRIGHT_SUPERSET_USERNAME: ${{ secrets.TEST_SUPERSET_USERNAME }}
PLAYWRIGHT_SUPERSET_PASSWORD: ${{ secrets.TEST_SUPERSET_PASSWORD }}
PLAYWRIGHT_SUPERSET_HOST_PORT: ${{ secrets.TEST_SUPERSET_HOST_PORT }}
PLAYWRIGHT_KAFKA_BOOTSTRAP_SERVERS: ${{ secrets.TEST_KAFKA_BOOTSTRAP_SERVERS }}
PLAYWRIGHT_KAFKA_SCHEMA_REGISTRY_URL: ${{ secrets.TEST_KAFKA_SCHEMA_REGISTRY_URL }}
PLAYWRIGHT_GLUE_ACCESS_KEY: ${{ secrets.TEST_GLUE_ACCESS_KEY }}
PLAYWRIGHT_GLUE_SECRET_KEY: ${{ secrets.TEST_GLUE_SECRET_KEY }}
PLAYWRIGHT_GLUE_AWS_REGION: ${{ secrets.TEST_GLUE_AWS_REGION }}
PLAYWRIGHT_GLUE_ENDPOINT: ${{ secrets.TEST_GLUE_ENDPOINT }}
PLAYWRIGHT_GLUE_STORAGE_SERVICE: ${{ secrets.TEST_GLUE_STORAGE_SERVICE }}
PLAYWRIGHT_MYSQL_USERNAME: ${{ secrets.TEST_MYSQL_USERNAME }}
PLAYWRIGHT_MYSQL_PASSWORD: ${{ secrets.TEST_MYSQL_PASSWORD }}
PLAYWRIGHT_MYSQL_HOST_PORT: ${{ secrets.TEST_MYSQL_HOST_PORT }}
PLAYWRIGHT_MYSQL_DATABASE_SCHEMA: ${{ secrets.TEST_MYSQL_DATABASE_SCHEMA }}
PLAYWRIGHT_POSTGRES_USERNAME: ${{ secrets.TEST_POSTGRES_USERNAME }}
PLAYWRIGHT_POSTGRES_PASSWORD: ${{ secrets.TEST_POSTGRES_PASSWORD }}
PLAYWRIGHT_POSTGRES_HOST_PORT: ${{ secrets.TEST_POSTGRES_HOST_PORT }}
PLAYWRIGHT_POSTGRES_DATABASE: ${{ secrets.TEST_POSTGRES_DATABASE }}
PLAYWRIGHT_AIRFLOW_HOST_PORT: ${{ secrets.TEST_AIRFLOW_HOST_PORT }}
PLAYWRIGHT_ML_MODEL_TRACKING_URI: ${{ secrets.TEST_ML_MODEL_TRACKING_URI }}
PLAYWRIGHT_ML_MODEL_REGISTRY_URI: ${{ secrets.TEST_ML_MODEL_REGISTRY_URI }}
PLAYWRIGHT_S3_STORAGE_ACCESS_KEY_ID: ${{ secrets.TEST_S3_STORAGE_ACCESS_KEY_ID }}
PLAYWRIGHT_S3_STORAGE_SECRET_ACCESS_KEY: ${{ secrets.TEST_S3_STORAGE_SECRET_ACCESS_KEY }}
PLAYWRIGHT_S3_STORAGE_END_POINT_URL: ${{ secrets.TEST_S3_STORAGE_END_POINT_URL }}
# Recommended: pass the GitHub token lets this action correctly
# determine the unique run id necessary to re-run the checks
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-${{ matrix.shardIndex }}
path: openmetadata-ui/src/main/resources/ui/playwright/output/playwright-report
retention-days: 5
- name: Upload test results (screenshots, traces)
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-test-results-${{ matrix.shardIndex }}
path: openmetadata-ui/src/main/resources/ui/playwright/output/test-results
retention-days: 5
- name: Post failure summary to PR
if: ${{ !cancelled() && github.event_name == 'pull_request_target' }}
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const path = require('path');
const shard = '${{ matrix.shardIndex }}';
const runId = '${{ github.run_id }}';
const repo = context.repo;
const prNumber = context.payload.pull_request?.number;
if (!prNumber) return;
const jsonPath = 'openmetadata-ui/src/main/resources/ui/playwright/output/results.json';
if (!fs.existsSync(jsonPath)) {
console.log('No results.json found, skipping comment');
return;
}
const report = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
const suites = report.suites || [];
// Flatten all test results from nested suites
const allTests = [];
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,
file: file,
status: test.status, // expected, unexpected, flaky, skipped
duration: results.reduce((sum, r) => sum + (r.duration || 0), 0),
retries: results.length - 1,
error: lastResult.error?.message || firstResult.error?.message || '',
attachments: lastResult.attachments || firstResult.attachments || [],
});
}
}
for (const child of (suite.suites || [])) {
collectTests(child, file);
}
}
for (const suite of suites) {
collectTests(suite, '');
}
const genuine = allTests.filter(t => t.status === 'unexpected');
const flaky = allTests.filter(t => t.status === 'flaky');
const passed = allTests.filter(t => t.status === 'expected');
const skipped = allTests.filter(t => t.status === 'skipped');
// Helper to find existing bot comment for this shard
async function findExistingComment() {
const marker = `Playwright Shard ${shard}`;
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(marker)
);
if (found) return found;
}
return null;
}
// If everything passed, delete any stale failure comment and exit
if (genuine.length === 0 && flaky.length === 0) {
console.log(`Shard ${shard}: all ${passed.length} tests passed`);
const stale = await findExistingComment();
if (stale) {
await github.rest.issues.deleteComment({ ...repo, comment_id: stale.id });
console.log(`Deleted stale failure comment for shard ${shard}`);
}
return;
}
const artifactUrl = `https://github.com/${repo.owner}/${repo.repo}/actions/runs/${runId}`;
const lines = [];
if (genuine.length > 0) {
lines.push(`## 🔴 Playwright Shard ${shard} — ${genuine.length} genuine failure(s)${flaky.length > 0 ? `, ${flaky.length} flaky` : ''}`);
} else {
lines.push(`## 🟡 Playwright Shard ${shard} — ${flaky.length} flaky test(s) (all passed on retry)`);
}
lines.push('');
lines.push(`✅ ${passed.length} passed · ❌ ${genuine.length} failed · 🟡 ${flaky.length} flaky · ⏭️ ${skipped.length} skipped`);
lines.push('');
if (genuine.length > 0) {
lines.push('### Genuine Failures (failed on all attempts)');
lines.push('');
for (const t of genuine.slice(0, 20)) {
const shortFile = t.file.replace(/.*playwright\/e2e\//, '');
const errorSnippet = t.error.split('\n')[0].substring(0, 200);
lines.push(`<details><summary>❌ <code>${shortFile}</code> ${t.title}</summary>`);
lines.push('');
lines.push('```');
lines.push(t.error.substring(0, 1000));
lines.push('```');
lines.push('</details>');
lines.push('');
}
if (genuine.length > 20) {
lines.push(`... and ${genuine.length - 20} more failures`);
lines.push('');
}
}
if (flaky.length > 0) {
lines.push('<details><summary>🟡 Flaky tests (passed on retry)</summary>');
lines.push('');
for (const t of flaky.slice(0, 20)) {
const shortFile = t.file.replace(/.*playwright\/e2e\//, '');
lines.push(`- \`${shortFile}\` ${t.title} (${t.retries} ${t.retries === 1 ? 'retry' : 'retries'})`);
}
if (flaky.length > 20) {
lines.push(`- ... and ${flaky.length - 20} more`);
}
lines.push('');
lines.push('</details>');
lines.push('');
}
lines.push(`📦 [Download artifacts (report + screenshots + traces + videos)](${artifactUrl})`);
lines.push('');
lines.push('<details><summary>How to debug locally</summary>');
lines.push('');
lines.push('```bash');
lines.push('# Download playwright-test-results-' + shard + ' artifact and unzip');
lines.push('npx playwright show-trace path/to/trace.zip # view trace');
lines.push('# Videos are in the same artifact as .webm files');
lines.push('```');
lines.push('</details>');
const body = lines.join('\n');
const existing = await findExistingComment();
if (existing) {
await github.rest.issues.updateComment({ ...repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ ...repo, issue_number: prNumber, body });
}
// Write a lightweight summary to GITHUB_STEP_SUMMARY (no inline images to stay under 1024k limit)
const summaryLines = [`## Shard ${shard} Failure Summary\n`];
const resultsDir = 'openmetadata-ui/src/main/resources/ui/playwright/output/test-results';
if (fs.existsSync(resultsDir)) {
const failedTests = [];
for (const entry of fs.readdirSync(resultsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const testDir = path.join(resultsDir, entry.name);
const hasScreenshots = fs.readdirSync(testDir).some(f => f.endsWith('.png'));
const hasTraces = fs.readdirSync(testDir).some(f => f.endsWith('.zip'));
const testName = entry.name.replace(/-chromium.*$/, '').replace(/-/g, ' ');
failedTests.push({ testName, hasScreenshots, hasTraces });
}
if (failedTests.length > 0) {
summaryLines.push(`| Test | Screenshots | Traces |`);
summaryLines.push(`|------|-------------|--------|`);
for (const t of failedTests.slice(0, 30)) {
summaryLines.push(`| ${t.testName} | ${t.hasScreenshots ? '📸' : '-'} | ${t.hasTraces ? '🔍' : '-'} |`);
}
summaryLines.push('');
summaryLines.push(`📦 [Download screenshots, traces & videos from artifacts](${artifactUrl})`);
}
}
if (summaryLines.length > 1) {
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summaryLines.join('\n'));
}
- name: Clean Up
run: |
cd ./docker/development
docker compose down --remove-orphans
sudo rm -rf ${PWD}/docker-volume