misskey/.github/workflows/frontend-bundle-report-comment.yml

327 lines
14 KiB
YAML

name: frontend-bundle-report-comment
on:
workflow_run:
workflows:
- frontend-bundle-report
types:
- completed
pull_request_target:
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- packages/frontend/**
- packages/frontend-shared/**
- packages/frontend-builder/**
- packages/i18n/**
- packages/icons-subsetter/**
- packages/misskey-js/**
- packages/misskey-reversi/**
- packages/misskey-bubble-game/**
- package.json
- pnpm-lock.yaml
- pnpm-workspace.yaml
- .node-version
- .github/scripts/frontend-js-size.mjs
- .github/scripts/frontend-bundle-visualizer-report.mjs
- .github/workflows/frontend-bundle-report.yml
- .github/workflows/frontend-bundle-report-comment.yml
permissions:
actions: read
contents: read
issues: write
pull-requests: write
jobs:
comment:
name: Comment frontend bundle report
if: github.event_name == 'pull_request_target' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest
concurrency:
group: frontend-bundle-report-comment-${{ github.event.pull_request.number || github.event.workflow_run.id }}
cancel-in-progress: true
steps:
- name: Find bundle report run
if: github.event_name == 'pull_request_target'
id: find-report-run
uses: actions/github-script@v9
with:
script: |
const workflow_id = 'frontend-bundle-report.yml';
const artifactName = 'frontend-bundle-report';
const headSha = context.payload.pull_request.head.sha;
const prNumber = context.payload.pull_request.number;
const pollIntervalMs = 30_000;
const timeoutMs = 90 * 60_000;
const startedAt = Date.now();
const { owner, repo } = context.repo;
async function listReportWorkflowRuns() {
const runsForHead = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
event: 'pull_request',
head_sha: headSha,
per_page: 100,
});
if (runsForHead.length > 0) {
return runsForHead;
}
const recentRuns = await github.paginate(github.rest.actions.listWorkflowRuns, {
owner,
repo,
workflow_id,
event: 'pull_request',
per_page: 100,
});
return recentRuns.filter((run) =>
run.pull_requests?.some((pullRequest) => pullRequest.number === prNumber));
}
async function findReportRun() {
const runs = (await listReportWorkflowRuns())
.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
for (const run of runs) {
if (run.status !== 'completed') continue;
if (run.conclusion !== 'success') {
core.warning(`Frontend bundle report run ${run.id} completed with conclusion: ${run.conclusion}`);
return { done: true, run: null };
}
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner,
repo,
run_id: run.id,
per_page: 100,
});
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
if (report) return { done: true, run };
core.info(`Frontend bundle report run ${run.id} did not produce ${artifactName}.`);
return { done: true, run: null };
}
return { done: false, run: null };
}
while (Date.now() - startedAt < timeoutMs) {
const { done, run } = await findReportRun();
if (run) {
core.info(`Found frontend bundle report on workflow run ${run.id}.`);
core.setOutput('run-id', String(run.id));
return;
}
if (done) {
return;
}
core.info('Waiting for frontend bundle report artifact...');
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}
core.warning(`Timed out waiting for ${artifactName} from ${workflow_id} for ${headSha}.`);
- name: Find bundle report artifact
if: github.event_name == 'workflow_run'
id: find-report-artifact
uses: actions/github-script@v9
with:
script: |
const artifactName = 'frontend-bundle-report';
const { owner, repo } = context.repo;
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner,
repo,
run_id: context.payload.workflow_run.id,
per_page: 100,
});
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
if (report) {
core.setOutput('exists', 'true');
} else {
core.info(`Workflow run ${context.payload.workflow_run.id} did not produce ${artifactName}.`);
core.setOutput('exists', 'false');
}
- name: Download bundle report from workflow_run
if: github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true'
uses: actions/download-artifact@v8
with:
name: frontend-bundle-report
path: ${{ runner.temp }}/frontend-bundle-report
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ github.event.workflow_run.id }}
- name: Download bundle report from pull_request_target
if: github.event_name == 'pull_request_target' && steps.find-report-run.outputs.run-id != ''
uses: actions/download-artifact@v8
with:
name: frontend-bundle-report
path: ${{ runner.temp }}/frontend-bundle-report
github-token: ${{ github.token }}
repository: ${{ github.repository }}
run-id: ${{ steps.find-report-run.outputs.run-id }}
- name: Comment on pull request
if: (github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true') || steps.find-report-run.outputs.run-id != ''
uses: actions/github-script@v9
with:
github-token: ${{ secrets.FRONTEND_BUNDLE_REPORT_COMMENT_TOKEN || secrets.FRONTEND_JS_SIZE_COMMENT_TOKEN || secrets.FRONTEND_BUNDLE_VISUALIZER_COMMENT_TOKEN || github.token }}
script: |
const fs = require('node:fs');
const path = require('node:path');
const jsSizeMarker = '<!-- misskey-frontend-js-size -->';
const visualizerMarker = '<!-- misskey-frontend-bundle-visualizer -->';
const reportMarkers = [jsSizeMarker, visualizerMarker];
const reportDir = path.join(process.env.RUNNER_TEMP, 'frontend-bundle-report');
const jsSizeReportPath = path.join(reportDir, 'frontend-js-size-report.md');
const visualizerReportPath = path.join(reportDir, 'frontend-bundle-visualizer-report.md');
const prNumberPath = path.join(reportDir, 'pr-number.txt');
const headShaPath = path.join(reportDir, 'head-sha.txt');
const workflowRun = context.payload.workflow_run;
const pullRequest = context.payload.pull_request;
const eventHeadSha = workflowRun?.head_sha ?? pullRequest?.head?.sha ?? null;
const { owner, repo } = context.repo;
if (!fs.existsSync(jsSizeReportPath)) {
core.setFailed('The frontend bundle report artifact does not contain frontend-js-size-report.md.');
return;
}
if (!fs.existsSync(visualizerReportPath)) {
core.setFailed('The frontend bundle report artifact does not contain frontend-bundle-visualizer-report.md.');
return;
}
const artifactHeadSha = fs.existsSync(headShaPath)
? fs.readFileSync(headShaPath, 'utf8').trim()
: null;
if (eventHeadSha != null && artifactHeadSha != null && artifactHeadSha !== eventHeadSha) {
core.info(`The artifact head SHA (${artifactHeadSha}) differs from the event head SHA (${eventHeadSha}). Using artifact metadata for PR validation.`);
}
const reportHeadSha = artifactHeadSha ?? eventHeadSha;
const artifactPrNumber = fs.existsSync(prNumberPath)
? Number(fs.readFileSync(prNumberPath, 'utf8').trim())
: null;
let issue_number = null;
if (pullRequest != null) {
issue_number = pullRequest.number;
if (Number.isInteger(artifactPrNumber) && artifactPrNumber !== issue_number) {
core.setFailed(`The artifact pull request number (${artifactPrNumber}) does not match the event pull request number (${issue_number}).`);
return;
}
} else if (workflowRun != null) {
const associatedPullRequests = new Map();
for (const pullRequest of workflowRun.pull_requests ?? []) {
if (Number.isInteger(pullRequest.number)) {
associatedPullRequests.set(pullRequest.number, pullRequest);
}
}
if (reportHeadSha != null) {
const pullRequestsForCommit = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {
owner,
repo,
commit_sha: reportHeadSha,
per_page: 100,
});
for (const pullRequest of pullRequestsForCommit) {
associatedPullRequests.set(pullRequest.number, pullRequest);
}
}
if (Number.isInteger(artifactPrNumber) && associatedPullRequests.has(artifactPrNumber)) {
issue_number = artifactPrNumber;
} else if (Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 0) {
issue_number = artifactPrNumber;
} else if (!Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 1) {
issue_number = [...associatedPullRequests.keys()][0];
} else if (Number.isInteger(artifactPrNumber)) {
core.setFailed(`The artifact pull request number (${artifactPrNumber}) is not associated with ${reportHeadSha}.`);
return;
} else {
core.setFailed(`Could not determine the pull request associated with ${reportHeadSha}.`);
return;
}
} else {
core.setFailed('Could not determine the pull request event for this report.');
return;
}
const currentPullRequest = await github.rest.pulls.get({
owner,
repo,
pull_number: issue_number,
});
const currentHeadSha = currentPullRequest.data.head?.sha;
if (reportHeadSha != null && currentHeadSha != null && reportHeadSha !== currentHeadSha) {
core.info(`The report head SHA (${reportHeadSha}) is not the current pull request head SHA (${currentHeadSha}). Skipping stale frontend bundle report.`);
return;
}
const jsSizeReport = fs.readFileSync(jsSizeReportPath, 'utf8').trim();
if (!jsSizeReport.includes(jsSizeMarker)) {
core.setFailed('The frontend JS size report is missing the expected marker.');
return;
}
const visualizerReport = fs.readFileSync(visualizerReportPath, 'utf8').trim();
let body = [
jsSizeReport,
visualizerReport,
].join('\n\n') + '\n';
const maxCommentLength = 65_000;
if (body.length > maxCommentLength) {
const reportLocation = workflowRun?.html_url != null
? `[workflow run](${workflowRun.html_url})`
: 'workflow artifact';
const footer = [
'',
'',
`_Report truncated because it exceeded ${maxCommentLength.toLocaleString('en-US')} characters. See the ${reportLocation} for the full report._`,
].join('\n');
body = `${body.slice(0, maxCommentLength - footer.length)}${footer}`;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const previousReports = comments.filter((comment) =>
comment.user?.type === 'Bot' && reportMarkers.some((reportMarker) => comment.body?.includes(reportMarker)));
if (previousReports.length > 0) {
const [previous, ...duplicates] = previousReports;
await github.rest.issues.updateComment({
owner,
repo,
comment_id: previous.id,
body,
});
for (const duplicate of duplicates) {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: duplicate.id,
});
}
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});
}