The Node.js® Website

Add Lighthouse CI (#6047)

* add lighthouse preview

* add more than one url

* remove unneeded step

* use other vercel action

* remove unnecessary condition

* checkout forked branch again

* increase vercel timeout

* use correct filename

* update pull-request comment

* tun assertions

* add more than one url a different way

* format lighthouse output

* fix typo

* set locale in urls

* remove unused config

* format result

* use same comment on final result

* make valid cjs module

* comment todo

* formatting scores

* revert longer timeout

* formatting

* increase vercel timeout afterall

* add more comment

* change to ESM

* add /en/about page

* Revert "change to ESM"

This reverts commit db8b02a0b652da95cc9c686b7693ef79901e96d2.

* add previous releases page

* condensed output

* add blog

* cleanup

* do not run on push

* simplify comment, trigger change

* troubleshoot why links output is empty

* testing lighthouse

* chore: simplify code, add tests

* use renamed function

* increase vercel preview timeout

authored by Brian Muenzenmeyer and committed by GitHub 1e2f96aa 3cd2d6d2

Changed files
+254
.github
workflows
scripts
lighthouse
+117
.github/workflows/lighthouse.yml
··· 1 + # Security Notes 2 + # This workflow uses `pull_request_target`, so will run against all PRs automatically (without approval), be careful with allowing any user-provided code to be run here 3 + # Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) 4 + # for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. 5 + # REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! 6 + # AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. 7 + # MERGE QUEUE NOTE: This Workflow does not run on `merge_group` trigger, as this Workflow is not required for Merge Queue's 8 + 9 + name: Lighthouse 10 + 11 + on: 12 + pull_request_target: 13 + branches: 14 + - main 15 + types: 16 + - labeled 17 + 18 + defaults: 19 + run: 20 + # This ensures that the working directory is the root of the repository 21 + working-directory: ./ 22 + 23 + permissions: 24 + contents: read 25 + actions: read 26 + # This permission is required by `thollander/actions-comment-pull-request` 27 + pull-requests: write 28 + 29 + jobs: 30 + lighthouse-ci: 31 + # We want to skip our lighthouse analysis on Dependabot PRs 32 + if: startsWith(github.event.pull_request.head.ref, 'dependabot/') == false 33 + 34 + name: Lighthouse Report 35 + runs-on: ubuntu-latest 36 + 37 + steps: 38 + - name: Git Checkout 39 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 40 + with: 41 + # Since we checkout the HEAD of the current Branch, if the Pull Request comes from a Fork 42 + # we want to clone the fork's repository instead of the base repository 43 + # this allows us to have the correct history tree of the perspective of the Pull Request's branch 44 + # If the Workflow is running on `merge_group` or `push` events it fallsback to the base repository 45 + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} 46 + # We checkout the branch itself instead of a specific SHA (Commit) as we want to ensure that this Workflow 47 + # is always running with the latest `ref` (changes) of the Pull Request's branch 48 + # If the Workflow is running on `merge_group` or `push` events it fallsback to `github.ref` which will often be `main` 49 + # or the merge_group `ref` 50 + ref: ${{ github.event.pull_request.head.ref || github.ref }} 51 + 52 + - name: Add Comment to PR 53 + # Signal that a lighthouse run is about to start 54 + uses: thollander/actions-comment-pull-request@d61db783da9abefc3437960d0cce08552c7c004f # v2.4.2 55 + with: 56 + message: | 57 + Running Lighthouse audit... 58 + # Used later to edit the existing comment 59 + comment_tag: 'lighthouse_audit' 60 + 61 + - name: Capture Vercel Preview 62 + uses: patrickedqvist/wait-for-vercel-preview@dca4940010f36d2d44caa487087a09b57939b24a # v1.3.1 63 + id: vercel_preview_url 64 + with: 65 + token: ${{ secrets.GITHUB_TOKEN }} 66 + max_timeout: 90 67 + 68 + - name: Git Checkout 69 + uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 70 + with: 71 + # By default Git Checkout on `pull-request-target` will checkout 72 + # the `default` branch of the Pull Request. We want to checkout 73 + # the actual branch of the Pull Request. 74 + ref: ${{ github.event.pull_request.head.ref }} 75 + 76 + - name: Audit Preview URL with Lighthouse 77 + # Conduct the lighthouse audit 78 + id: lighthouse_audit 79 + uses: treosh/lighthouse-ci-action@03becbfc543944dd6e7534f7ff768abb8a296826 # v10.1.0 80 + with: 81 + # Defines the settings and assertions to audit 82 + configPath: './.lighthouserc.json' 83 + # These URLS capture critical pages / site functionality. 84 + urls: | 85 + ${{ steps.vercel_preview_url.outputs.url }}/en 86 + ${{ steps.vercel_preview_url.outputs.url }}/en/about 87 + ${{ steps.vercel_preview_url.outputs.url }}/en/about/previous-releases 88 + ${{ steps.vercel_preview_url.outputs.url }}/en/download 89 + ${{ steps.vercel_preview_url.outputs.url }}/en/blog 90 + uploadArtifacts: true # save results as a action artifacts 91 + temporaryPublicStorage: true # upload lighthouse report to the temporary storage 92 + 93 + - name: Format Lighthouse Score 94 + # Transform the audit results into a single, friendlier output 95 + id: format_lighthouse_score 96 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 97 + env: 98 + # using env as input to our script 99 + # see https://github.com/actions/github-script#use-env-as-input 100 + LIGHTHOUSE_RESULT: ${{ steps.lighthouse_audit.outputs.manifest }} 101 + LIGHTHOUSE_LINKS: ${{ steps.lighthouse_audit.outputs.links }} 102 + VERCEL_PREVIEW_URL: ${{ steps.vercel_preview_url.outputs.url }} 103 + with: 104 + # Run as a separate file so we do not have to inline all of our formatting logic. 105 + # See https://github.com/actions/github-script#run-a-separate-file for more info. 106 + script: | 107 + const { formatLighthouseResults } = await import('${{github.workspace}}/scripts/lighthouse/index.mjs') 108 + await formatLighthouseResults({core}) 109 + 110 + - name: Add Comment to PR 111 + # Replace the previous message with our formatted lighthouse results 112 + uses: thollander/actions-comment-pull-request@d61db783da9abefc3437960d0cce08552c7c004f # v2.4.2 113 + with: 114 + # Reference the previously created comment 115 + comment_tag: 'lighthouse_audit' 116 + message: | 117 + ${{ steps.format_lighthouse_score.outputs.comment }}
+18
.lighthouserc.json
··· 1 + { 2 + "ci": { 3 + "collect": { 4 + "numberOfRuns": 1, 5 + "settings": { 6 + "preset": "desktop" 7 + } 8 + }, 9 + "assert": { 10 + "assertions": { 11 + "categories:performance": ["warn", { "minScore": 0.9 }], 12 + "categories:accessibility": ["warn", { "minScore": 0.9 }], 13 + "categories:best-practices": ["warn", { "minScore": 0.9 }], 14 + "categories:seo": ["warn", { "minScore": 0.9 }] 15 + } 16 + } 17 + } 18 + }
+69
scripts/lighthouse/__tests__/index.test.mjs
··· 1 + import { formatLighthouseResults } from '..'; 2 + 3 + describe('formatLighthouseResults', () => { 4 + const MOCK_VERCEL_PREVIEW_URL = `https://some.vercel.preview.url`; 5 + 6 + const MOCK_LIGHTHOUSE_RESULT = `[ 7 + { 8 + "url": "${MOCK_VERCEL_PREVIEW_URL}/en", 9 + "isRepresentativeRun": true, 10 + "summary": { "performance": 0.99, "accessibility": 0.98, "best-practices": 1, "seo": 0.96, "pwa": 0.71 } 11 + }, 12 + { 13 + "url": "${MOCK_VERCEL_PREVIEW_URL}/en/download", 14 + "isRepresentativeRun": true, 15 + "summary": { "performance": 0.49, "accessibility": 0.75, "best-practices": 1, "seo": 0.90, "pwa": 0.71 } 16 + } 17 + ]`; 18 + 19 + const MOCK_LIGHTHOUSE_LINKS = `{ 20 + "${MOCK_VERCEL_PREVIEW_URL}/en": "fake.url/to/result/1", 21 + "${MOCK_VERCEL_PREVIEW_URL}/en/download" : "fake.url/to/result/2" 22 + }`; 23 + 24 + let mockCore, originalEnv; 25 + 26 + beforeEach(() => { 27 + mockCore = { setOutput: jest.fn() }; 28 + originalEnv = process.env; 29 + process.env = { 30 + ...process.env, 31 + LIGHTHOUSE_RESULT: MOCK_LIGHTHOUSE_RESULT, 32 + LIGHTHOUSE_LINKS: MOCK_LIGHTHOUSE_LINKS, 33 + VERCEL_PREVIEW_URL: MOCK_VERCEL_PREVIEW_URL, 34 + }; 35 + }); 36 + 37 + afterEach(() => { 38 + process.env = originalEnv; 39 + }); 40 + 41 + it('formats preview urls correctly', () => { 42 + formatLighthouseResults({ core: mockCore }); 43 + 44 + const expectations = [ 45 + expect.stringContaining(`[/en](${MOCK_VERCEL_PREVIEW_URL}/en)`), 46 + expect.stringContaining( 47 + `[/en/download](${MOCK_VERCEL_PREVIEW_URL}/en/download)` 48 + ), 49 + ]; 50 + 51 + expectations.forEach(expectation => { 52 + expect(mockCore.setOutput).toBeCalledWith('comment', expectation); 53 + }); 54 + }); 55 + 56 + it('formats stoplight colors correctly', () => { 57 + formatLighthouseResults({ core: mockCore }); 58 + 59 + const expectations = [ 60 + expect.stringContaining(`🟢 90`), 61 + expect.stringContaining(`🟠 75`), 62 + expect.stringContaining(`🔴 49`), 63 + ]; 64 + 65 + expectations.forEach(expectation => { 66 + expect(mockCore.setOutput).toBeCalledWith('comment', expectation); 67 + }); 68 + }); 69 + });
+50
scripts/lighthouse/index.mjs
··· 1 + 'use strict'; 2 + 3 + const stoplight = res => (res >= 90 ? '🟢' : res >= 75 ? '🟠' : '🔴'); 4 + const normalizeScore = res => Math.round(res * 100); 5 + const formatScore = res => { 6 + const normalizedScore = normalizeScore(res); 7 + return `${stoplight(normalizedScore)} ${normalizedScore}`; 8 + }; 9 + 10 + /** 11 + * `core` is in scope from https://github.com/actions/github-script 12 + */ 13 + export const formatLighthouseResults = ({ core }) => { 14 + // this will be the shape of https://github.com/treosh/lighthouse-ci-action#manifest 15 + const results = JSON.parse(process.env.LIGHTHOUSE_RESULT); 16 + 17 + // this will be the shape of https://github.com/treosh/lighthouse-ci-action#links 18 + const links = JSON.parse(process.env.LIGHTHOUSE_LINKS); 19 + 20 + // start creating our markdown table 21 + const header = [ 22 + 'Lighthouse Results', 23 + 'URL | Performance | Accessibility | Best Practices | SEO | Report', 24 + '| - | - | - | - | - | - |', 25 + ]; 26 + 27 + // map over each url result, formatting and linking to the output 28 + const urlResults = results.map(({ url, summary }) => { 29 + // make the tested link as a markdown link, without the long-generated host 30 + const shortPreviewLink = `[${url.replace( 31 + process.env.VERCEL_PREVIEW_URL, 32 + '' 33 + )}](${url})`; 34 + 35 + // make each formatted score from our lighthouse properties 36 + const performanceScore = formatScore(summary.performance); 37 + const accessibilityScore = formatScore(summary.accessibility); 38 + const bestPracticesScore = formatScore(summary['best-practices']); 39 + const seoScore = formatScore(summary.seo); 40 + 41 + // create the markdown table row 42 + return `${shortPreviewLink} | ${performanceScore} | ${accessibilityScore} | ${bestPracticesScore} | ${seoScore} | [🔗](${links[url]})`; 43 + }); 44 + 45 + // join the header and the rows together 46 + const finalResults = [...header, ...urlResults].join('\n'); 47 + 48 + // return our output to the github action 49 + core.setOutput('comment', finalResults); 50 + };