Lighthouse check #1449
Workflow file for this run
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: Lighthouse check | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "Audit mode" | |
| required: true | |
| default: "changed" | |
| type: choice | |
| options: | |
| - "changed" | |
| - "depth" | |
| depth: | |
| description: "Sitemap depth when mode is depth (0–2)" | |
| required: true | |
| default: "0" | |
| type: choice | |
| options: | |
| - "0" | |
| - "1" | |
| - "2" | |
| pr_number: | |
| description: "Pull request number to audit" | |
| required: true | |
| type: string | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| actions: read | |
| jobs: | |
| lighthouse: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Resolve PR context | |
| id: pr | |
| uses: actions/github-script@v6 | |
| with: | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| async function getPr(prNumber) { | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner, | |
| repo, | |
| pull_number: prNumber, | |
| }); | |
| if (pr.draft) { | |
| core.setFailed('Skipping Lighthouse for draft PR'); | |
| return null; | |
| } | |
| const labels = (pr.labels || []).map(l => l.name); | |
| const fullAudit = labels.includes('lighthouse:full'); | |
| return { | |
| number: pr.number, | |
| head_ref: pr.head.ref, | |
| head_sha: pr.head.sha, | |
| base_ref: pr.base.ref, | |
| full_audit: fullAudit, | |
| }; | |
| } | |
| const prNumber = parseInt('${{ inputs.pr_number }}', 10); | |
| const info = await getPr(prNumber); | |
| if (!info) return; | |
| let mode = '${{ inputs.mode }}'; | |
| let depth = '${{ inputs.depth }}'; | |
| if (info.full_audit && mode === 'changed') { | |
| mode = 'depth'; | |
| depth = '2'; | |
| } | |
| core.setOutput('number', String(info.number)); | |
| core.setOutput('head_ref', info.head_ref); | |
| core.setOutput('head_sha', info.head_sha); | |
| core.setOutput('base_ref', info.base_ref); | |
| core.setOutput('mode', mode); | |
| core.setOutput('depth', depth); | |
| core.setOutput('full_audit', String(info.full_audit)); | |
| - name: Check out repository | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.pr.outputs.head_sha }} | |
| fetch-depth: 0 | |
| - name: Set environment | |
| run: | | |
| echo "PREVIEW_URL=https://docs-staging.validmind.ai/pr_previews/${{ steps.pr.outputs.head_ref }}" >> $GITHUB_ENV | |
| echo "COMMIT_SHA=${{ steps.pr.outputs.head_sha }}" >> $GITHUB_ENV | |
| echo "COMMIT_SHA_SHORT=$(echo ${{ steps.pr.outputs.head_sha }} | cut -c1-7)" >> $GITHUB_ENV | |
| echo "LIGHTHOUSE_MODE=${{ steps.pr.outputs.mode }}" >> $GITHUB_ENV | |
| echo "LIGHTHOUSE_DEPTH=${{ steps.pr.outputs.depth }}" >> $GITHUB_ENV | |
| echo "PR_NUMBER=${{ steps.pr.outputs.number }}" >> $GITHUB_ENV | |
| - name: Check for PR preview URL | |
| id: check_preview | |
| run: | | |
| check_url() { | |
| local url=$1 | |
| local status | |
| status=$(curl -s -o /dev/null -w "%{http_code}" -I -A "Mozilla/5.0" "$url") | |
| echo "Checking $url — status: $status" | |
| [ "$status" -eq 200 ] | |
| } | |
| echo "Waiting for preview site to become available ..." | |
| for i in $(seq 1 30); do | |
| if check_url "$PREVIEW_URL/index.html"; then | |
| echo "Info: Preview site is now available" | |
| break | |
| fi | |
| if [ "$i" -eq 30 ]; then | |
| echo "Error: Preview URL did not become available after 30 minutes" | |
| exit 1 | |
| fi | |
| echo "Attempt $i/30: waiting 1 minute..." | |
| sleep 60 | |
| done | |
| if ! check_url "$PREVIEW_URL/sitemap.xml"; then | |
| echo "Error: Sitemap missing at $PREVIEW_URL/sitemap.xml" | |
| exit 1 | |
| fi | |
| echo "preview_exists=true" >> $GITHUB_OUTPUT | |
| - name: Install Python dependencies | |
| if: steps.check_preview.outputs.preview_exists == 'true' | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install requests | |
| - name: Generate URLs to check | |
| if: steps.check_preview.outputs.preview_exists == 'true' | |
| id: generate_urls | |
| env: | |
| INSTALLATION_USER: ${{ secrets.INSTALLATION_USER }} | |
| INSTALLATION_PW: ${{ secrets.INSTALLATION_PW }} | |
| run: | | |
| cd site/scripts | |
| python lighthouse_urls.py \ | |
| --mode "$LIGHTHOUSE_MODE" \ | |
| --base-ref "${{ steps.pr.outputs.base_ref }}" \ | |
| --depth "$LIGHTHOUSE_DEPTH" \ | |
| --preview-url "$PREVIEW_URL" \ | |
| --output ../../lhci-urls.txt \ | |
| --metadata ../../lighthouse-metadata.json \ | |
| --skip-file ../../lighthouse-skip.txt | |
| if [ -f ../../lighthouse-skip.txt ]; then | |
| echo "skip=true" >> $GITHUB_OUTPUT | |
| echo "No site pages to audit in this PR." | |
| exit 0 | |
| fi | |
| if [ ! -s ../../lhci-urls.txt ]; then | |
| echo "Error: No URLs were generated." | |
| exit 1 | |
| fi | |
| echo "skip=false" >> $GITHUB_OUTPUT | |
| echo "Lighthouse will check:" | |
| cat ../../lhci-urls.txt | |
| # Probe first URL from list (beyond index.html) when in changed mode | |
| if [ "$LIGHTHOUSE_MODE" = "changed" ]; then | |
| FIRST=$(head -n1 ../../lhci-urls.txt) | |
| status=$(curl -s -o /dev/null -w "%{http_code}" -I -A "Mozilla/5.0" "$FIRST") | |
| echo "Probe $FIRST — status: $status" | |
| if [ "$status" -ne 200 ]; then | |
| echo "Error: Changed page not reachable on preview" | |
| exit 1 | |
| fi | |
| fi | |
| - name: Verify installation page auth | |
| if: | | |
| steps.check_preview.outputs.preview_exists == 'true' && | |
| steps.generate_urls.outputs.skip != 'true' | |
| run: | | |
| if ! grep -q '/installation/' lhci-urls.txt 2>/dev/null; then | |
| echo "No installation pages in URL list — skipping auth check" | |
| exit 0 | |
| fi | |
| auth_url="https://${{ secrets.INSTALLATION_USER }}:${{ secrets.INSTALLATION_PW }}@docs-staging.validmind.ai/pr_previews/${{ steps.pr.outputs.head_ref }}/installation/index.html" | |
| status=$(curl -s -o /dev/null -w "%{http_code}" -I -A "Mozilla/5.0" --anyauth "$auth_url") | |
| echo "Checking installation page — status: $status" | |
| if [ "$status" -ne 200 ]; then | |
| echo "Error: Installation page not accessible with authentication" | |
| exit 1 | |
| fi | |
| - name: Post skip comment | |
| if: steps.generate_urls.outputs.skip == 'true' | |
| uses: actions/github-script@v6 | |
| with: | |
| script: | | |
| const prNumber = parseInt(process.env.PR_NUMBER, 10); | |
| const body = `## Lighthouse check results\n\n✓ INFO: No site pages to audit in this PR.\n\nCommit SHA: [${process.env.COMMIT_SHA_SHORT}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${process.env.COMMIT_SHA})`; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body, | |
| }); | |
| - name: Install Lighthouse CI | |
| if: steps.generate_urls.outputs.skip != 'true' && steps.check_preview.outputs.preview_exists == 'true' | |
| run: npm install -g @lhci/cli | |
| - name: Create Lighthouse config | |
| if: steps.generate_urls.outputs.skip != 'true' && steps.check_preview.outputs.preview_exists == 'true' | |
| run: | | |
| cat > .lighthouserc.js << 'EOF' | |
| const fs = require('fs'); | |
| const urls = fs.readFileSync('lhci-urls.txt', 'utf-8').split('\n').filter(Boolean); | |
| const urlsWithAuth = urls.map(url => { | |
| if (url.includes('/installation/')) { | |
| return `https://${process.env.INSTALLATION_USER}:${process.env.INSTALLATION_PW}@${new URL(url).host}${new URL(url).pathname}`; | |
| } | |
| return url; | |
| }); | |
| module.exports = { | |
| ci: { | |
| collect: { | |
| url: urlsWithAuth, | |
| numberOfRuns: 3, | |
| settings: { | |
| formFactor: 'desktop', | |
| screenEmulation: { | |
| mobile: false, | |
| width: 1350, | |
| height: 940, | |
| deviceScaleFactor: 1, | |
| disabled: false, | |
| }, | |
| throttling: { | |
| rttMs: 40, | |
| throughputKbps: 10240, | |
| cpuSlowdownMultiplier: 1, | |
| requestLatencyMs: 0, | |
| downloadThroughputKbps: 0, | |
| uploadThroughputKbps: 0, | |
| }, | |
| }, | |
| }, | |
| assert: { | |
| assertions: { | |
| 'categories:accessibility': ['error', { minScore: 0.9 }], | |
| }, | |
| }, | |
| upload: { | |
| target: 'temporary-public-storage', | |
| }, | |
| }, | |
| }; | |
| EOF | |
| - name: Run Lighthouse audit | |
| if: steps.generate_urls.outputs.skip != 'true' && steps.check_preview.outputs.preview_exists == 'true' | |
| uses: treosh/lighthouse-ci-action@v11 | |
| id: lighthouse | |
| env: | |
| INSTALLATION_USER: ${{ secrets.INSTALLATION_USER }} | |
| INSTALLATION_PW: ${{ secrets.INSTALLATION_PW }} | |
| with: | |
| configPath: .lighthouserc.js | |
| uploadArtifacts: true | |
| temporaryPublicStorage: true | |
| - name: Check Lighthouse audit result | |
| if: steps.generate_urls.outputs.skip != 'true' && steps.check_preview.outputs.preview_exists == 'true' | |
| run: | | |
| if [ -z "${{ steps.lighthouse.outputs.manifest }}" ]; then | |
| echo "Error: Lighthouse audit failed - no manifest output" | |
| exit 1 | |
| fi | |
| if ! echo '${{ steps.lighthouse.outputs.manifest }}' | jq . > /dev/null 2>&1; then | |
| echo "Error: Lighthouse audit failed - invalid manifest format" | |
| exit 1 | |
| fi | |
| if ! echo '${{ steps.lighthouse.outputs.manifest }}' | jq 'length > 0' > /dev/null 2>&1; then | |
| echo "Error: Lighthouse audit failed - no URLs were successfully audited" | |
| exit 1 | |
| fi | |
| # Fail if any page scored below 0.9 on accessibility | |
| below=$(echo '${{ steps.lighthouse.outputs.manifest }}' | jq '[.[] | select(.summary.accessibility < 0.9)] | length') | |
| if [ "$below" -gt 0 ]; then | |
| echo "Error: $below page(s) scored below 0.9 on accessibility" | |
| echo '${{ steps.lighthouse.outputs.manifest }}' | jq -r '.[] | select(.summary.accessibility < 0.9) | "\(.url): \(.summary.accessibility)"' | |
| exit 1 | |
| fi | |
| - name: Post Lighthouse results comment | |
| if: steps.generate_urls.outputs.skip != 'true' && steps.check_preview.outputs.preview_exists == 'true' | |
| uses: actions/github-script@v6 | |
| env: | |
| LIGHTHOUSE_MODE: ${{ env.LIGHTHOUSE_MODE }} | |
| LIGHTHOUSE_DEPTH: ${{ env.LIGHTHOUSE_DEPTH }} | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const prNumber = parseInt(process.env.PR_NUMBER, 10); | |
| const runId = context.runId; | |
| const baseUrl = process.env.PREVIEW_URL; | |
| const commitSha = process.env.COMMIT_SHA; | |
| const commitShaShort = process.env.COMMIT_SHA_SHORT; | |
| const mode = process.env.LIGHTHOUSE_MODE; | |
| const depth = process.env.LIGHTHOUSE_DEPTH; | |
| let metadata = {}; | |
| try { | |
| metadata = JSON.parse(fs.readFileSync('lighthouse-metadata.json', 'utf8')); | |
| } catch (e) { | |
| console.log('No metadata file:', e.message); | |
| } | |
| const manifest = '${{ steps.lighthouse.outputs.manifest }}'; | |
| let manifestJson; | |
| try { | |
| manifestJson = JSON.parse(manifest); | |
| if (!Array.isArray(manifestJson) || manifestJson.length === 0) { | |
| throw new Error('Invalid manifest'); | |
| } | |
| } catch (error) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: `## Lighthouse check results\n\n⚠️ WARN: Failed to parse Lighthouse results. [Workflow run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId})`, | |
| }); | |
| return; | |
| } | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| for (const comment of comments) { | |
| if (comment.user.login === 'github-actions[bot]' && | |
| comment.body.includes('## Lighthouse check results')) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: comment.id, | |
| }); | |
| } | |
| } | |
| const scores = manifestJson.map(run => run.summary.accessibility); | |
| const avgScore = (scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(2); | |
| const lighthouseReportUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${runId}`; | |
| const lighthouseComment = parseFloat(avgScore) >= 0.9 | |
| ? `✓ INFO: Average accessibility score is **${avgScore}** (required: ≥0.9) — [View the workflow run](${lighthouseReportUrl})` | |
| : `⚠️ WARN: Average accessibility score is **${avgScore}** (required: ≥0.9) — [Check the workflow run](${lighthouseReportUrl})`; | |
| const stripAuth = url => { | |
| try { | |
| const u = new URL(url); | |
| u.username = ''; | |
| u.password = ''; | |
| return u.toString(); | |
| } catch { | |
| return url; | |
| } | |
| }; | |
| const links = (() => { | |
| try { | |
| return JSON.parse(`${{ steps.lighthouse.outputs.links }}`); | |
| } catch { | |
| return {}; | |
| } | |
| })(); | |
| const scoresTable = manifestJson | |
| .map(run => { | |
| const formatScore = score => score === null ? 'N/A' : score.toFixed(2); | |
| const displayPath = stripAuth(run.url).replace(baseUrl, '') || run.url; | |
| const reportUrl = links[run.url] || lighthouseReportUrl; | |
| return `| [${displayPath}](${reportUrl}) | ${formatScore(run.summary.accessibility)} | ${formatScore(run.summary.performance)} | ${formatScore(run.summary['best-practices'])} | ${formatScore(run.summary.seo)} |`; | |
| }) | |
| .join('\n'); | |
| const modeLine = mode === 'changed' | |
| ? `Audit mode: **changed pages** (${metadata.paths?.length || manifestJson.length} URL(s))` | |
| : `Audit mode: **depth ${depth}** (sitemap)`; | |
| let comment = `## Lighthouse check results\n\n`; | |
| comment += `${lighthouseComment}\n\n`; | |
| comment += `${modeLine}\n\n`; | |
| comment += `<details>\n<summary>Show Lighthouse scores</summary>\n\n`; | |
| comment += `Commit SHA: [${commitShaShort}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${commitSha})\n\n`; | |
| if (metadata.global_fallback) { | |
| comment += `_Global site files changed — audited root navigation pages._\n\n`; | |
| } | |
| comment += `For a thorough audit, run the **Lighthouse check** workflow manually (Actions → Lighthouse check → Run workflow) with depth 0–2, or add the \`lighthouse:full\` label for depth 2 on the next validate run.\n\n`; | |
| comment += `| Page | Accessibility | Performance | Best Practices | SEO |\n`; | |
| comment += `|------|---------------|-------------|----------------|-----|\n`; | |
| comment += `${scoresTable}\n\n`; | |
| comment += `</details>\n\n`; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: comment, | |
| }); |