-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4773 from guardian/jtl-surface-lighthouse
feat: Surface Lighthouse results
- Loading branch information
Showing
4 changed files
with
251 additions
and
23 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,41 @@ | ||
name: DCR Run Lighthouse CI | ||
on: | ||
push: | ||
paths-ignore: | ||
- "apps-rendering/**" | ||
push: | ||
branches: | ||
- main | ||
paths-ignore: | ||
- "apps-rendering/**" | ||
pull_request: | ||
# If/when we compare results to `main`, we should also run on 'reopened' | ||
types: [opened, synchronize] | ||
|
||
jobs: | ||
lhci: | ||
name: DCR Lighthouse | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v3 | ||
- name: Install Node | ||
uses: guardian/actions-setup-node@main | ||
# Make sure we install dependencies in the root directory | ||
- uses: bahmutov/npm-install@v1 | ||
- run: make build | ||
working-directory: dotcom-rendering | ||
- name: Install and run Lighthouse CI | ||
working-directory: dotcom-rendering | ||
env: | ||
LHCI_GITHUB_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }} | ||
run: | | ||
npm install -g [email protected] @lhci/[email protected] | ||
lhci autorun | ||
lhci: | ||
name: DCR Lighthouse | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout code | ||
uses: actions/checkout@v3 | ||
- name: Install Node | ||
uses: guardian/actions-setup-node@main | ||
# Make sure we install dependencies in the root directory | ||
- uses: bahmutov/npm-install@v1 | ||
- run: make build | ||
working-directory: dotcom-rendering | ||
- name: Install and run Lighthouse CI | ||
working-directory: dotcom-rendering | ||
env: | ||
LHCI_GITHUB_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }} | ||
run: | | ||
npm install -g [email protected] @lhci/[email protected] | ||
lhci autorun | ||
- name: Setup deno | ||
uses: denolib/setup-deno@v2 | ||
with: | ||
deno-version: v1.21.0 | ||
|
||
- name: Surface Lighthouse Results | ||
run: deno run --no-check --allow-net=api.github.com --allow-env="GITHUB_TOKEN","GITHUB_EVENT_PATH" --allow-read scripts/deno/surface-lighthouse-results.ts | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,3 +44,6 @@ coverage | |
|
||
# Architecture Diagram | ||
webArchitecture.svg | ||
|
||
# lighthouse results from local build | ||
.lighthouseci/ |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
import { Octokit } from "https://cdn.skypack.dev/octokit"; | ||
import type { RestEndpointMethodTypes } from "https://cdn.skypack.dev/@octokit/plugin-rest-endpoint-methods?dts"; | ||
import type { EventPayloadMap } from "https://cdn.skypack.dev/@octokit/webhooks-types?dts"; | ||
import "https://raw.githubusercontent.com/GoogleChrome/lighthouse-ci/v0.8.2/types/assert.d.ts"; | ||
|
||
/* -- Setup -- */ | ||
|
||
type OctokitWithRest = { | ||
rest: { | ||
issues: { | ||
[Method in keyof RestEndpointMethodTypes["issues"]]: ( | ||
arg: RestEndpointMethodTypes["issues"][Method]["parameters"] | ||
) => Promise<RestEndpointMethodTypes["issues"][Method]["response"]>; | ||
}; | ||
}; | ||
}; | ||
|
||
/** Github token for Authentication */ | ||
const token = Deno.env.get("GITHUB_TOKEN"); | ||
if (!token) throw new Error("Missing GITHUB_TOKEN"); | ||
|
||
/** Path for workflow event */ | ||
const path = Deno.env.get("GITHUB_EVENT_PATH"); | ||
if (!path) throw new Error("Missing GITHUB_EVENT_PATH"); | ||
|
||
/** | ||
* https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads | ||
*/ | ||
const payload: EventPayloadMap["push" | "pull_request"] = JSON.parse( | ||
Deno.readTextFileSync(path) | ||
); | ||
|
||
const isPullRequestEvent = ( | ||
payload: EventPayloadMap[keyof EventPayloadMap] | ||
): payload is EventPayloadMap["pull_request"] => | ||
//@ts-expect-error -- We’re actually checking the type | ||
typeof payload?.pull_request?.number === "number"; | ||
|
||
/** | ||
* One of two values depending on the workflow trigger event | ||
* | ||
* - on a "pull_request" event: the current Issue / PR number | ||
* on which to add or update the Lighthouse report | ||
* | ||
* - on a "push" event: the original issue to track the latest report: | ||
* https://github.com/guardian/dotcom-rendering/issues/4584, which allows | ||
* to track the state Lighthouse CI Reports on `main` over time. | ||
*/ | ||
const issue_number = isPullRequestEvent(payload) | ||
? // If PullRequestEvent | ||
payload.pull_request.number | ||
: // If PushEvent | ||
4584; | ||
|
||
console.log(`Using issue #${issue_number}`); | ||
|
||
/** The Lighthouse results directory */ | ||
const dir = "dotcom-rendering/.lighthouseci"; | ||
|
||
const links: Record<string, string> = JSON.parse( | ||
Deno.readTextFileSync(`${dir}/links.json`) | ||
); | ||
|
||
/** https://github.com/GoogleChrome/lighthouse-ci/blob/5963dcce0e88b8d3aedaba56a93ec4b93cf073a1/packages/utils/src/assertions.js#L15-L30 */ | ||
interface AssertionResult { | ||
url: string; | ||
name: | ||
| keyof Omit<LHCI.AssertCommand.AssertionOptions, "aggregationMethod"> | ||
| "auditRan"; | ||
operator: string; | ||
expected: number; | ||
actual: number; | ||
values: number[]; | ||
passed: boolean; | ||
level?: LHCI.AssertCommand.AssertionFailureLevel; | ||
auditId?: string; | ||
auditProperty?: string; | ||
auditTitle?: string; | ||
auditDocumentationLink?: string; | ||
message?: string; | ||
} | ||
const results: AssertionResult[] = JSON.parse( | ||
Deno.readTextFileSync(`${dir}/assertion-results.json`) | ||
); | ||
|
||
/* -- Definitions -- */ | ||
|
||
/** The string to search for when looking for a comment */ | ||
const REPORT_TITLE = "⚡️ Lighthouse report"; | ||
const GIHUB_PARAMS = { | ||
owner: "guardian", | ||
repo: "dotcom-rendering", | ||
issue_number, | ||
} as const; | ||
|
||
// @ts-expect-error -- Octokit’s own types are not as good as ours | ||
const octokit = new Octokit({ auth: token }) as OctokitWithRest; | ||
|
||
/* -- Methods -- */ | ||
|
||
/** If large number, round to 0 decimal, if small, round to 6 decimal points */ | ||
const formatNumber = (expected: number, actual: number): string => | ||
expected > 100 ? Math.ceil(actual).toString() : actual.toFixed(6); | ||
|
||
const getStatus = ( | ||
passed: boolean, | ||
level: AssertionResult["level"] | ||
): "✅" | "⚠️" | "❌" => { | ||
if (passed) return "✅"; | ||
|
||
switch (level) { | ||
case "off": | ||
case "warn": | ||
return "⚠️"; | ||
case "error": | ||
default: | ||
return "❌"; | ||
} | ||
}; | ||
|
||
const generateAuditTable = ( | ||
auditUrl: string, | ||
results: AssertionResult[] | ||
): string => { | ||
const reportUrl = links[auditUrl]; | ||
|
||
const resultsTemplateString = results.map( | ||
({ auditTitle, auditProperty, passed, expected, actual, level }) => | ||
`| ${auditTitle ?? auditProperty ?? "Unknown Test"} | ${getStatus( | ||
passed, | ||
level | ||
)} | ${expected} | ${formatNumber(expected, actual)} |` | ||
); | ||
|
||
const [endpoint, testUrlClean] = auditUrl.split("?url="); | ||
|
||
const table = [ | ||
`### [Report for ${endpoint.split("/").slice(-1)}](${reportUrl})`, | ||
`> tested url \`${testUrlClean}\``, | ||
"", | ||
"| Category | Status | Expected | Actual |", | ||
"| --- | --- | --- | --- |", | ||
...resultsTemplateString, | ||
"", | ||
].join("\n"); | ||
|
||
return table; | ||
}; | ||
|
||
const createLighthouseResultsMd = (): string => { | ||
const auditCount = results.length; | ||
const failedAuditCount = results.filter((result) => !result.passed).length; | ||
const auditUrls = [...new Set<string>(results.map((result) => result.url))]; | ||
|
||
return [ | ||
`## ${REPORT_TITLE} for the changes in this PR`, | ||
`Lighthouse tested ${auditUrls.length} URLs `, | ||
failedAuditCount > 0 | ||
? `⚠️ Budget exceeded for ${failedAuditCount} of ${auditCount} audits.` | ||
: "All audits passed", | ||
...auditUrls.map((url) => | ||
generateAuditTable( | ||
url, | ||
results.filter((result) => result.url === url) | ||
) | ||
), | ||
].join("\n\n"); | ||
}; | ||
|
||
const getCommentID = async (): Promise<number | null> => { | ||
const { data: comments } = await octokit.rest.issues.listComments({ | ||
...GIHUB_PARAMS, | ||
}); | ||
|
||
const comment = comments.find((comment) => | ||
comment.body?.includes(REPORT_TITLE) | ||
); | ||
|
||
return comment?.id ?? null; | ||
}; | ||
|
||
try { | ||
const body = createLighthouseResultsMd(); | ||
const comment_id = await getCommentID(); | ||
|
||
const { data } = comment_id | ||
? await octokit.rest.issues.updateComment({ | ||
...GIHUB_PARAMS, | ||
comment_id, | ||
body, | ||
}) | ||
: await octokit.rest.issues.createComment({ | ||
...GIHUB_PARAMS, | ||
body, | ||
}); | ||
|
||
console.log( | ||
`Successfully ${ | ||
comment_id ? "updated" : "created" | ||
} Lighthouse report comment` | ||
); | ||
console.log("See:", data.html_url); | ||
} catch (error) { | ||
if (error instanceof Error) throw error; | ||
|
||
console.error("there was an error:", error.message); | ||
Deno.exit(1); | ||
} |