name: UI Lint permissions: contents: read on: pull_request: branches: - main - litellm_internal_staging - litellm_oss_staging - "litellm_**" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: frontend-lint: runs-on: ubuntu-latest timeout-minutes: 8 defaults: run: working-directory: ui/litellm-dashboard steps: - name: Checkout repository uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: fetch-depth: 1 persist-credentials: false - name: Collect changed files id: changed env: GH_TOKEN: ${{ github.token }} BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | # base.sha is the base branch tip from when the PR was opened, while # actions/checkout leaves HEAD on a merge of the PR into the *current* # base tip. "$BASE_SHA"...HEAD therefore spans every base-branch commit # landed since, so a PR that touches no UI file still gets linted # against hundreds of other people's files. Diff the PR head against its # own merge base instead, which is exactly what this PR changed. merge_base=$(gh api "repos/${{ github.repository }}/compare/${BASE_SHA}...${HEAD_SHA}?per_page=1" --jq '.merge_base_commit.sha') test -n "$merge_base" git fetch --no-tags --depth=1 origin "$merge_base" "$HEAD_SHA" : > "$RUNNER_TEMP/prettier_files.txt" : > "$RUNNER_TEMP/eslint_files.txt" while IFS= read -r f; do [ -f "$f" ] || continue case "$f" in *.js | *.jsx | *.ts | *.tsx | *.mjs | *.cjs) printf '%s\n' "$f" >> "$RUNNER_TEMP/prettier_files.txt" printf '%s\n' "$f" >> "$RUNNER_TEMP/eslint_files.txt" ;; *.json | *.css | *.scss | *.md | *.mdx | *.yml | *.yaml | *.html) printf '%s\n' "$f" >> "$RUNNER_TEMP/prettier_files.txt" ;; esac done < <(git diff --name-only --diff-filter=ACMR --relative "$merge_base" "$HEAD_SHA" -- .) if [ -s "$RUNNER_TEMP/prettier_files.txt" ] || [ -s "$RUNNER_TEMP/eslint_files.txt" ]; then echo "has_files=true" >> "$GITHUB_OUTPUT" else echo "has_files=false" >> "$GITHUB_OUTPUT" echo "No lintable UI files changed in this PR; nothing to check." fi - name: Setup Node.js if: steps.changed.outputs.has_files == 'true' uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 with: node-version-file: ui/litellm-dashboard/.nvmrc cache: "npm" cache-dependency-path: ui/litellm-dashboard/package-lock.json - name: Install dependencies if: steps.changed.outputs.has_files == 'true' run: npm ci - name: Lint changed files (prettier + eslint) if: steps.changed.outputs.has_files == 'true' run: | prettier_files=() eslint_files=() while IFS= read -r f; do prettier_files+=("$f"); done < "$RUNNER_TEMP/prettier_files.txt" while IFS= read -r f; do eslint_files+=("$f"); done < "$RUNNER_TEMP/eslint_files.txt" status=0 if [ ${#prettier_files[@]} -gt 0 ]; then echo "::group::Prettier (${#prettier_files[@]} files)" npx prettier --check "${prettier_files[@]}" || { status=1; echo "::error::Unformatted files. Fix with: npm run format"; } echo "::endgroup::" fi if [ ${#eslint_files[@]} -gt 0 ]; then echo "::group::ESLint (${#eslint_files[@]} files)" npx eslint --no-warn-ignored --pass-on-unpruned-suppressions "${eslint_files[@]}" || status=1 echo "::endgroup::" fi exit $status - name: Check lint budgets if: ${{ !cancelled() && steps.changed.outputs.has_files == 'true' }} run: | npx eslint . -f json -o "$RUNNER_TEMP/lint-report.json" || true node scripts/check-lint-budgets.mjs "$RUNNER_TEMP/lint-report.json" eslint-budgets.json - name: Check for dead code (knip) if: ${{ !cancelled() && steps.changed.outputs.has_files == 'true' }} run: npm run knip:ci