Repository navigation
269 lines (242 loc) · 10.5 KB
/
Copy pathlighthouse.yml
File metadata and controls
269 lines (242 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
name: Lighthouse
# Lighthouse and axe for both sites, on each pull request's Vercel preview.
# bench renders from its database, which only a deployment has, so the jobs
# audit the site and bench together there, instead of a local build.
#
# Each page gets its own Lighthouse job, and axe runs in another, all at the
# same time. They can't share a runner: a second audit or browser on the same
# CPU would skew Lighthouse's timings. The `report` job, the required check
# "Lighthouse and axe", collects their results.
on:
pull_request:
permissions:
contents: read
# Including `github.workflow` in the key keeps this from colliding with any
# other workflow that happens to share a ref-based group prefix.
concurrency:
group: lighthouse-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
PROXY: http://127.0.0.1:3201
jobs:
lighthouse:
name: Lighthouse (${{ matrix.page }})
# Fork PRs and Dependabot's PRs get no secrets, so they can't reach the
# protected preview. The jobs are skipped for them, which counts as
# passing for the required check.
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
deployments: read
strategy:
fail-fast: false
# The keys of `pages` in lighthouserc.cjs.
matrix:
page: [home, blog, post, bench, chart]
steps:
# The PR's head commit, which Vercel built the preview from, not the
# merge commit a pull_request checkout defaults to: the config must
# describe the app it audits (a post only on main would be a 404).
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
# The proxy and lighthouserc.cjs use only Node's built-in modules, so
# this job needs no `bun install`.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- uses: ./.github/actions/preview-proxy
with:
sha: ${{ github.event.pull_request.head.sha }}
bypass-secret: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
# Request the page once, so no Lighthouse run measures a cold start.
# The chart job audits the first chart bench lists, and finds none on
# a database with no charts.
- name: Pick the page and warm it
id: page
env:
PAGE: ${{ matrix.page }}
run: |
path="$(node -p "require('./lighthouserc.cjs').pages['${PAGE}'] ?? ''")"
if [ "${PAGE}" = chart ]; then
slug="$(curl -sf "${PROXY}/bench/api/groups" \
| jq -r '[.groups[] | select(.charts | length > 0)][0].charts[0].slug // empty')"
if [ -z "${slug}" ]; then
echo "::notice::bench lists no charts, so there is no chart page to audit."
exit 0
fi
echo "chart=/chart/${slug}" >> "$GITHUB_OUTPUT"
path="/bench/chart/${slug}"
fi
if [ -z "${path}" ]; then
echo "::notice::There is no ${PAGE} page to audit."
exit 0
fi
echo "path=${path}" >> "$GITHUB_OUTPUT"
curl -s -o /dev/null -w "%{http_code} %{time_total}s ${path}\n" "${PROXY}${path}"
# The action runs Lighthouse against the page, asserts the thresholds
# and writes the reports to .lighthouseci/. They stay out of
# Lighthouse's public report storage, because the pages come from the
# protected preview. Threshold rationale lives in lighthouserc.cjs.
- name: Lighthouse CI
if: steps.page.outputs.path != ''
uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18 # v12
env:
BASE_URL: ${{ env.PROXY }}
LHCI_PAGE: ${{ matrix.page }}
BENCH_CHART_PATH: ${{ steps.page.outputs.chart }}
with:
configPath: ./lighthouserc.cjs
uploadArtifacts: false
# The report job's comment needs only each run's URL and category
# scores. The full reports hold screenshots and page details of the
# protected preview, and anyone signed in to GitHub can download a
# public repository's artifacts, so the full reports stay on the
# runner. The assertion results print in the Lighthouse CI step's log.
- name: Keep only the scores
if: always() && steps.page.outputs.path != ''
run: |
set -Eeuo pipefail
shopt -s nullglob
mkdir -p "${RUNNER_TEMP}/lighthouse-scores"
for report in .lighthouseci/lhr-*.json; do
jq '{finalUrl, categories: (.categories | map_values({score}))}' "${report}" \
> "${RUNNER_TEMP}/lighthouse-scores/$(basename "${report}")"
done
# Uploaded when the assertions fail too.
- name: Upload the scores
if: always() && steps.page.outputs.path != ''
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: lighthouse-${{ matrix.page }}
path: ${{ runner.temp }}/lighthouse-scores/
if-no-files-found: ignore
retention-days: 7
axe:
name: axe
if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]'
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
deployments: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.4.4
# Bun is the package manager and script runner; the proxy and
# Playwright run on Node, pinned by .nvmrc.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: .nvmrc
- name: Cache Bun install cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install
run: bun install --frozen-lockfile
- uses: ./.github/actions/preview-proxy
with:
sha: ${{ github.event.pull_request.head.sha }}
bypass-secret: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
# The same browsers as the smoke tests in ci.yml, from the same cache,
# keyed on the installed Playwright version and the runner image.
- name: Get Playwright version
id: playwright-version
run: |
VERSION=$(node -p "require('@playwright/test/package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "image=${ImageOS:-unknown}" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ steps.playwright-version.outputs.image }}-playwright-${{ steps.playwright-version.outputs.version }}
- name: Install Playwright browsers
env:
CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }}
run: |
if [ "${CACHE_HIT}" = "true" ]; then
bunx playwright install-deps chromium webkit
else
bunx playwright install --with-deps chromium webkit
fi
# playwright.config.ts runs one test at a time, for the smoke tests
# against the local server in ci.yml. The preview has no such limit, so
# axe uses the runner's four CPUs.
- name: axe
env:
BASE_URL: ${{ env.PROXY }}
BENCH_URL: ${{ env.PROXY }}/bench
run: bunx playwright test --grep @axe --workers=4
# In CI, playwright.config.ts uses only the `github` reporter, so there
# is no HTML report; a failed test keeps its trace in test-results/.
- name: Upload Playwright traces
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: playwright-traces
path: test-results/
retention-days: 7
# The required check. It runs when a job above fails, so the comment still
# shows which page regressed, and it fails unless every job above passed.
# It is skipped with them for fork and Dependabot PRs.
report:
name: Lighthouse and axe
needs: [lighthouse, axe]
if: ${{ !cancelled() && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-26.04
timeout-minutes: 10
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
sparse-checkout: scripts/lighthouse-comment.sh
sparse-checkout-cone-mode: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: lighthouse-*
path: reports
# A failed comment must not fail this required check, so both comment
# steps are non-fatal.
- name: Format PR comment
id: lh-comment
continue-on-error: true
run: |
shopt -s nullglob
reports=(reports/*/lhr-*.json)
if [ "${#reports[@]}" -eq 0 ]; then exit 0; fi
{
echo 'body<<COMMENT_EOF'
scripts/lighthouse-comment.sh 'Lighthouse audit' "${reports[@]}"
echo 'COMMENT_EOF'
} >> "$GITHUB_OUTPUT"
- name: Post or update PR comment
if: steps.lh-comment.outputs.body != ''
continue-on-error: true
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: lighthouse
message: ${{ steps.lh-comment.outputs.body }}
- name: Require every job to pass
env:
LIGHTHOUSE: ${{ needs.lighthouse.result }}
AXE: ${{ needs.axe.result }}
run: |
echo "Lighthouse: ${LIGHTHOUSE}, axe: ${AXE}"
[ "${LIGHTHOUSE}" = success ] && [ "${AXE}" = success ]