Skip to content

Commit

Permalink
Merge pull request #4773 from guardian/jtl-surface-lighthouse
Browse files Browse the repository at this point in the history
feat: Surface Lighthouse results
  • Loading branch information
mxdvl authored May 3, 2022
2 parents e7219da + bd58254 commit 6c6e83f
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 6c6e83f

Please sign in to comment.