Skip to content

fix(playwright): avoid overlay click intercepts in domain and service flows #29319

fix(playwright): avoid overlay click intercepts in domain and service flows

fix(playwright): avoid overlay click intercepts in domain and service flows #29319

Workflow file for this run

# 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.
name: UI Checkstyle
on:
merge_group:
# Note: paths filter removed — the workflow always triggers so that the
# required status check (report) always completes. Path filtering is handled
# inside the workflow via dorny/paths-filter so PRs without UI changes skip
# the expensive jobs while still reporting a passing status.
pull_request_target:
# main only. Release branches carry their own ui-checkstyle.yml and must be
# validated by it: this workflow assumes tooling (eslint-pr-report,
# tw-audit, tw-deprecation-guard, core-components check-i18n-all) that older
# branches do not have, so running it against them fails on MODULE_NOT_FOUND
# before the PR's code is ever examined.
branches:
- main
types:
- opened
- synchronize
- reopened
- ready_for_review
- labeled
permissions:
contents: read
concurrency:
group: ui-checkstyle-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.event_name != 'pull_request_target' || github.event.action != 'labeled' || (github.event.label.name == 'safe to test' && github.event.pull_request.head.repo.full_name != github.repository) }}
env:
UI_WORKING_DIRECTORY: openmetadata-ui/src/main/resources/ui
CORE_COMPONENTS_WORKING_DIRECTORY: openmetadata-ui-core-components/src/main/resources/ui
jobs:
check-changes:
runs-on: ubuntu-latest
outputs:
ui-changed: ${{ steps.filter.outputs.ui }}
steps:
- name: Checkout
uses: actions/checkout@v7
if: ${{ github.event_name == 'merge_group' }}
with:
fetch-depth: 0
filter: blob:none
persist-credentials: false
- uses: dorny/paths-filter@v4
id: filter
with:
base: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || '' }}
filters: |
ui:
- 'openmetadata-ui/src/main/resources/ui/**'
- 'openmetadata-spec/src/main/resources/json/schema/**'
- '.github/workflows/ui-checkstyle.yml'
- 'openmetadata-ui-core-components/src/main/resources/ui/**'
authorize:
needs: check-changes
if: |
needs.check-changes.outputs.ui-changed == 'true' &&
(github.event_name != 'pull_request_target' ||
(github.event.action != 'labeled' || github.event.label.name == 'safe to test'))
runs-on: ubuntu-latest
steps:
- 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_name == 'merge_group' && github.sha || 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
checkstyle:
needs: authorize
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
lint_src_result: ${{ steps.lint_src.outcome }}
lint_src_changed_files: ${{ steps.lint_src.outputs.changed_files }}
license_result: ${{ steps.license.outcome }}
license_changed_files: ${{ steps.license.outputs.changed_files }}
i18n_result: ${{ steps.i18n.outcome }}
i18n_changed_files: ${{ steps.i18n.outputs.changed_files }}
app_docs_result: ${{ steps.app_docs.outcome }}
app_docs_changed_files: ${{ steps.app_docs.outputs.changed_files }}
lint_playwright_result: ${{ steps.lint_playwright.outcome }}
lint_playwright_changed_files: ${{ steps.lint_playwright.outputs.changed_files }}
lint_core_components_result: ${{ steps.lint_core_components.outcome }}
lint_core_components_changed_files: ${{ steps.lint_core_components.outputs.changed_files }}
i18n_core_result: ${{ steps.i18n_core.outcome }}
i18n_core_changed_files: ${{ steps.i18n_core.outputs.changed_files }}
tw_audit_result: ${{ steps.tw_audit.outcome }}
tw_audit_changed_files: ${{ steps.tw_audit.outputs.changed_files }}
tw_guard_result: ${{ steps.tw_guard.outcome }}
tw_guard_changed_files: ${{ steps.tw_guard.outputs.changed_files }}
eslint_has_findings: ${{ steps.lint_src.outputs.has_findings }}
eslint_summary: ${{ steps.lint_src.outputs.summary }}
eslint_details: ${{ steps.lint_src.outputs.details }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'merge_group' && github.sha || github.event.pull_request.head.sha }}
allow-unsafe-pr-checkout: true
fetch-depth: 0
filter: blob:none
persist-credentials: false
- uses: actions/setup-node@v5
with:
node-version-file: "${{ env.UI_WORKING_DIRECTORY }}/.nvmrc"
- name: Install Antlr4 CLI
run: sudo make install_antlr_cli
# setup-node's built-in `cache: yarn` is exact-key only — a yarn.lock
# change leaves the cache completely cold and exposes installs to transient
# registry 5xx. An explicit cache with restore-keys falls back to the most
# recent lockfile's cache so a lockfile change only fetches the delta.
# RESTORE-ONLY: this workflow runs on PR refs whose exact key is never
# reused; saving here would duplicate ~700 MB per run and evict main-scoped
# caches. The main-scoped copy is saved by populate-playwright-caches.yml.
- name: Restore yarn package cache
uses: actions/cache/restore@v6
with:
path: ~/.cache/yarn
key: yarn-pkg-cache-${{ runner.os }}-${{ hashFiles(format('{0}/yarn.lock', env.UI_WORKING_DIRECTORY)) }}
restore-keys: |
yarn-pkg-cache-${{ runner.os }}-
- name: Install UI Yarn Packages
working-directory: ${{ env.UI_WORKING_DIRECTORY }}
run: |
for attempt in 1 2 3; do
yarn install --frozen-lockfile --network-timeout 100000 && break
if [[ "$attempt" -eq 3 ]]; then
echo "yarn install failed after 3 attempts" >&2
exit 1
fi
echo "::warning::yarn install attempt $attempt failed (registry blip?); retrying in $((attempt * 15))s"
sleep $((attempt * 15))
done
- name: Get changed src files
id: changed-src-files
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323
with:
path: ${{ env.UI_WORKING_DIRECTORY }}
files_ignore: |
src/generated/**
src/jsons/**
files: |
src/**/*.{ts,tsx,js,jsx,json}
- name: ESLint + Prettier + Organise Imports (src)
id: lint_src
if: steps.changed-src-files.outputs.any_changed == 'true'
continue-on-error: true
working-directory: ${{ env.UI_WORKING_DIRECTORY }}
env:
CHANGED_FILES: ${{ steps.changed-src-files.outputs.all_changed_files }}
run: |
if [ -z "$CHANGED_FILES" ]; then
echo "No added or modified files to process."
exit 0
fi
TS_FILES=$(echo "$CHANGED_FILES" | tr ' ' '\n' | awk '/\.(ts|tsx|js|jsx)$/ { printf "%s ", $0 }')
# Report findings BEFORE --fix, so the PR comment reflects what the
# contributor actually wrote. Reporting only; the gate is the git-diff
# check at the end of this step.
#
# Deliberately part of this step rather than its own: every `run:`
# executing PR-authored code in this pull_request_target job is a
# CodeQL "poisonable step" (there are already 5 in this file, and 22
# repo-wide). Adding a separate step would add a sixth for no gain in
# isolation — it would run the same command with the same privileges.
if [ -n "$TS_FILES" ]; then
yarn lint:base -f json $TS_FILES > /tmp/eslint.json 2>/dev/null || true
node scripts/eslint-pr-report.js /tmp/eslint.json >> "$GITHUB_OUTPUT"
yarn organize-imports:cli $TS_FILES
else
echo "has_findings=false" >> "$GITHUB_OUTPUT"
fi
yarn lint:base --fix $CHANGED_FILES
yarn pretty:base --write $CHANGED_FILES
if [ -n "$(git status --porcelain)" ]; then
FILES=$(git status --porcelain | awk '{print " - `" $2 "`"}' | head -30)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
git checkout -- .
git clean -fd
exit 1
fi
- name: Get all changed UI files
id: changed-ui-files
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323
with:
path: ${{ env.UI_WORKING_DIRECTORY }}
- name: Licence Header Check
id: license
if: steps.changed-ui-files.outputs.any_changed == 'true'
continue-on-error: true
working-directory: ${{ env.UI_WORKING_DIRECTORY }}
env:
CHANGED_FILES_ALL: ${{ steps.changed-ui-files.outputs.all_changed_files }}
run: |
if [ -z "$CHANGED_FILES_ALL" ]; then
echo "No added or modified files to process."
exit 0
fi
yarn license-header-fix $CHANGED_FILES_ALL
if [ -n "$(git status --porcelain)" ]; then
FILES=$(git status --porcelain | awk '{print " - `" $2 "`"}' | head -30)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
git checkout -- .
git clean -fd
exit 1
fi
- name: I18n Sync
id: i18n
continue-on-error: true
working-directory: ${{ env.UI_WORKING_DIRECTORY }}
run: |
yarn i18n
if [ -n "$(git status --porcelain)" ]; then
FILES=$(git status --porcelain | awk '{print " - `" $2 "`"}' | head -20)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
git checkout -- .
git clean -fd
exit 1
fi
- name: generate:app-docs
id: app_docs
continue-on-error: true
working-directory: ${{ env.UI_WORKING_DIRECTORY }}
run: |
yarn generate:app-docs
if [ -n "$(git status --porcelain)" ]; then
FILES=$(git status --porcelain | awk '{print " - `" $2 "`"}' | head -20)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
git checkout -- .
git clean -fd
exit 1
fi
- name: Get changed Playwright files
id: changed-playwright-files
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323
with:
path: ${{ env.UI_WORKING_DIRECTORY }}
files_ignore: |
playwright/test-data/**
# Note sits outside `files` — it is a block scalar, every line a glob.
# The step also runs the rule-table drift check, so its inputs trigger it.
files: |
playwright/**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}
playwright/**/*.md
playwright/eslint-rules/**
playwright.config.ts
eslint.config.mjs
eslint-suppressions.json
eslint-rules/**
scripts/generate-playwright-rule-table.mjs
package.json
- name: ESLint + Prettier + Organise Imports (playwright)
id: lint_playwright
if: steps.changed-playwright-files.outputs.any_changed == 'true'
continue-on-error: true
working-directory: ${{ env.UI_WORKING_DIRECTORY }}
env:
CHANGED_FILES: ${{ steps.changed-playwright-files.outputs.all_changed_files }}
run: |
if [ -z "$CHANGED_FILES" ]; then
echo "No added or modified files to process."
exit 0
fi
# Catches empty-rule-list disables and inline `/* eslint <rule>: 0 */`
# config, both of which silence the guardrails and neither of which
# justified-rule-disable can self-police (ESLint applies inline config
# before rules run, and filters a rule's own reports on lines its own
# disable covers). Runs before --fix so it judges what was written.
# A path filter, not --exclude-dir: that matches a basename at any depth.
MATCHES=$(grep -rnE 'eslint-disable(-next-line|-line)?[[:space:]]*(\*/|--|$)|eslint-disable.*om-playwright/justified-rule-disable|/\*[[:space:]]*eslint[[:space:]][^*]*(om-)?playwright/[a-z-]+[[:space:]]*:' \
playwright/ --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' \
| grep -v '^playwright/eslint-rules/' | head -30 | cut -c1-300 || true)
if [ -n "$MATCHES" ]; then
echo "$MATCHES"
echo "::error::Found a blanket 'eslint-disable' (no rule list) or a disable of om-playwright/justified-rule-disable itself. Both bypass the Playwright lint guardrails entirely and are never allowed, justified or not. Disable individual rules instead, each with a justification, e.g. // eslint-disable-next-line om-playwright/no-positional-locator -- <why>."
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$MATCHES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
exit 1
fi
# Non-pruning variant: --prune-suppressions rewrites the file and exits 0,
# so a fixed violation would never fail CI and the baseline never ratchets.
#
# Real violations and stale entries both exit non-zero, so they are split:
# violations land in errorCount on stdout, the stale notice only on stderr.
# A stale entry is main drifting under us, not a defect in this PR, so it
# warns; errorCount is what fails. Never key off the exit code alone -- it
# is 2 even when a real violation is also present, which would swallow it.
yarn lint:playwright -f json > /tmp/pw-lint.json 2> /tmp/pw-lint.err || true
# ESLint's json formatter emits the whole report as a SINGLE line, so grep/head over it
# returns one multi-megabyte "line". Written to $GITHUB_OUTPUT that overflows the job
# output expression limit, and the job dies at "Evaluate and set job outputs" -- which
# blanks every needs.checkstyle.output and leaves the PR with a red check and no comment.
# Render one bounded line per error here; never grep the raw JSON.
ERRORS=$(node -e "
const fs=require('fs');
const m=/(\[[\s\S]*\])/.exec(fs.readFileSync('/tmp/pw-lint.json','utf8'));
const d=m?JSON.parse(m[1]):[];
const cwd=process.cwd()+'/';
const lines=[];
for (const f of d) {
for (const x of f.messages) {
if (x.severity !== 2) continue;
lines.push(f.filePath.replace(cwd,'')+':'+x.line+':'+x.column+' '+x.ruleId+' '+x.message);
}
}
const out=lines.slice(0,30).map(function(l){return l.slice(0,300);});
// Trailing newline matters: this file is appended to /tmp/pw-guardrails.out, and
// without it the last error would be glued onto whatever line is appended next.
fs.writeFileSync('/tmp/pw-lint-errors.txt', out.length?out.join('\n')+'\n':'');
console.log(d.reduce((n,f)=>n+f.errorCount,0));
")
if grep -q 'suppressions left that do not occur anymore' /tmp/pw-lint.err; then
echo "::warning::Stale suppression entries -- main fixed violations under this branch. Run 'yarn lint:playwright:suppressions' and commit the pruned baseline."
fi
if { [ "$ERRORS" -eq 0 ] && \
yarn test:eslint-rules && \
node scripts/generate-playwright-rule-table.mjs --check; } > /tmp/pw-guardrails.out 2>&1; then
RESULT=0
else
RESULT=$?
[ "$ERRORS" -eq 0 ] || cat /tmp/pw-lint-errors.txt >> /tmp/pw-guardrails.out
fi
if [ "$RESULT" -ne 0 ]; then
cat /tmp/pw-guardrails.out
# `|| true`: a grep matching nothing exits 1 under `bash -eo pipefail`.
# `cut`: this value becomes a job output, where one over-long line is enough to break
# job-output evaluation for the entire job.
# The last alternative is the rendered lint line shape (`path:line:col rule msg`).
# Without it a lint failure is filtered out entirely -- no rule message carries the
# word "error", so the comment would name the check and then list nothing.
OUT=$(sed 's/\x1b\[[0-9;]*m//g' /tmp/pw-guardrails.out | grep -E 'error|Error|not ok|fail|stale|^[^ ]+:[0-9]+:[0-9]+ ' | head -30 | cut -c1-300) || true
# continue-on-error renders this step with a green check, so without an annotation the
# run page gives no hint which step actually failed.
echo "::error title=Playwright guardrails + ESLint::${ERRORS} ESLint error(s) under playwright/, or a failing guardrail check. See the UI Checkstyle comment on this PR."
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$OUT" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
exit 1
fi
# organize-imports aborts on any file outside the TS project, so a .json
# kills the step — and the merge queue always lists eslint-suppressions.json.
TS_FILES=$(echo "$CHANGED_FILES" | tr ' ' '\n' | awk '/\.(ts|tsx)$/ { printf "%s ", $0 }')
# Return codes are captured rather than left to `bash -e`: organize-imports
# and prettier disagree permanently, so files are dirty between these lines.
# Aborting here would skip the cleanup below and leave that dirt for the
# next step's `git status` gate -- which is how a lint failure once turned
# the unrelated i18n job red.
RC=0
if [ -n "$TS_FILES" ]; then
yarn organize-imports:cli $TS_FILES || RC=$?
fi
yarn lint:base --fix $CHANGED_FILES || RC=$?
yarn pretty:base --write $CHANGED_FILES || RC=$?
if [ -n "$(git status --porcelain)" ]; then
FILES=$(git status --porcelain | awk '{print " - `" $2 "`"}' | head -30)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
git checkout -- .
git clean -fd
exit 1
fi
[ "$RC" -eq 0 ] || exit 1
- name: Install Core Components Yarn Packages
working-directory: ${{ env.CORE_COMPONENTS_WORKING_DIRECTORY }}
run: |
for attempt in 1 2 3; do
yarn install --frozen-lockfile --network-timeout 100000 && break
if [[ "$attempt" -eq 3 ]]; then
echo "yarn install failed after 3 attempts" >&2
exit 1
fi
echo "::warning::yarn install attempt $attempt failed (registry blip?); retrying in $((attempt * 15))s"
sleep $((attempt * 15))
done
- name: I18n Sync (core-components)
id: i18n_core
continue-on-error: true
working-directory: ${{ env.CORE_COMPONENTS_WORKING_DIRECTORY }}
run: |
yarn check-i18n-all
if [ -n "$(git status --porcelain)" ]; then
FILES=$(git status --porcelain | awk '{print " - `" $2 "`"}' | head -20)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
git checkout -- .
git clean -fd
exit 1
fi
- name: Get changed core-components files
id: changed-core-components-files
uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323
with:
path: ${{ env.CORE_COMPONENTS_WORKING_DIRECTORY }}
files: |
src/**/*.{ts,tsx,js,jsx,json}
- name: ESLint + Prettier (core-components)
id: lint_core_components
if: steps.changed-core-components-files.outputs.any_changed == 'true'
continue-on-error: true
working-directory: ${{ env.CORE_COMPONENTS_WORKING_DIRECTORY }}
env:
CHANGED_FILES: ${{ steps.changed-core-components-files.outputs.all_changed_files }}
run: |
if [ -z "${CHANGED_FILES// }" ]; then
echo "No added or modified files to process."
exit 0
fi
yarn lint:base --fix $CHANGED_FILES
yarn pretty:base --write $CHANGED_FILES
if [ -n "$(git status --porcelain)" ]; then
FILES=$(git status --porcelain | awk '{print " - `" $2 "`"}' | head -30)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$FILES" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
git checkout -- .
git clean -fd
exit 1
fi
- name: Tailwind Audit (no hardcoded values)
id: tw_audit
if: steps.changed-src-files.outputs.any_changed == 'true'
continue-on-error: true
working-directory: ${{ env.UI_WORKING_DIRECTORY }}
env:
CHANGED_FILES: ${{ steps.changed-src-files.outputs.all_changed_files }}
run: |
TS_FILES=$(echo "$CHANGED_FILES" | tr ' ' '\n' | awk '/\.(ts|tsx|js|jsx)$/ { printf "%s ", $0 }')
if [ -z "${TS_FILES// }" ]; then
echo "No TSX/TS files to audit."
exit 0
fi
if ! node scripts/tw-audit.js $TS_FILES > /tmp/tw-audit.out 2>&1; then
cat /tmp/tw-audit.out
OUT=$(sed 's/\x1b\[[0-9;]*m//g' /tmp/tw-audit.out | grep -E 'errors|Files with|^ +[0-9]' | head -30 | sed 's/^/ /')
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$OUT" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
exit 1
fi
- name: Antd + Less Deprecation Guard (no new debt)
id: tw_guard
if: steps.changed-ui-files.outputs.any_changed == 'true'
continue-on-error: true
env:
BASE_SHA: ${{ github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event.pull_request.base.sha }}
run: |
if ! node ${{ env.UI_WORKING_DIRECTORY }}/scripts/tw-deprecation-guard.js "$BASE_SHA" > /tmp/tw-guard.out 2>&1; then
cat /tmp/tw-guard.out
OUT=$(sed 's/\x1b\[[0-9;]*m//g' /tmp/tw-guard.out | grep -E '^ ' | head -30)
echo "changed_files<<EOF" >> "$GITHUB_OUTPUT"
echo "$OUT" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
exit 1
fi
ui-checkstyle:
needs: [authorize, checkstyle]
if: always()
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Find existing summary comment
uses: peter-evans/find-comment@v4
id: fc
if: ${{ github.event_name == 'pull_request_target' }}
continue-on-error: true
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: '<!-- check:ui-checkstyle-summary -->'
- name: Post or update summary comment
uses: actions/github-script@v9
if: ${{ github.event_name == 'pull_request_target' && steps.fc.outcome == 'success' }}
continue-on-error: true
with:
script: |
const checks = [
{
name: 'ESLint + Prettier + Organise Imports (src)',
reason: 'One or more source files have linting or formatting issues.',
result: '${{ needs.checkstyle.outputs.lint_src_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.lint_src_changed_files) }} || '',
},
{
name: 'Licence Header',
reason: 'One or more files are missing or have an outdated Apache 2.0 licence header.',
result: '${{ needs.checkstyle.outputs.license_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.license_changed_files) }} || '',
},
{
name: 'I18n Sync',
reason: 'Translation locale files are out of sync with `en-us.json`.',
result: '${{ needs.checkstyle.outputs.i18n_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.i18n_changed_files) }} || '',
},
{
name: 'App Docs',
reason: 'Generated application docs are stale and need to be regenerated.',
result: '${{ needs.checkstyle.outputs.app_docs_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.app_docs_changed_files) }} || '',
},
{
name: 'Playwright - Guardrails + ESLint + Prettier + Organise Imports',
reason: 'Either a Playwright test file has linting/formatting issues, or a guardrail check failed: ESLint rule unit tests, a new guardrail violation, a stale suppression entry (its violation was fixed but the baseline was not pruned), a blanket `eslint-disable`, or a stale generated rule table. For the guardrail cases run `yarn lint:playwright:suppressions` in `openmetadata-ui/src/main/resources/ui` and commit the pruned `eslint-suppressions.json`, then `yarn test:eslint-rules && node scripts/generate-playwright-rule-table.mjs --check`.',
result: '${{ needs.checkstyle.outputs.lint_playwright_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.lint_playwright_changed_files) }} || '',
},
{
name: 'Core Components - ESLint + Prettier',
reason: 'One or more core-component files have linting or formatting issues.',
result: '${{ needs.checkstyle.outputs.lint_core_components_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.lint_core_components_changed_files) }} || '',
},
{
name: 'Core Components - I18n Sync',
reason: 'Core-components `t()` keys, locale files, or language-set are out of sync. Run `yarn check-i18n-all` locally.',
result: '${{ needs.checkstyle.outputs.i18n_core_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.i18n_core_changed_files) }} || '',
},
{
name: 'Tailwind Audit',
reason: 'Hardcoded Tailwind values found. Use a design-system utility (run `yarn tw-audit:report` for the token each value maps to).',
result: '${{ needs.checkstyle.outputs.tw_audit_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.tw_audit_changed_files) }} || '',
},
{
name: 'Antd + Less Deprecation Guard',
reason: 'A new `antd` import or new `.less` file was added. Use UntitledUI + Tailwind for new work.',
result: '${{ needs.checkstyle.outputs.tw_guard_result }}',
files: ${{ toJSON(needs.checkstyle.outputs.tw_guard_changed_files) }} || '',
},
];
const failures = checks.filter(c => c.result === 'failure');
const commentId = '${{ steps.fc.outputs.comment-id }}';
const eslintFindings = '${{ needs.checkstyle.outputs.eslint_has_findings }}' === 'true';
const eslintSummary = ${{ toJSON(needs.checkstyle.outputs.eslint_summary) }} || '';
const eslintDetails = ${{ toJSON(needs.checkstyle.outputs.eslint_details) }} || '';
// The checkstyle job can die before its outputs are evaluated — a step output too
// large for the job-output expression limit does exactly that. Every entry above is
// then blank, so without this branch the PR gets a red required check and no comment.
const jobResult = '${{ needs.checkstyle.result }}';
// Only a genuine failure. `cancelled` is the concurrency group superseding this run,
// and `skipped` is a PR with no UI changes — neither is worth a red comment.
const jobFailed = jobResult === 'failure';
const unexplained = jobFailed && failures.length === 0;
// Warnings do not fail the build, so a comment posted only on
// failure would hide them entirely — which defeats the warn tier.
if (failures.length === 0 && !eslintFindings && !jobFailed) {
if (commentId) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: parseInt(commentId, 10),
});
}
return;
}
const sections = failures.map(c => {
const lines = [`#### ❌ ${c.name}`, c.reason];
if (c.files && c.files.trim()) {
lines.push('', '<details><summary>Affected files</summary>', '', c.files.trim(), '', '</details>');
}
return lines.join('\n');
});
const title = failures.length || unexplained
? '### ❌ UI Checkstyle Failed'
: '### ⚠️ UI Checkstyle passed — lint findings in changed files';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const unexplainedSection = unexplained
? [
`#### ❌ The checkstyle job ended as \`${jobResult}\` before it could report`,
'',
'No individual check reported a result, so the job itself failed rather than a',
'specific check — most often a step whose output was too large to evaluate as a',
'job output. The step annotations on the run say which step it was.',
'',
`[View run](${runUrl})`,
'',
]
: [];
const eslintSection = eslintFindings
? [
`#### 🔍 ESLint findings in this PR's files — ${eslintSummary}`,
'',
'Errors block the build. Warnings do not yet — they are rules whose backlog is still',
'being worked down, listed so this PR does not add to it. See `docs/ui-code-quality-gate.md`.',
'',
eslintDetails,
'',
]
: [];
const body = [
'<!-- check:ui-checkstyle-summary -->',
title,
'',
...unexplainedSection,
...sections.flatMap(s => [s, '']),
...eslintSection,
'---',
'**Fix locally (fast - only checks files changed in this branch):**',
'```bash',
'make ui-checkstyle-changed',
'```',
].join('\n');
if (commentId) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: parseInt(commentId, 10),
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Check final results
if: always()
run: |
checkstyle_result="${{ needs.checkstyle.result }}"
if [[ "$checkstyle_result" != "success" && "$checkstyle_result" != "skipped" ]]; then
echo "Checkstyle job ended with status: $checkstyle_result"
exit 1
fi
if [ "${{ needs.checkstyle.outputs.lint_src_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.license_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.i18n_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.i18n_core_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.app_docs_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.lint_playwright_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.lint_core_components_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.tw_audit_result }}" = 'failure' ] || \
[ "${{ needs.checkstyle.outputs.tw_guard_result }}" = 'failure' ]; then
echo "One or more checks failed."
exit 1
fi
echo "All checks passed or were not required for this PR."