Merge pull request #617 from Junman140/fix/612-613-614-615 #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Frontend CI | |
| on: | |
| pull_request_target: | |
| paths: | |
| - "frontend/**" | |
| - ".github/workflows/frontend.yml" | |
| - "lighthouserc.js" | |
| - "budget.json" | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - "frontend/**" | |
| - ".github/workflows/frontend.yml" | |
| - "lighthouserc.js" | |
| - "budget.json" | |
| jobs: | |
| frontend: | |
| name: Frontend checks | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: frontend | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20.x | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - run: npm ci | |
| - run: npm run lint | |
| - run: npm run build | |
| lighthouse: | |
| name: Lighthouse CI | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| # Only run on PRs to avoid burning CI minutes on every push to main | |
| if: github.event_name == 'pull_request_target' | |
| defaults: | |
| run: | |
| working-directory: frontend | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20.x | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - run: npm ci | |
| - run: npm run build | |
| - name: Run Lighthouse CI | |
| run: npx @lhci/cli@0.14.x autorun --config=../lighthouserc.js | |
| env: | |
| LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} | |
| - name: Upload Lighthouse report artifacts | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: lighthouse-results-${{ github.sha }} | |
| path: frontend/.lighthouseci/ | |
| retention-days: 14 | |
| - name: Comment PR with Lighthouse results | |
| uses: actions/github-script@v7 | |
| if: github.event_name == 'pull_request_target' && always() | |
| continue-on-error: true | |
| env: | |
| RESULTS_DIR: frontend/.lighthouseci/ | |
| with: | |
| script: | | |
| const fs = require("fs"); | |
| const path = require("path"); | |
| const resultsDir = path.join( | |
| process.env.GITHUB_WORKSPACE, | |
| process.env.RESULTS_DIR | |
| ); | |
| const prNumber = | |
| context.issue.number || context.payload.pull_request?.number; | |
| if (!prNumber) { | |
| console.log("Not a PR — skipping comment"); | |
| return; | |
| } | |
| // Collect report URL from LHCI links | |
| let reportUrl = null; | |
| const linksPath = path.join(resultsDir, "links.json"); | |
| if (fs.existsSync(linksPath)) { | |
| try { | |
| const links = JSON.parse(fs.readFileSync(linksPath, "utf8")); | |
| if (links.length > 0) reportUrl = links[0]; | |
| } catch { | |
| console.log("Could not parse links.json"); | |
| } | |
| } | |
| // Read LHR files to extract scores | |
| let scores = []; | |
| try { | |
| const files = fs.readdirSync(resultsDir); | |
| const lhrFiles = files.filter( | |
| (f) => f.startsWith("lhr-") && f.endsWith(".json") | |
| ); | |
| scores = lhrFiles.map((f) => { | |
| const lhr = JSON.parse( | |
| fs.readFileSync(path.join(resultsDir, f), "utf8") | |
| ); | |
| return { | |
| performance: Math.round( | |
| ((lhr.categories.performance?.score ?? 0) * 100) | |
| ), | |
| accessibility: Math.round( | |
| ((lhr.categories.accessibility?.score ?? 0) * 100) | |
| ), | |
| bestPractices: Math.round( | |
| ((lhr.categories["best-practices"]?.score ?? 0) * 100) | |
| ), | |
| seo: Math.round( | |
| ((lhr.categories.seo?.score ?? 0) * 100) | |
| ), | |
| lcp: lhr.audits["largest-contentful-paint"]?.numericValue, | |
| tbt: lhr.audits["total-blocking-time"]?.numericValue, | |
| cls: lhr.audits["cumulative-layout-shift"]?.numericValue, | |
| }; | |
| }); | |
| } catch (e) { | |
| console.log("Could not read LHR files:", e.message); | |
| } | |
| if (scores.length === 0) { | |
| await github.rest.issues.createComment({ | |
| ...context.repo, | |
| issue_number: prNumber, | |
| body: | |
| "## Lighthouse Report 🚀\n\n" + | |
| "⚠️ Lighthouse results not available. " + | |
| "The audit may have failed during collection.\n", | |
| }); | |
| return; | |
| } | |
| // Average scores across runs | |
| const count = scores.length; | |
| const avg = (key) => | |
| scores.reduce((sum, s) => sum + (s[key] || 0), 0) / count; | |
| const perf = avg("performance"); | |
| const access = avg("accessibility"); | |
| const bp = avg("bestPractices"); | |
| const seoScore = avg("seo"); | |
| const lcp = avg("lcp"); | |
| const tbt = avg("tbt"); | |
| const cls = avg("cls"); | |
| const badge = (score, pass) => | |
| score >= pass ? `${score} 🟢` : `${score} 🔴`; | |
| const fmtMs = (v) => | |
| v != null ? `${Math.round(v)} ms` : "—"; | |
| const fmtSec = (v) => | |
| v != null ? `${(v / 1000).toFixed(2)} s` : "—"; | |
| const fmtCls = (v) => | |
| v != null ? v.toFixed(3) : "—"; | |
| const body = [ | |
| "## Lighthouse Report 🚀\n", | |
| "| Category | Score |", | |
| "|----------|-------|", | |
| `| **Performance** | ${badge(perf, 70)} |`, | |
| `| **Accessibility** | ${badge(access, 85)} |`, | |
| `| **Best Practices** | ${badge(bp, 80)} |`, | |
| `| **SEO** | ${badge(seoScore, 85)} |\n`, | |
| "### Web Vitals", | |
| "| Metric | Value |", | |
| "|--------|-------|", | |
| `| **LCP** (target < 4.0 s) | ${fmtSec(lcp)} |`, | |
| `| **TBT** (target < 500 ms) | ${fmtMs(tbt)} |`, | |
| `| **CLS** (target < 0.15) | ${fmtCls(cls)} |\n`, | |
| reportUrl | |
| ? `[View full report](${reportUrl})\n` | |
| : "", | |
| `_Ran ${count} audit(s) on ${new Date().toISOString().split("T")[0]}_`, | |
| ].join("\n"); | |
| // Find existing bot comment to update (for re-runs) | |
| const { data: comments } = | |
| await github.rest.issues.listComments({ | |
| ...context.repo, | |
| issue_number: prNumber, | |
| }); | |
| const botComment = comments.find( | |
| (c) => | |
| c.user?.type === "Bot" && | |
| /Lighthouse Report/.test(c.body || "") | |
| ); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| ...context.repo, | |
| comment_id: botComment.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| ...context.repo, | |
| issue_number: prNumber, | |
| body, | |
| }); | |
| } |