# 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: 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' }} 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: - uses: dorny/paths-filter@v4 id: filter with: 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: | github.event_name == 'merge_group' || (github.event_name == 'pull_request_target' && needs.check-changes.outputs.ui-changed == 'true' && (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 }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.event_name == 'merge_group' && github.sha || github.event.pull_request.head.sha }} fetch-depth: 0 filter: blob:none - uses: actions/setup-node@v4 with: node-version-file: "${{ env.UI_WORKING_DIRECTORY }}/.nvmrc" cache: yarn cache-dependency-path: | ${{ env.UI_WORKING_DIRECTORY }}/yarn.lock ${{ env.CORE_COMPONENTS_WORKING_DIRECTORY }}/yarn.lock - name: Install Antlr4 CLI run: sudo make install_antlr_cli - name: Install UI Yarn Packages working-directory: ${{ env.UI_WORKING_DIRECTORY }} run: yarn install --frozen-lockfile - 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/** 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 }') if [ -n "$TS_FILES" ]; then yarn organize-imports:cli $TS_FILES 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<> "$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<> "$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<> "$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<> "$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/** files: | playwright/**/*.{ts,tsx,js,jsx} - 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 yarn organize-imports:cli $CHANGED_FILES 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<> "$GITHUB_OUTPUT" echo "$FILES" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" git checkout -- . git clean -fd exit 1 fi - name: Install Core Components Yarn Packages working-directory: ${{ env.CORE_COMPONENTS_WORKING_DIRECTORY }} run: yarn install --frozen-lockfile - 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<> "$GITHUB_OUTPUT" echo "$FILES" >> "$GITHUB_OUTPUT" echo "EOF" >> "$GITHUB_OUTPUT" git checkout -- . git clean -fd 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@v3 id: fc if: ${{ github.event_name == 'pull_request_target' }} with: issue-number: ${{ github.event.pull_request.number }} comment-author: github-actions[bot] body-includes: '' - name: Post or update summary comment uses: actions/github-script@v7 if: ${{ github.event_name == 'pull_request_target' }} 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 - ESLint + Prettier + Organise Imports', reason: 'One or more Playwright test files have linting or formatting issues.', 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) }} || '', }, ]; const failures = checks.filter(c => c.result === 'failure'); const commentId = '${{ steps.fc.outputs.comment-id }}'; if (failures.length === 0) { 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('', '
Affected files', '', c.files.trim(), '', '
'); } return lines.join('\n'); }); const body = [ '', '### ❌ UI Checkstyle Failed', '', ...sections.flatMap(s => [s, '']), '---', '**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.app_docs_result }}" = 'failure' ] || \ [ "${{ needs.checkstyle.outputs.lint_playwright_result }}" = 'failure' ] || \ [ "${{ needs.checkstyle.outputs.lint_core_components_result }}" = 'failure' ]; then echo "One or more checks failed." exit 1 fi echo "All checks passed or were not required for this PR."