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" # 根据文件位置选择正确的 prettier 配置 if [[ "$file" == web/admin-spa/* ]]; then # 前端文件:进入前端目录运行 prettier 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 # 后端文件:使用根目录的 prettier 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!' }); }