Skip to content

Commit 3fe960e

Browse files
authored
Add Lighthouse CI to preview deployments (#47)
1 parent ca04b1e commit 3fe960e

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

.github/workflows/fly-preview.yml

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,121 @@ jobs:
168168
body,
169169
});
170170
}
171+
172+
lighthouse:
173+
name: Lighthouse CI
174+
runs-on: ubuntu-latest
175+
needs: preview
176+
if: github.event.action != 'closed'
177+
continue-on-error: true
178+
179+
steps:
180+
- name: Checkout
181+
uses: actions/checkout@v4
182+
183+
- name: Wait for preview to be ready
184+
run: |
185+
echo "Waiting for preview deployment to be ready..."
186+
for i in {1..30}; do
187+
if curl -s -o /dev/null -w "%{http_code}" "${{ needs.preview.outputs.url }}" | grep -q "200"; then
188+
echo "Preview is ready!"
189+
exit 0
190+
fi
191+
echo "Attempt $i: Preview not ready yet, waiting 10s..."
192+
sleep 10
193+
done
194+
echo "Preview did not become ready in time"
195+
exit 1
196+
197+
- name: Get changed pages
198+
id: changed-pages
199+
uses: actions/github-script@v7
200+
with:
201+
script: |
202+
const { data: files } = await github.rest.pulls.listFiles({
203+
owner: context.repo.owner,
204+
repo: context.repo.repo,
205+
pull_number: context.issue.number,
206+
});
207+
208+
const baseUrl = '${{ needs.preview.outputs.url }}';
209+
const urls = new Set([baseUrl]); // Always test homepage
210+
211+
for (const file of files) {
212+
// Match .mdx files in src/content/docs/
213+
const match = file.filename.match(/^src\/content\/docs\/(.+)\.mdx$/);
214+
if (match && file.status !== 'removed') {
215+
let path = match[1];
216+
// index.mdx maps to root, others map to their path
217+
if (path === 'index') {
218+
urls.add(baseUrl);
219+
} else {
220+
urls.add(`${baseUrl}/${path}/`);
221+
}
222+
}
223+
}
224+
225+
const urlList = Array.from(urls).join('\n');
226+
console.log('URLs to test:\n' + urlList);
227+
core.setOutput('urls', urlList);
228+
229+
- name: Run Lighthouse CI
230+
id: lighthouse
231+
uses: treosh/lighthouse-ci-action@v12
232+
with:
233+
urls: ${{ steps.changed-pages.outputs.urls }}
234+
configPath: ./lighthouserc.json
235+
budgetPath: ./budget.json
236+
uploadArtifacts: true
237+
temporaryPublicStorage: true
238+
239+
- name: Comment Lighthouse results on PR
240+
if: always()
241+
uses: actions/github-script@v7
242+
with:
243+
script: |
244+
const manifest = ${{ steps.lighthouse.outputs.manifest }};
245+
const links = ${{ steps.lighthouse.outputs.links }};
246+
247+
let body = '### Lighthouse Results\n\n';
248+
body += '| URL | Performance | Accessibility | Best Practices | SEO |\n';
249+
body += '|-----|-------------|---------------|----------------|-----|\n';
250+
251+
for (const result of manifest) {
252+
const url = new URL(result.url).pathname || '/';
253+
const perf = Math.round(result.summary.performance * 100);
254+
const a11y = Math.round(result.summary.accessibility * 100);
255+
const bp = Math.round(result.summary['best-practices'] * 100);
256+
const seo = Math.round(result.summary.seo * 100);
257+
258+
const perfEmoji = perf >= 90 ? '🟢' : perf >= 50 ? '🟠' : '🔴';
259+
const a11yEmoji = a11y >= 90 ? '🟢' : a11y >= 50 ? '🟠' : '🔴';
260+
const bpEmoji = bp >= 90 ? '🟢' : bp >= 50 ? '🟠' : '🔴';
261+
const seoEmoji = seo >= 90 ? '🟢' : seo >= 50 ? '🟠' : '🔴';
262+
263+
const reportLink = links[result.url] ? `[${url}](${links[result.url]})` : url;
264+
body += `| ${reportLink} | ${perfEmoji} ${perf} | ${a11yEmoji} ${a11y} | ${bpEmoji} ${bp} | ${seoEmoji} ${seo} |\n`;
265+
}
266+
267+
const { data: comments } = await github.rest.issues.listComments({
268+
owner: context.repo.owner,
269+
repo: context.repo.repo,
270+
issue_number: context.issue.number,
271+
});
272+
const existing = comments.find(c => c.body.includes('### Lighthouse Results'));
273+
274+
if (existing) {
275+
await github.rest.issues.updateComment({
276+
owner: context.repo.owner,
277+
repo: context.repo.repo,
278+
comment_id: existing.id,
279+
body,
280+
});
281+
} else {
282+
await github.rest.issues.createComment({
283+
owner: context.repo.owner,
284+
repo: context.repo.repo,
285+
issue_number: context.issue.number,
286+
body,
287+
});
288+
}

budget.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"path": "/*",
4+
"resourceSizes": [
5+
{ "resourceType": "document", "budget": 50 },
6+
{ "resourceType": "script", "budget": 300 },
7+
{ "resourceType": "stylesheet", "budget": 100 },
8+
{ "resourceType": "image", "budget": 500 },
9+
{ "resourceType": "font", "budget": 200 },
10+
{ "resourceType": "total", "budget": 1000 }
11+
],
12+
"resourceCounts": [
13+
{ "resourceType": "script", "budget": 15 },
14+
{ "resourceType": "third-party", "budget": 5 }
15+
]
16+
}
17+
]

lighthouserc.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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.8 }],
12+
"categories:accessibility": ["error", { "minScore": 0.9 }],
13+
"categories:best-practices": ["warn", { "minScore": 0.9 }],
14+
"categories:seo": ["warn", { "minScore": 0.9 }],
15+
"first-contentful-paint": ["warn", { "maxNumericValue": 2000 }],
16+
"largest-contentful-paint": ["warn", { "maxNumericValue": 2500 }],
17+
"cumulative-layout-shift": ["warn", { "maxNumericValue": 0.1 }],
18+
"total-blocking-time": ["warn", { "maxNumericValue": 300 }]
19+
}
20+
},
21+
"upload": {
22+
"target": "temporary-public-storage"
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)