| name: PR Lint and Format Check |
|
|
| on: |
| pull_request: |
| types: [opened, synchronize, reopened] |
| paths: |
| - '**.js' |
| - '**.jsx' |
| - '**.ts' |
| - '**.tsx' |
| - '**.vue' |
| - '**.json' |
| - '**.cjs' |
| - '**.mjs' |
| - '.prettierrc' |
| - '.eslintrc.cjs' |
| - 'package.json' |
| - 'web/admin-spa/**' |
|
|
| permissions: |
| contents: read |
| pull-requests: write |
| issues: write |
|
|
| jobs: |
| lint-and-format: |
| runs-on: ubuntu-latest |
| name: Check Code Quality |
| |
| steps: |
| - name: Checkout code |
| uses: actions/checkout@v4 |
| with: |
| fetch-depth: 0 |
|
|
| - name: Setup Node.js |
| uses: actions/setup-node@v4 |
| with: |
| node-version: '18' |
| cache: 'npm' |
|
|
| - name: Cache dependencies |
| uses: actions/cache@v3 |
| with: |
| path: ~/.npm |
| key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} |
| restore-keys: | |
| ${{ runner.os }}-node- |
| |
| - name: Install dependencies |
| run: | |
| npm ci --prefer-offline --no-audit |
| # 安装 web 目录的依赖(如果存在) |
| if [ -d "web/admin-spa" ] && [ -f "web/admin-spa/package.json" ]; then |
| cd web/admin-spa |
| npm ci --prefer-offline --no-audit |
| cd ../.. |
| fi |
| |
| - name: Get changed files |
| id: changed-files |
| uses: tj-actions/changed-files@v41 |
| with: |
| files: | |
| **/*.js |
| **/*.jsx |
| **/*.ts |
| **/*.tsx |
| **/*.vue |
| **/*.cjs |
| **/*.mjs |
| **/*.json |
| files_ignore: | |
| node_modules/** |
| dist/** |
| build/** |
| coverage/** |
| .git/** |
| logs/** |
| temp/** |
| tmp/** |
| |
| - name: Check Prettier formatting |
| if: steps.changed-files.outputs.any_changed == 'true' |
| id: prettier-check |
| run: | |
| echo "🔍 Checking Prettier formatting for changed files..." |
| echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}" |
| |
| |
| PRETTIER_FAILED=false |
| PRETTIER_OUTPUT="" |
| |
| |
| for file in ${{ steps.changed-files.outputs.all_changed_files }}; do |
| if [ -f "$file" ]; then |
| echo "Checking: $file" |
| |
| |
| if [[ "$file" == web/admin-spa/* ]]; then |
| |
| cd web/admin-spa |
| RELATIVE_FILE="${file#web/admin-spa/}" |
| if ! npx prettier --check "$RELATIVE_FILE" 2>&1; then |
| PRETTIER_FAILED=true |
| DIFF=$(npx prettier "$RELATIVE_FILE" | diff -u "$RELATIVE_FILE" - || true) |
| if [ -n "$DIFF" ]; then |
| PRETTIER_OUTPUT="${PRETTIER_OUTPUT}❌ File needs formatting: $file\n" |
| PRETTIER_OUTPUT="${PRETTIER_OUTPUT}\`\`\`diff\n${DIFF}\n\`\`\`\n\n" |
| fi |
| else |
| echo "✅ $file is properly formatted" |
| fi |
| cd ../.. |
| else |
| |
| if ! npx prettier --check "$file" 2>&1; then |
| PRETTIER_FAILED=true |
| DIFF=$(npx prettier "$file" | diff -u "$file" - || true) |
| if [ -n "$DIFF" ]; then |
| PRETTIER_OUTPUT="${PRETTIER_OUTPUT}❌ File needs formatting: $file\n" |
| PRETTIER_OUTPUT="${PRETTIER_OUTPUT}\`\`\`diff\n${DIFF}\n\`\`\`\n\n" |
| fi |
| else |
| echo "✅ $file is properly formatted" |
| fi |
| fi |
| fi |
| done |
| |
| |
| if [ "$PRETTIER_FAILED" = true ]; then |
| echo "prettier_failed=true" >> $GITHUB_OUTPUT |
| echo -e "$PRETTIER_OUTPUT" > prettier-report.md |
| echo "❌ Some files are not properly formatted." |
| echo "Please run: npm run format (backend) or cd web/admin-spa && npm run format (frontend)" |
| exit 1 |
| else |
| echo "prettier_failed=false" >> $GITHUB_OUTPUT |
| echo "✅ All files are properly formatted" |
| fi |
|
|
| - name: Run ESLint |
| if: steps.changed-files.outputs.any_changed == 'true' |
| id: eslint-check |
| run: | |
| echo "🔍 Running ESLint on changed files..." |
| |
| |
| BACKEND_FILES="" |
| FRONTEND_FILES="" |
| |
| for file in ${{ steps.changed-files.outputs.all_changed_files }}; do |
| if [[ "$file" =~ \.(js|jsx|vue|cjs|mjs)$ ]] && [ -f "$file" ]; then |
| if [[ "$file" == web/admin-spa/* ]]; then |
| FRONTEND_FILES="$FRONTEND_FILES ${file#web/admin-spa/}" |
| else |
| BACKEND_FILES="$BACKEND_FILES $file" |
| fi |
| fi |
| done |
| |
| ESLINT_FAILED=false |
| ESLINT_OUTPUT="" |
| |
| |
| if [ -n "$BACKEND_FILES" ]; then |
| echo "Linting backend files: $BACKEND_FILES" |
| set +e |
| BACKEND_OUTPUT=$(npx eslint $BACKEND_FILES --format stylish 2>&1) |
| BACKEND_EXIT_CODE=$? |
| set -e |
| |
| if [ $BACKEND_EXIT_CODE -ne 0 ]; then |
| ESLINT_FAILED=true |
| ESLINT_OUTPUT="${ESLINT_OUTPUT}### Backend ESLint Issues\n\`\`\`\n${BACKEND_OUTPUT}\n\`\`\`\n\n" |
| fi |
| fi |
| |
| |
| if [ -n "$FRONTEND_FILES" ]; then |
| echo "Linting frontend files: $FRONTEND_FILES" |
| cd web/admin-spa |
| set +e |
| FRONTEND_OUTPUT=$(npx eslint $FRONTEND_FILES --format stylish 2>&1) |
| FRONTEND_EXIT_CODE=$? |
| set -e |
| cd ../.. |
| |
| if [ $FRONTEND_EXIT_CODE -ne 0 ]; then |
| ESLINT_FAILED=true |
| ESLINT_OUTPUT="${ESLINT_OUTPUT}### Frontend ESLint Issues\n\`\`\`\n${FRONTEND_OUTPUT}\n\`\`\`\n\n" |
| fi |
| fi |
| |
| |
| if [ "$ESLINT_FAILED" = true ]; then |
| echo "eslint_failed=true" >> $GITHUB_OUTPUT |
| echo "❌ ESLint found issues" |
| |
| |
| echo "## ESLint Report" > eslint-report.md |
| echo "$ESLINT_OUTPUT" >> eslint-report.md |
| echo "" >> eslint-report.md |
| echo "Please fix these issues by running:" >> eslint-report.md |
| echo '```bash' >> eslint-report.md |
| echo "# Backend: npm run lint" >> eslint-report.md |
| echo "# Frontend: cd web/admin-spa && npm run lint" >> eslint-report.md |
| echo '```' >> eslint-report.md |
| |
| exit 1 |
| else |
| echo "eslint_failed=false" >> $GITHUB_OUTPUT |
| echo "✅ ESLint check passed" |
| fi |
|
|
| - name: Debug PR Context |
| if: failure() |
| run: | |
| echo "PR Number: ${{ github.event.pull_request.number }}" |
| echo "Repo: ${{ github.repository }}" |
| echo "Event Name: ${{ github.event_name }}" |
| echo "Actor: ${{ github.actor }}" |
| |
| - name: Comment PR with results |
| if: failure() |
| continue-on-error: true |
| uses: actions/github-script@v7 |
| with: |
| github-token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} |
| script: | |
| const fs = require('fs'); |
| let comment = '## 🚨 Code Quality Check Failed\n\n'; |
| |
| // 读取 Prettier 报告 |
| if (fs.existsSync('prettier-report.md')) { |
| const prettierReport = fs.readFileSync('prettier-report.md', 'utf8'); |
| comment += '### Prettier Formatting Issues\n\n'; |
| comment += prettierReport; |
| comment += '\n**Fix command:**\n```bash\nnpm run format\n```\n\n'; |
| } |
| |
| // 读取 ESLint 报告 |
| if (fs.existsSync('eslint-report.md')) { |
| const eslintReport = fs.readFileSync('eslint-report.md', 'utf8'); |
| comment += '### ESLint Issues\n\n'; |
| comment += eslintReport; |
| } |
| |
| comment += '\n---\n'; |
| comment += '💡 **提示**: 在本地运行以下命令来自动修复大部分问题:\n'; |
| comment += '```bash\n'; |
| comment += '# 后端代码\n'; |
| comment += 'npm run format # 修复后端 Prettier 格式问题\n'; |
| comment += 'npm run lint # 修复后端 ESLint 问题\n'; |
| comment += '\n'; |
| comment += '# 前端代码\n'; |
| comment += 'cd web/admin-spa\n'; |
| comment += 'npm run format # 修复前端 Prettier 格式问题\n'; |
| comment += 'npm run lint # 修复前端 ESLint 问题\n'; |
| comment += '```\n'; |
| |
| // 查找是否已有机器人评论 |
| const { data: comments } = await github.rest.issues.listComments({ |
| owner: context.repo.owner, |
| repo: context.repo.repo, |
| issue_number: context.issue.number, |
| }); |
| |
| const botComment = comments.find(comment => |
| comment.user.type === 'Bot' && |
| comment.body.includes('Code Quality Check Failed') |
| ); |
| |
| if (botComment) { |
| // 更新现有评论 |
| await github.rest.issues.updateComment({ |
| owner: context.repo.owner, |
| repo: context.repo.repo, |
| comment_id: botComment.id, |
| body: comment |
| }); |
| } else { |
| // 创建新评论 |
| await github.rest.issues.createComment({ |
| owner: context.repo.owner, |
| repo: context.repo.repo, |
| issue_number: context.issue.number, |
| body: comment |
| }); |
| } |
|
|
| - name: Success comment |
| if: success() && steps.changed-files.outputs.any_changed == 'true' |
| continue-on-error: true |
| uses: actions/github-script@v7 |
| with: |
| github-token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} |
| script: | |
| // 查找是否已有失败的评论 |
| const { data: comments } = await github.rest.issues.listComments({ |
| owner: context.repo.owner, |
| repo: context.repo.repo, |
| issue_number: context.issue.number, |
| }); |
| |
| const botComment = comments.find(comment => |
| comment.user.type === 'Bot' && |
| comment.body.includes('Code Quality Check Failed') |
| ); |
| |
| if (botComment) { |
| // 如果之前有失败评论,更新为成功 |
| await github.rest.issues.updateComment({ |
| owner: context.repo.owner, |
| repo: context.repo.repo, |
| comment_id: botComment.id, |
| body: '## ✅ Code Quality Check Passed\n\nAll files are properly formatted and pass linting checks!' |
| }); |
| } |