Skip to content

Commit

Permalink
feat(lighthouse): surface scores on every PR
Browse files Browse the repository at this point in the history
Adding new deno script that surface lighthouse results
on every PR. On pushes to main, update issue #4584:
#4584

- Add TS types for the Github API & Lighthouse
- Run workflow on pull_request "opened" & "synchronize"
- `includePassedAssertions` to get passed checks

Co-authored-by: Joshua <[email protected]>
Co-authored-by: Pete Faulconbridge <[email protected]>
  • Loading branch information
3 people committed May 3, 2022
1 parent e7219da commit 61d4726
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 23 deletions.
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;

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

0 comments on commit 61d4726

Please sign in to comment.