'use strict';
const {readFileSync, writeFileSync} = require('fs');
const {
MARKER_PREFIX,
extractReport,
parseReportHead,
} = require('./render-comment');
const CONTEXT_PATH = 'sizebot-context.json';
const COMMENT_PATH = 'sizebot-comment.md';
const COMMENT_AUTHOR = 'github-actions[bot]';
const MAX_LISTABLE_FILES = 3000;
const DEVTOOLS_PATH = 'packages/react-devtools';
async function findExistingComment(github, context, prNumber) {
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
for (const comment of comments) {
if (
comment.user.login === COMMENT_AUTHOR &&
comment.body.startsWith(MARKER_PREFIX)
) {
return comment;
}
}
return null;
}
async function findPullRequestNumber(github, context, workflowRun) {
if (
workflowRun.pull_requests != null &&
workflowRun.pull_requests.length > 0
) {
return workflowRun.pull_requests[0].number;
}
if (workflowRun.head_repository == null) {
return null;
}
const {data: pulls} = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${workflowRun.head_repository.owner.login}:${workflowRun.head_branch}`,
state: 'open',
per_page: 100,
});
if (pulls.length === 0) {
return null;
}
return pulls[0].number;
}
async function findPullRequest(github, context, workflowRun) {
const number = await findPullRequestNumber(github, context, workflowRun);
if (number === null) {
return null;
}
const {data} = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: number,
});
return data;
}
async function isDevToolsOnly(github, context, pullRequest) {
if (
!Number.isInteger(pullRequest.changed_files) ||
pullRequest.changed_files > MAX_LISTABLE_FILES
) {
return false;
}
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullRequest.number,
per_page: 100,
});
if (files.length === 0) {
return false;
}
return files.every(file => file.filename.includes(DEVTOOLS_PATH));
}
async function resolve({github, context, core}) {
const workflowRun = context.payload.workflow_run;
const action = context.payload.action;
const pullRequest = await findPullRequest(github, context, workflowRun);
if (pullRequest === null) {
core.info('No open pull request for this run. Nothing to comment on.');
core.setOutput('action', 'skip');
return;
}
const runRepo = workflowRun.head_repository?.full_name ?? null;
const pullRequestRepo = pullRequest.head.repo?.full_name ?? null;
if (runRepo === null || pullRequestRepo === null) {
core.info('Head repository is unavailable. Nothing to comment on.');
core.setOutput('action', 'skip');
return;
}
if (pullRequestRepo !== runRepo) {
core.setFailed(
`Pull request #${pullRequest.number} has head repository ` +
`${pullRequestRepo}, but the run came from ${runRepo}.`
);
return;
}
if (pullRequest.base.ref !== 'main') {
core.info(
`Pull request #${pullRequest.number} targets ${pullRequest.base.ref}, not main.`
);
core.setOutput('action', 'skip');
return;
}
const existing = await findExistingComment(
github,
context,
pullRequest.number
);
const existingReportHead =
existing === null ? null : parseReportHead(existing.body);
if (
existingReportHead !== null &&
existingReportHead === pullRequest.head.sha &&
workflowRun.head_sha !== pullRequest.head.sha
) {
core.info(
`Comment already reports on ${pullRequest.head.sha}; this run is for ` +
`${workflowRun.head_sha}. Leaving it alone.`
);
core.setOutput('action', 'skip');
return;
}
const devtoolsOnly =
action === 'completed'
? await isDevToolsOnly(github, context, pullRequest)
: false;
writeFileSync(
CONTEXT_PATH,
JSON.stringify(
{
action,
prNumber: pullRequest.number,
prHeadSha: pullRequest.head.sha,
runHeadSha: workflowRun.head_sha,
runUrl: workflowRun.html_url,
runStatus: workflowRun.status,
runConclusion: workflowRun.conclusion,
devtoolsOnly,
existingCommentId: existing === null ? null : existing.id,
existingReportHead,
existingReport: existing === null ? null : extractReport(existing.body),
commentRunUrl: `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
},
null,
2
) + '\n'
);
core.setOutput('action', 'continue');
core.setOutput('download_results', String(action === 'completed'));
}
async function post({github, context, core}) {
const sizebotContext = JSON.parse(readFileSync(CONTEXT_PATH, 'utf8'));
const body = readFileSync(COMMENT_PATH, 'utf8');
async function create() {
const {data} = await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: sizebotContext.prNumber,
body,
});
core.info(`Created ${data.html_url}`);
}
if (sizebotContext.existingCommentId === null) {
await create();
return;
}
try {
const {data} = await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: sizebotContext.existingCommentId,
body,
});
core.info(`Updated ${data.html_url}`);
} catch (error) {
if (error.status !== 404) {
throw error;
}
core.info('Existing comment is gone, posting a new one.');
await create();
}
}
module.exports = {post, resolve};