diff --git a/README.md b/README.md index 315fdbe..66e7c38 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,11 @@ Add the following step to a workflow which runs on a [pull_request](https://docs with: # Optional, specifies where to look for .next folder. Defaults to cwd. working-directory: ./apps/my-next-app + # Optional, configures commenting strategy around insignificant changes, defaults to `always`. + # Available options: + # always: Always comment on PRs, even if it's just to say there are no significant changes. + # skip-insignificant: Skip commenting on PRs if there are no signficant changes in page sizes. + comment-strategy: 'always' # Optional, defaults to `github.token`. github-token: ${{ github.token }} ``` diff --git a/action.yml b/action.yml index 8a00966..0f73598 100644 --- a/action.yml +++ b/action.yml @@ -8,6 +8,15 @@ inputs: description: 'Directory containing built files' required: false default: '' + comment-strategy: + description: > + The desired behavior for commenting on Pull Requests. + + Available options: + always: Always comment on PRs, even if it's just to say there are no significant changes. + skip-insignificant: Skip commenting on PRs if there are no signficant changes in page sizes. + required: false + default: 'always' github-token: description: 'The GitHub token used to authenticate with the GitHub API.' required: false diff --git a/dist/index.js b/dist/index.js index 1227bd9..9050ad7 100644 --- a/dist/index.js +++ b/dist/index.js @@ -127772,9 +127772,16 @@ function loadBuildManifest(workingDir) { const file = external_fs_default().readFileSync(external_path_default().join(process.cwd(), workingDir, '.next', 'build-manifest.json'), 'utf-8'); return JSON.parse(file); } -function getMarkdownTable(referenceBundleSizes = [], bundleSizes, name = 'Route') { +function getSingleColumnMarkdownTable({ bundleSizes, name, }) { + const rows = getPageChangeInfo([], bundleSizes); + return formatTableNoDiff(name, rows); +} +/** + * @returns a Markdown table of changes or null if there are no significant changes. + */ +function getComparisonMarkdownTable({ referenceBundleSizes, actualBundleSizes, name, }) { // Produce a Markdown table with each page, its size and difference to default branch - const rows = getPageChangeInfo(referenceBundleSizes, bundleSizes); + const rows = getPageChangeInfo(referenceBundleSizes, actualBundleSizes); if (rows.length === 0) { return `${name}: None found.`; } @@ -127786,12 +127793,7 @@ function getMarkdownTable(referenceBundleSizes = [], bundleSizes, name = 'Route' if (significant.length > 0) { return formatTable(name, significant); } - return `${name}: No significant changes found`; -} -function getBundleComparisonInfo({ referenceSha, referenceBundleSizes, actualBundleSizes, }) { - const info = `Compared against ${referenceSha}`; - const routesTable = getMarkdownTable(referenceBundleSizes, actualBundleSizes, 'Route'); - return { info, routesTable }; + return null; } function getPageChangeInfo(referenceBundleSizes, bundleSizes) { const addedAndChanged = bundleSizes.map(({ page, size }) => { @@ -127881,29 +127883,26 @@ function createContentByDelimiter({ title, content, delimiterIdentifier, }) { ;// CONCATENATED MODULE: ./src/comments.ts - +const FALLBACK_COMPARISON_TEXT = 'No significant changes found'; async function findCommentByTextMatch({ octokit, issueNumber, text, }) { const { data: comments } = await octokit.rest.issues.listComments(Object.assign(Object.assign({}, github.context.repo), { issue_number: issueNumber })); return comments.find((comment) => { var _a; return (_a = comment.body) === null || _a === void 0 ? void 0 : _a.includes(text); }); } -async function createOrReplaceComment({ octokit, issueNumber, appName, referenceSha, referenceBundleSizes, actualBundleSizes, }) { - const title = `### Bundle sizes [${appName}]`; - const { info, routesTable } = getBundleComparisonInfo({ - referenceSha, - referenceBundleSizes, - actualBundleSizes, - }); - const body = formatTextFragments(title, info, routesTable); +async function createOrReplaceComment({ octokit, issueNumber, title, shaInfo, routesTable, strategy, }) { const existingComment = await findCommentByTextMatch({ octokit, issueNumber, text: title, }); + const body = formatTextFragments(title, shaInfo, routesTable !== null && routesTable !== void 0 ? routesTable : FALLBACK_COMPARISON_TEXT); if (existingComment) { console.log(`Updating comment ${existingComment.id}`); const response = await octokit.rest.issues.updateComment(Object.assign(Object.assign({}, github.context.repo), { comment_id: existingComment.id, body })); console.log(`Done with status ${response.status}`); } + else if (!existingComment && !routesTable && strategy === 'skip-insignificant') { + console.log(`Skipping comment [${title}]: no significant changes`); + } else { console.log(`Creating comment on PR ${issueNumber}`); const response = await octokit.rest.issues.createComment(Object.assign(Object.assign({}, github.context.repo), { issue_number: issueNumber, body })); @@ -127998,16 +127997,27 @@ async function downloadArtifactAsJson(octokit, branch, artifactName, fileName) { } } -;// CONCATENATED MODULE: ./src/issue.ts +;// CONCATENATED MODULE: ./src/input-helper.ts + +function getInputs() { + const workingDirectory = core.getInput('working-directory'); + const commentStrategy = core.getInput('comment-strategy'); + const githubToken = core.getInput('github-token'); + const inputs = { + workingDirectory, + commentStrategy: commentStrategy === 'skip-insignificant' ? 'skip-insignificant' : 'always', + githubToken, + }; + return inputs; +} +;// CONCATENATED MODULE: ./src/issue.ts async function findIssueByTitleMatch({ octokit, title }) { const { data: issues } = await octokit.rest.issues.listForRepo(github.context.repo); return issues.find((issue) => issue.title === title); } -async function createOrReplaceIssue({ octokit, appName, actualBundleSizes, }) { - const title = `Bundle sizes [${appName}]`; - const routesTable = getMarkdownTable([], actualBundleSizes, 'Route'); +async function createOrReplaceIssue({ octokit, title, routesTable, }) { const existingIssue = await findIssueByTitleMatch({ octokit, title }); if (existingIssue) { console.log(`Updating issue ${existingIssue.number} with latest bundle sizes`); @@ -128048,43 +128058,51 @@ async function uploadJsonAsArtifact(artifactName, fileName, data) { + const ARTIFACT_NAME_PREFIX = 'next-bundle-analyzer__'; const FILE_NAME = 'bundle-sizes.json'; async function run() { var _a; try { - const workingDir = core.getInput('working-directory'); - const token = core.getInput('github-token'); - const appName = determineAppName(workingDir); + const inputs = getInputs(); + const appName = determineAppName(inputs.workingDirectory); const artifactName = `${ARTIFACT_NAME_PREFIX}${appName}`; - const octokit = (0,github.getOctokit)(token); + const octokit = (0,github.getOctokit)(inputs.githubToken); const { data: { default_branch }, } = await octokit.rest.repos.get(github.context.repo); const issueNumber = (_a = github.context.payload.pull_request) === null || _a === void 0 ? void 0 : _a.number; console.log(`> Downloading bundle sizes from ${default_branch}`); const referenceBundleSizes = (await downloadArtifactAsJson(octokit, default_branch, artifactName, FILE_NAME)) || { sha: 'none', data: [] }; console.log(referenceBundleSizes); console.log('> Calculating local bundle sizes'); - const bundleSizes = getStaticBundleSizes(workingDir); + const bundleSizes = getStaticBundleSizes(inputs.workingDirectory); console.log(bundleSizes); console.log('> Uploading local bundle sizes'); await uploadJsonAsArtifact(artifactName, FILE_NAME, bundleSizes); if (issueNumber) { - console.log('> Commenting on PR'); + const title = `### Bundle sizes [${appName}]`; + const shaInfo = `Compared against ${referenceBundleSizes.sha}`; + const routesTable = getComparisonMarkdownTable({ + referenceBundleSizes: referenceBundleSizes.data, + actualBundleSizes: bundleSizes, + name: 'Route', + }); createOrReplaceComment({ octokit, issueNumber, - appName, - referenceSha: referenceBundleSizes.sha, - referenceBundleSizes: referenceBundleSizes.data, - actualBundleSizes: bundleSizes, + title, + shaInfo, + routesTable, + strategy: inputs.commentStrategy, }); } else if (github.context.ref === `refs/heads/${default_branch}`) { console.log('> Creating/updating bundle size issue'); + const title = `Bundle sizes [${appName}]`; + const routesTable = getSingleColumnMarkdownTable({ bundleSizes, name: 'Route' }); createOrReplaceIssue({ octokit, - appName, - actualBundleSizes: bundleSizes, + title, + routesTable, }); } } diff --git a/src/bundle-size.ts b/src/bundle-size.ts index d9fe80d..27166f6 100644 --- a/src/bundle-size.ts +++ b/src/bundle-size.ts @@ -37,13 +37,31 @@ function loadBuildManifest(workingDir: string): BuildManifest { return JSON.parse(file); } -export function getMarkdownTable( - referenceBundleSizes: PageBundleSizes = [], - bundleSizes: PageBundleSizes, - name: string = 'Route', -): string { +export function getSingleColumnMarkdownTable({ + bundleSizes, + name, +}: { + bundleSizes: PageBundleSizes; + name: string; +}): string { + const rows = getPageChangeInfo([], bundleSizes); + return formatTableNoDiff(name, rows); +} + +/** + * @returns a Markdown table of changes or null if there are no significant changes. + */ +export function getComparisonMarkdownTable({ + referenceBundleSizes, + actualBundleSizes, + name, +}: { + referenceBundleSizes: PageBundleSizes; + actualBundleSizes: PageBundleSizes; + name: string; +}): string | null { // Produce a Markdown table with each page, its size and difference to default branch - const rows = getPageChangeInfo(referenceBundleSizes, bundleSizes); + const rows = getPageChangeInfo(referenceBundleSizes, actualBundleSizes); if (rows.length === 0) { return `${name}: None found.`; } @@ -57,21 +75,7 @@ export function getMarkdownTable( if (significant.length > 0) { return formatTable(name, significant); } - return `${name}: No significant changes found`; -} - -export function getBundleComparisonInfo({ - referenceSha, - referenceBundleSizes, - actualBundleSizes, -}: { - referenceSha: string; - referenceBundleSizes: PageBundleSizes; - actualBundleSizes: PageBundleSizes; -}) { - const info = `Compared against ${referenceSha}`; - const routesTable = getMarkdownTable(referenceBundleSizes, actualBundleSizes, 'Route'); - return { info, routesTable }; + return null; } type PageChangeInfo = { diff --git a/src/comments.ts b/src/comments.ts index 9c7dc82..78de4a5 100644 --- a/src/comments.ts +++ b/src/comments.ts @@ -1,8 +1,10 @@ import { context } from '@actions/github'; -import { PageBundleSizes, getBundleComparisonInfo } from './bundle-size'; import { formatTextFragments } from './text-format'; import type { Octokit } from './types'; +import { ActionInputs } from './input-helper'; + +const FALLBACK_COMPARISON_TEXT = 'No significant changes found'; async function findCommentByTextMatch({ octokit, @@ -23,31 +25,26 @@ async function findCommentByTextMatch({ export async function createOrReplaceComment({ octokit, issueNumber, - appName, - referenceSha, - referenceBundleSizes, - actualBundleSizes, + title, + shaInfo, + routesTable, + strategy, }: { octokit: Octokit; issueNumber: number; - appName: string; - referenceSha: string; - referenceBundleSizes: PageBundleSizes; - actualBundleSizes: PageBundleSizes; + title: string; + shaInfo: string; + routesTable: string | null; + strategy: ActionInputs['commentStrategy']; }): Promise { - const title = `### Bundle sizes [${appName}]`; - const { info, routesTable } = getBundleComparisonInfo({ - referenceSha, - referenceBundleSizes, - actualBundleSizes, - }); - const body = formatTextFragments(title, info, routesTable); const existingComment = await findCommentByTextMatch({ octokit, issueNumber, text: title, }); + const body = formatTextFragments(title, shaInfo, routesTable ?? FALLBACK_COMPARISON_TEXT); + if (existingComment) { console.log(`Updating comment ${existingComment.id}`); const response = await octokit.rest.issues.updateComment({ @@ -56,6 +53,8 @@ export async function createOrReplaceComment({ body, }); console.log(`Done with status ${response.status}`); + } else if (!existingComment && !routesTable && strategy === 'skip-insignificant') { + console.log(`Skipping comment [${title}]: no significant changes`); } else { console.log(`Creating comment on PR ${issueNumber}`); const response = await octokit.rest.issues.createComment({ diff --git a/src/index.ts b/src/index.ts index a136248..8e722ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,15 @@ import * as core from '@actions/core'; import { context, getOctokit } from '@actions/github'; -import { getStaticBundleSizes } from './bundle-size'; +import { + getComparisonMarkdownTable, + getSingleColumnMarkdownTable, + getStaticBundleSizes, +} from './bundle-size'; import { createOrReplaceComment } from './comments'; import { determineAppName } from './determine-app-name'; import { downloadArtifactAsJson } from './download-artifacts'; +import { getInputs } from './input-helper'; import { createOrReplaceIssue } from './issue'; import { uploadJsonAsArtifact } from './upload-artifacts'; @@ -13,12 +18,11 @@ const FILE_NAME = 'bundle-sizes.json'; async function run() { try { - const workingDir = core.getInput('working-directory'); - const token = core.getInput('github-token'); - const appName = determineAppName(workingDir); + const inputs = getInputs(); + const appName = determineAppName(inputs.workingDirectory); const artifactName = `${ARTIFACT_NAME_PREFIX}${appName}`; - const octokit = getOctokit(token); + const octokit = getOctokit(inputs.githubToken); const { data: { default_branch }, @@ -36,28 +40,36 @@ async function run() { console.log(referenceBundleSizes); console.log('> Calculating local bundle sizes'); - const bundleSizes = getStaticBundleSizes(workingDir); + const bundleSizes = getStaticBundleSizes(inputs.workingDirectory); console.log(bundleSizes); console.log('> Uploading local bundle sizes'); await uploadJsonAsArtifact(artifactName, FILE_NAME, bundleSizes); if (issueNumber) { - console.log('> Commenting on PR'); + const title = `### Bundle sizes [${appName}]`; + const shaInfo = `Compared against ${referenceBundleSizes.sha}`; + const routesTable = getComparisonMarkdownTable({ + referenceBundleSizes: referenceBundleSizes.data, + actualBundleSizes: bundleSizes, + name: 'Route', + }); createOrReplaceComment({ octokit, issueNumber, - appName, - referenceSha: referenceBundleSizes.sha, - referenceBundleSizes: referenceBundleSizes.data, - actualBundleSizes: bundleSizes, + title, + shaInfo, + routesTable, + strategy: inputs.commentStrategy, }); } else if (context.ref === `refs/heads/${default_branch}`) { console.log('> Creating/updating bundle size issue'); + const title = `Bundle sizes [${appName}]`; + const routesTable = getSingleColumnMarkdownTable({ bundleSizes, name: 'Route' }); createOrReplaceIssue({ octokit, - appName, - actualBundleSizes: bundleSizes, + title, + routesTable, }); } } catch (e) { diff --git a/src/input-helper.ts b/src/input-helper.ts new file mode 100644 index 0000000..45fe1d8 --- /dev/null +++ b/src/input-helper.ts @@ -0,0 +1,21 @@ +import * as core from '@actions/core'; + +export interface ActionInputs { + workingDirectory: string; + commentStrategy: 'always' | 'skip-insignificant'; + githubToken: string; +} + +export function getInputs(): ActionInputs { + const workingDirectory = core.getInput('working-directory'); + const commentStrategy = core.getInput('comment-strategy'); + const githubToken = core.getInput('github-token'); + + const inputs = { + workingDirectory, + commentStrategy: commentStrategy === 'skip-insignificant' ? 'skip-insignificant' : 'always', + githubToken, + } satisfies ActionInputs; + + return inputs; +} diff --git a/src/issue.ts b/src/issue.ts index c21d24c..764e3e0 100644 --- a/src/issue.ts +++ b/src/issue.ts @@ -1,6 +1,5 @@ import { context } from '@actions/github'; -import { PageBundleSizes, getMarkdownTable } from './bundle-size'; import type { Octokit } from './types'; async function findIssueByTitleMatch({ octokit, title }: { octokit: Octokit; title: string }) { @@ -10,15 +9,13 @@ async function findIssueByTitleMatch({ octokit, title }: { octokit: Octokit; tit export async function createOrReplaceIssue({ octokit, - appName, - actualBundleSizes, + title, + routesTable, }: { octokit: Octokit; - appName: string; - actualBundleSizes: PageBundleSizes; + title: string; + routesTable: string; }): Promise { - const title = `Bundle sizes [${appName}]`; - const routesTable = getMarkdownTable([], actualBundleSizes, 'Route'); const existingIssue = await findIssueByTitleMatch({ octokit, title }); if (existingIssue) {