ci(playwright): unify postgres/mysql e2e into a reusable workflow - #31628
Conversation
The nightly postgres and mysql files had drifted from the maintained
`playwright-postgresql-e2e.yml` pipeline — same intent, but hand-copied
shard logic, no smart selection, no distribution/fixture cache, and their
own env blocks. A change to the pipeline meant editing three files.
Extract every pipeline job into `playwright-e2e-reusable.yml`
(`on: workflow_call`) parameterised by `database`, `profile`,
`workflow_filename`, and `ref`. Three thin callers:
- `playwright-postgresql-e2e.yml` (94 lines) — PR + merge_group +
pull_request_target + workflow_dispatch on the current branch.
- `playwright-postgresql-nightly.yml` (81 lines, new) — on-demand,
takes a `branch:` input for release-branch verification.
- `playwright-mysql-nightly.yml` (80 lines) — on-demand, same shape;
MySQL is not a PR gate.
The old on-demand branch-runners (`postgresql-nightly-e2e.yml`,
`mysql-nightly-e2e.yml`) and the required-check placeholder
`playwright-mysql-e2e-skip.yml` are deleted; their behaviour is folded
into the callers above.
Security-sensitive checkouts (`playwright-summary` render script,
`refresh-timing-baseline`) deliberately ignore the branch input and
stay on `github.sha`/`main` so a compromised release branch cannot
tamper with the trusted-code paths.
The 20-line rename `playwright-ci-postgresql` → `playwright-ci` (the
sharded job is DB-neutral now) shows up in the four tests / two comments
that parsed the workflow file by string; all 103 workflow-parsing tests
still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
| - name: Calculate cache fingerprints | ||
| id: fingerprints | ||
| env: | ||
| # inputs.coarse_bundle is always defined (workflow_call), defaults to true. | ||
| COARSE_BUNDLE: ${{ inputs.coarse_bundle }} | ||
| run: | | ||
| bundle_mode=regular | ||
| if [[ "$COARSE_BUNDLE" == "true" ]]; then | ||
| bundle_mode=coarse | ||
| fi | ||
| toolchain=$(mvn --version | sed -n '1p' | tr -d '\r') | ||
| distribution=$(python3 .github/scripts/playwright_cache_fingerprint.py \ | ||
| --kind distribution \ | ||
| --bundle-mode "$bundle_mode" \ | ||
| --toolchain "$toolchain") | ||
| { | ||
| echo "bundle_mode=$bundle_mode" | ||
| echo "distribution=$distribution" | ||
| echo "fixture=$(python3 .github/scripts/playwright_cache_fingerprint.py --kind fixture)" | ||
| echo "ingestion=$(python3 .github/scripts/playwright_cache_fingerprint.py --kind ingestion)" | ||
| echo "toolchain=$toolchain" | ||
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| build: |
| - name: Install antlr cli | ||
| if: ${{ steps.distribution-cache.outputs.usable != 'true' }} | ||
| run: sudo make install_antlr_cli | ||
|
|
||
| - name: Restore openmetadata-ui dist cache |
| - name: Restore openmetadata-ui dist cache | ||
| # Two guards ANDed together: | ||
| # - distribution-cache miss (otherwise we already have the tarball) | ||
| # - not a fork PR under pull_request_target — that combination runs | ||
| # base-branch privileged with untrusted checked-out code, so a | ||
| # cache save would poison the default-branch cache scope (CodeQL | ||
| # `actions/actions/cache-poisoning`). Same guard lives inside the | ||
| # composite action for defence in depth, but CodeQL is a static | ||
| # analyser and needs to see it here at the workflow level. | ||
| if: >- | ||
| steps.distribution-cache.outputs.usable != 'true' && | ||
| !(github.event_name == 'pull_request_target' && | ||
| github.event.pull_request.head.repo.full_name != github.repository) | ||
| uses: ./.github/actions/cache-ui-dist | ||
| with: | ||
| # PW_E2E_BUILD alters vite chunking; PW_E2E_BUNDLE alters it further. | ||
| # Keep those variants in separate cache buckets so a non-e2e build | ||
| # never returns an e2e-shaped dist (or vice versa). | ||
| variant: pw-e2e-${{ needs.cache-keys.outputs.bundle_mode == 'coarse' && 'bundle' || 'build' }} | ||
|
|
||
| - name: Build with Maven |
| - name: Build with Maven | ||
| if: ${{ steps.distribution-cache.outputs.usable != 'true' }} | ||
| env: | ||
| PW_E2E_BUILD: "true" | ||
| PW_E2E_BUNDLE: ${{ needs.cache-keys.outputs.bundle_mode == 'coarse' }} | ||
| run: mvn -DskipTests clean package -pl openmetadata-dist -am | ||
|
|
||
| - name: Package OpenMetadata distribution cache entry |
| - name: Select PR coverage | ||
| id: select | ||
| env: | ||
| ALL_FILES: ${{ steps.all-changes.outputs.all_changed_files }} | ||
| run: | | ||
| mkdir -p "$RUNNER_TEMP/playwright-selection" | ||
| # shellcheck disable=SC2086 | ||
| printf '%s\n' $ALL_FILES > "$RUNNER_TEMP/playwright-selection/changed-files.txt" | ||
| python3 .github/scripts/select_playwright_tests.py \ | ||
| --event-name "${{ github.event_name }}" \ | ||
| --changed-files "$RUNNER_TEMP/playwright-selection/changed-files.txt" \ | ||
| --impact-map .github/playwright/impact-map.json \ | ||
| --full-suite "${{ inputs.full_suite }}" \ | ||
| --output "$RUNNER_TEMP/playwright-selection/selection.json" \ | ||
| --github-output "$GITHUB_OUTPUT" | ||
| jq '{mode, reason, selected: (.selectors | length)}' \ | ||
| "$RUNNER_TEMP/playwright-selection/selection.json" | ||
|
|
||
| - name: Upload Playwright selection |
| - name: Install fixture-state dependencies | ||
| if: ${{ needs.restore-playwright-fixture.outputs.fixture_cache_hit != 'true' || (needs.plan-playwright.outputs.requires_airflow == 'true' && needs.restore-playwright-fixture.outputs.ingestion_cache_hit != 'true') }} | ||
| working-directory: openmetadata-ui/src/main/resources/ui | ||
| run: | | ||
| corepack enable | ||
| yarn --ignore-scripts --frozen-lockfile | ||
|
|
||
| - name: Restore Playwright browser cache |
| - name: Snapshot database and OpenSearch | ||
| if: ${{ needs.restore-playwright-fixture.outputs.fixture_cache_hit != 'true' || (needs.plan-playwright.outputs.requires_airflow == 'true' && needs.restore-playwright-fixture.outputs.ingestion_cache_hit != 'true') }} | ||
| env: | ||
| CREATE_INGESTION_IMAGE: ${{ needs.plan-playwright.outputs.requires_airflow == 'true' && needs.restore-playwright-fixture.outputs.ingestion_cache_hit != 'true' }} | ||
| run: | | ||
| if ! command -v zstd >/dev/null 2>&1; then | ||
| sudo apt-get update | ||
| sudo apt-get install -y zstd | ||
| fi | ||
| mkdir -p "$FIXTURE_CACHE_DIR" "$INGESTION_CACHE_DIR" | ||
| fixture_args=("$FIXTURE_CACHE_DIR/playwright-fixture.tar.zst") | ||
| if [[ "$CREATE_INGESTION_IMAGE" == "true" ]]; then | ||
| fixture_args+=("$INGESTION_CACHE_DIR/playwright-ingestion-image.tar.zst") | ||
| fi | ||
| ./.github/scripts/create_playwright_fixture.sh "${fixture_args[@]}" | ||
|
|
||
| - name: Validate generated golden fixture |
…thing Declaring a `secrets:` block on `on: workflow_call` turns it into the closed set of secrets the reusable can reference. The prior version listed only `E2E_SLACK_BOT_OAUTH_TOKEN`, so every `secrets.TEST_*` reference elsewhere in the file became an undefined property — GitHub Actions caught this at parse time and reported `startup_failure` on the `pull_request` run of the caller. Remove the block entirely so `secrets: inherit` on both callers passes the full secret set through unchanged. Actionlint drops from 44 errors to zero (only a pre-existing SC2016 warning remains). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Startup_failure on the pull_request run: "The nested job 'refresh-timing-baseline' is requesting 'contents: write, pull-requests: write', but is only allowed 'contents: read, pull-requests: read'." In reusable workflows the caller's job permissions are the *upper bound* on what any reusable job can request. The `refresh-timing-baseline` job inside the reusable declares `contents: write, pull-requests: write` so it can open the auto-refresh PR — but the callers only granted read. Elevate the caller-side `jobs.playwright.permissions` on all three callers so the reusable's baseline-refresh job is inside the grant. Every other reusable job declares its own read-only permissions block, and baseline-refresh itself is gated on `github.event_name == 'merge_group'` — so PRs, PR-targets, and dispatches never invoke a writer, they just declare a header they don't use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Playwright Results — workflow succeededValidated commit ✅ 551 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky PerformanceBlocking targets: ✅ met · Optimization targets: 🟡 in progress Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting. 🕒 Full workflow signal wall (to summary) 57m 39s ⏱️ Max setup 2m 58s · max shard execution 18m 36s · max shard-job elapsed before upload 22m 24s · reporting 4s 🌐 213.70 requests/attempt · 2.83 app boots/UI scenario · 13.17% common-shard skew Optimization targets still in progress:
How to debug locally# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip # view trace |
Reusable workflow jobs are always reported by GitHub as
`<caller-job> / <sub-job>`, so a `playwright-summary` job inside the
reusable ended up as `playwright / playwright-summary` — invisible to a
branch-protection rule requiring the unprefixed name. Same story for
`refresh-timing-baseline`.
Move both jobs out of the reusable and into `playwright-postgresql-e2e.yml`
so they run at the caller top level and produce the exact check names
branch protection expects:
* `playwright-summary` — required PR check + PR-comment payload
* `Refresh timing baseline` — merge_group-only auto-PR
To surface the reusable's per-job results/outputs into the caller, add a
`workflow_call.outputs:` block that publishes:
gate_result, gate_should_run, check_changes_{result,e2e,docker_compose},
cache_keys_result, build_result,
detect_changes_{result,mode}, plan_playwright_{result,matrix},
restore_playwright_fixture_result, prepare_playwright_fixture_result,
playwright_ci_result
The caller's summary/refresh jobs consume these via
`needs.playwright.outputs.<key>`. Every other reusable job (gate through
playwright-ci) still runs inside the reusable so the mysql/postgres
branch-runners inherit the pipeline unchanged. `slack-notify` inside the
reusable drops its dep on `playwright-summary` and now waits only on
`playwright-ci`.
Reverts the caller-side `contents/pull-requests: write` grant added in
9ccd91f — nothing inside the reusable needs write now that the refresh
job has moved out. Only the caller-level `refresh-timing-baseline`
declares its own write permissions block.
Actionlint warns 9x about `jobs.<id>.result` not being a valid property
in `workflow_call.outputs.value`; GitHub Actions accepts it at runtime
per docs on the `jobs` context. If runtime disagrees we'll add an
aggregator job.
Tests: point workflow-parsing tests at the caller / reusable as
appropriate; all 103 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review ✅ ApprovedConsolidates PostgreSQL and MySQL Playwright end-to-end pipelines into a single parameterized reusable workflow with thin callers, reducing duplication across configuration files. No issues found. OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source |
Summary
The nightly postgres and mysql workflows had drifted from the maintained
playwright-postgresql-e2e.ymlpipeline — same intent, but hand-copied shard logic, no smart selection, no distribution/fixture cache, and their own env blocks. Changing the pipeline meant editing three files.Extract every job into
playwright-e2e-reusable.yml(on: workflow_call) parameterised bydatabase,profile,workflow_filename, andref. Three thin callers now share it:playwright-postgresql-e2e.yml(94 lines)playwright-postgresql-nightly.yml(81 lines, new)branch:inputplaywright-mysql-nightly.yml(80 lines)branch:inputThe old on-demand runners (
postgresql-nightly-e2e.yml,mysql-nightly-e2e.yml) and the required-check placeholderplaywright-mysql-e2e-skip.ymlare deleted; their behaviour is folded into the callers above.Notable design points
ref:input on the reusable — branch-runners are always dispatched frommain; the input drives every test-path checkout (7 places:cache-keys,build,detect-changes,plan-playwright,restore-playwright-fixture,prepare-playwright-fixture,playwright-ci, plusslack-notify).playwright-summaryrender script usesgithub.shaand the timing-baseline refresh usesmain, so a compromised release branch can't rewrite the trusted-code paths.playwright-ci-postgresql→playwright-ci— the sharded job is DB-neutral now. This shows up in 4 tests / 2 comments that parsed the workflow by string; all 103 workflow-parsing tests still pass.send_slack_notification(was already there on the old nightly files); the PR flow never had it and still doesn't.Test plan
python3 -m pytest .github/scripts/tests/test_playwright_ci_planning.py .github/scripts/tests/test_playwright_cache_assets.py .github/scripts/tests/test_playwright_performance_gate.py— 103 passedpython3 -c "import yaml; ..."on all 4 workflow YAMLs — parses cleanmake harness-check— no new warnings introduced (2 pre-existing dead-reference warnings, 2 doc-size, 1 generated-fresh, all unrelated)Postgresql PR Playwright E2E Tests(workflow_dispatch) on this branch — full suite, http, coarse=true — and confirm greenPostgreSQL Playwright E2E (release branch)from main withbranch: unify-nightly-e2e-workflows— confirm every checkout picks up this branch, summary/baseline-refresh stay onmainMySQL Playwright E2E (release branch)from main withbranch: unify-nightly-e2e-workflows— confirm mysql compose file (docker-compose.yml) is selected,-d mysqlreaches the setup action,--profile mysql-nightlyreaches the shadow-gate🤖 Generated with Claude Code
Greptile Summary
The PR consolidates PostgreSQL and MySQL Playwright pipelines into a reusable workflow while retaining thin trigger-specific callers.
Confidence Score: 5/5
The PR appears safe to merge because no blocking failure remains within the eligible follow-up review scope.
No blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD PR[PostgreSQL PR and merge triggers] --> PGC[playwright-postgresql-e2e.yml] PGD[PostgreSQL release-branch dispatch] --> PGN[playwright-postgresql-nightly.yml] MYD[MySQL release-branch dispatch] --> MYN[playwright-mysql-nightly.yml] PGC --> RW[playwright-e2e-reusable.yml] PGN -->|database: postgresql| RW MYN -->|database: mysql| RW RW --> PLAN[Change detection and shard planning] RW --> FIXTURE[Build or restore fixture] RW --> TESTS[Playwright shard jobs] RW --> REPORTS[Reports and optional Slack notification] PGC --> SUMMARY[Trusted PR summary and baseline refresh]Reviews (3): Last reviewed commit: "ci(playwright): move summary+baseline-re..." | Re-trigger Greptile