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/utility.mts - .github/scripts/frontend-js-size.mts - .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 = ''; const visualizerMarker = ''; 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 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; } 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; } let body = `${jsSizeReport}\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, }); }