Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Surface Lighthouse results #4773

Merged
merged 28 commits into from
May 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
74f914b
Adding new deno script to surface lighthouse results on a PR
jlieb10 Apr 27, 2022
6fbfead
removed local lighthouse files
jlieb10 Apr 27, 2022
c877bd7
work in progress
jlieb10 Apr 28, 2022
8051900
Merge main into jtl-surface-lighthouse
mxdvl Apr 28, 2022
a522c00
feat: add types for the Github API
mxdvl Apr 28, 2022
366febb
feat: surface lightouse scores
mxdvl Apr 28, 2022
d92a979
fix: install deno (doh)
mxdvl Apr 28, 2022
194b57e
fix: no type check
mxdvl Apr 28, 2022
30efe3a
feat: add types
mxdvl Apr 28, 2022
baf9675
feat: pretty testing tables
mxdvl Apr 28, 2022
307c213
fix: allow deno to read files
mxdvl Apr 28, 2022
0aa4bbd
log more info
mxdvl Apr 29, 2022
78d32e6
fix: run on pull_request synchronize
mxdvl Apr 29, 2022
43cbd9b
refactor: organise steps
mxdvl Apr 29, 2022
8e43c5a
feat: improve octokit types
mxdvl Apr 29, 2022
02f840f
feat: include passed Lighthouse assertions
mxdvl Apr 29, 2022
0dc1129
refactor: organise files
mxdvl Apr 29, 2022
11f4285
refactor: more types
mxdvl Apr 29, 2022
a9a4ebd
refactor: rename title
mxdvl Apr 29, 2022
8f9d511
feat: Add Lighthouse types
mxdvl Apr 29, 2022
d432b2d
fix: types for lhci/cli
mxdvl Apr 29, 2022
35ce111
feat: add warning sign
mxdvl Apr 29, 2022
847440d
feat: log some info along the way
mxdvl Apr 29, 2022
ca4e74e
fix: check the pull request type correctly
mxdvl Apr 29, 2022
6b40cc8
docs(lighthouse): more information
mxdvl May 3, 2022
ac7ac3c
refactor: extract Octokit type
mxdvl May 3, 2022
522d014
fix: action + lighthouse passed assertion
mxdvl May 3, 2022
bd58254
docs: moar docs
mxdvl May 3, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 38 additions & 22 deletions .github/workflows/lighthouse.yml
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 }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ coverage

# Architecture Diagram
webArchitecture.svg

# lighthouse results from local build
.lighthouseci/
3 changes: 2 additions & 1 deletion dotcom-rendering/lighthouserc.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ module.exports = {
puppeteerScript: './scripts/lighthouse/puppeteer-script.js',
settings: {
onlyCategories: "accessibility,best-practices,performance,seo",
disableStorageReset: true
disableStorageReset: true,
}
},
upload: {
target: 'temporary-public-storage',
},
assert: {
includePassedAssertions: true,
assertions: {
"first-contentful-paint": ["warn", {"maxNumericValue": 1500}],
"largest-contentful-paint": ["warn", {"maxNumericValue": 3000}],
Expand Down
208 changes: 208 additions & 0 deletions scripts/deno/surface-lighthouse-results.ts
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;
jlieb10 marked this conversation as resolved.
Show resolved Hide resolved

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`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great

);

/** 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"]
): "✅" | "⚠️" | "❌" => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theres something interesting about a function that has an "emoji" return type, love it!

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);
}