From 2874a6d5ba903fb752ca7c1dad98c81964aff95e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:44:55 +0900 Subject: [PATCH] refacotr(gh): refactor --- ...frontend-bundle-diagnostics.render-md.mts} | 2 +- .github/scripts/frontend-js-size.test.mts | 252 -------------- ...> frontend-bundle-diagnostics.inspect.yml} | 16 +- .../frontend-bundle-diagnostics.report.yml | 179 ++++++++++ .../frontend-bundle-report-comment.yml | 318 ------------------ .github/workflows/lint.yml | 16 - 6 files changed, 188 insertions(+), 595 deletions(-) rename .github/scripts/{frontend-js-size.mts => frontend-bundle-diagnostics.render-md.mts} (99%) delete mode 100644 .github/scripts/frontend-js-size.test.mts rename .github/workflows/{frontend-bundle-report.yml => frontend-bundle-diagnostics.inspect.yml} (89%) create mode 100644 .github/workflows/frontend-bundle-diagnostics.report.yml delete mode 100644 .github/workflows/frontend-bundle-report-comment.yml diff --git a/.github/scripts/frontend-js-size.mts b/.github/scripts/frontend-bundle-diagnostics.render-md.mts similarity index 99% rename from .github/scripts/frontend-js-size.mts rename to .github/scripts/frontend-bundle-diagnostics.render-md.mts index db134bab79..7ec0cfc61d 100644 --- a/.github/scripts/frontend-js-size.mts +++ b/.github/scripts/frontend-bundle-diagnostics.render-md.mts @@ -7,7 +7,7 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import * as util from './utility.mts'; -const marker = ''; +const marker = ''; const locale = process.env.FRONTEND_JS_SIZE_LOCALE ?? 'ja-JP'; diff --git a/.github/scripts/frontend-js-size.test.mts b/.github/scripts/frontend-js-size.test.mts deleted file mode 100644 index d3b3db9af6..0000000000 --- a/.github/scripts/frontend-js-size.test.mts +++ /dev/null @@ -1,252 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import assert from 'node:assert/strict'; -import { execFile } from 'node:child_process'; -import { promises as fs } from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import test, { type TestContext } from 'node:test'; -import * as util from './utility.mts'; - -type Manifest = Record; - -const repoDir = path.resolve(import.meta.dirname, '../..'); -const reportScript = path.join(repoDir, '.github/scripts/frontend-js-size.mts'); - -async function writeBuild(repo: string, manifest: Manifest, sizes: Record) { - const outDir = path.join(repo, 'built/_frontend_vite_/'); - await fs.mkdir(path.join(outDir, 'ja-JP'), { recursive: true }); - await fs.writeFile(path.join(outDir, 'manifest.json'), JSON.stringify(manifest)); - for (const [file, size] of Object.entries(sizes)) { - const localizedFile = file.replace(/^scripts\//, ''); - const outputFile = path.join(outDir, 'ja-JP', localizedFile); - await fs.mkdir(path.dirname(outputFile), { recursive: true }); - await fs.writeFile(outputFile, Buffer.alloc(size)); - } -} - -async function runReport(t: TestContext, before: { manifest: Manifest; sizes: Record }, after: { manifest: Manifest; sizes: Record }, expectFailure = false) { - const root = await fs.mkdtemp(path.join(os.tmpdir(), 'frontend-js-size-')); - t.after(() => fs.rm(root, { recursive: true, force: true })); - const beforeDir = path.join(root, 'before'); - const afterDir = path.join(root, 'after'); - const beforeStats = path.join(root, 'before-stats.json'); - const afterStats = path.join(root, 'after-stats.json'); - const reportFile = path.join(root, 'report.md'); - await writeBuild(beforeDir, before.manifest, before.sizes); - await writeBuild(afterDir, after.manifest, after.sizes); - await fs.writeFile(beforeStats, '{}'); - await fs.writeFile(afterStats, '{}'); - const args = [reportScript, beforeDir, afterDir, beforeStats, afterStats, reportFile]; - if (expectFailure) { - return new Promise((resolve, reject) => { - execFile(process.execPath, args, (error, _stdout, stderr) => { - if (error == null) { - reject(new Error('Expected frontend report script to fail')); - } else { - resolve(stderr); - } - }); - }); - } - await util.run(process.execPath, args); - return fs.readFile(reportFile, 'utf8'); -} - -function fixture(suffix: string, generatedName: string, sizes: { entry: number; generatedA: number; generatedB: number; vue: number; i18n: number }) { - const files = { - entry: `scripts/entry-${suffix}.js`, - generatedA: `scripts/generated-a-${suffix}.js`, - generatedB: `scripts/generated-b-${suffix}.js`, - vue: `scripts/vue-${suffix}.js`, - i18n: `scripts/i18n-${suffix}.js`, - }; - return { - manifest: { - 'src/_boot_.ts': { file: files.entry, src: 'src/_boot_.ts', name: 'entry', isEntry: true, imports: ['_generatedA', '_generatedB', '_vue', '_i18n'] }, - _generatedA: { file: files.generatedA, name: generatedName }, - _generatedB: { file: files.generatedB, name: generatedName }, - _vue: { file: files.vue, name: 'vue' }, - _i18n: { file: files.i18n, name: 'i18n' }, - }, - sizes: Object.fromEntries(Object.entries(files).map(([key, file]) => [file, sizes[key as keyof typeof sizes]])), - }; -} - -test('groups generated chunks while preserving full and startup totals', async t => { - const report = await runReport( - t, - fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }), - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 55, i18n: 50 }), - ); - - assert.match(report, /\| \(total\) \| 220 B \| 285 B \|/); - assert.equal(report.match(/\| \(other generated chunks\) \| 30 B \| 70 B \|/g)?.length, 2); - assert.doesNotMatch(report, /generated chunks are grouped/); - assert.doesNotMatch(report, /`(?:dist|esm)`<\/summary>/); - assert.match(report, /`src\/_boot_\.ts`<\/summary>/); - assert.match(report, /`vue`<\/summary>/); -}); - -test('groups small deltas at the bottom while preserving all change counts', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - before.manifest._removedSmall = { file: 'scripts/removed-small-before.js', src: 'src/removed-small.ts' }; - before.manifest['src/_boot_.ts'].imports?.push('_removedSmall'); - before.sizes['scripts/removed-small-before.js'] = 5; - - const after = fixture('after', 'esm', { entry: 106, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }); - after.manifest._addedSmall = { file: 'scripts/added-small-after.js', src: 'src/added-small.ts' }; - after.manifest['src/_boot_.ts'].imports?.push('_addedSmall'); - after.sizes['scripts/added-small-after.js'] = 5; - - const report = await runReport(t, before, after); - - assert.match(report, /Chunk size diff \(2 updated, 1 added, 1 removed\)<\/summary>/); - assert.match(report, /Startup chunk size \(2 updated, 1 added, 1 removed\)<\/summary>/); - assert.equal(report.match(/`src\/_boot_\.ts`<\/summary>/g)?.length, 2); - assert.doesNotMatch(report, /`(?:vue|i18n|src\/added-small\.ts|src\/removed-small\.ts)`<\/summary>/); - assert.match(report, /\| \(total\) \| 225 B \| 276 B \|[^\n]*\n\| \| \| \| \| \|\n\|
`src\/_boot_\.ts`<\/summary>[^\n]*\n\| \(other generated chunks\) \| 30 B \| 70 B \|[^\n]*\n\| \(other\) \| 45 B \| 50 B \|/); - assert.match(report, /\| \(total\) \| 225 B \| 276 B \|[^\n]*\n\| \| \| \| \| \|\n\|
`src\/_boot_\.ts`<\/summary>[^\n]*\n\| \(other generated chunks\) \| 30 B \| 70 B \|[^\n]*\n\| \(other\) \| 95 B \| 100 B \|/); -}); - -test('fails instead of overwriting duplicate stable chunk keys', async t => { - const duplicateVue = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - duplicateVue.manifest._vueDuplicate = { file: 'scripts/vue-duplicate-before.js', name: 'vue' }; - duplicateVue.sizes['scripts/vue-duplicate-before.js'] = 60; - - const diagnostic = await runReport( - t, - duplicateVue, - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }), - true, - ); - assert.match(diagnostic, /Duplicate stable chunk key "named:vue".*vue-before\.js.*vue-duplicate-before\.js/); -}); - -test('shows both filenames for an updated stable chunk', async t => { - const report = await runReport( - t, - fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }), - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 55, i18n: 50 }), - ); - - assert.match(report, /`ja-JP\/entry-before\.js → ja-JP\/entry-after\.js`/); - assert.match(report, /`ja-JP\/vue-before\.js → ja-JP\/vue-after\.js`/); -}); - -test('fails descriptively when the startup entry is missing', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - delete before.manifest['src/_boot_.ts']; - delete before.sizes['scripts/entry-before.js']; - - const diagnostic = await runReport( - t, - before, - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }), - true, - ); - assert.match(diagnostic, /Unable to find frontend startup entry in Vite manifest/); -}); - -test('fails descriptively when a static import is missing from the manifest', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - before.manifest['src/_boot_.ts'].imports = ['_missing']; - - const diagnostic = await runReport( - t, - before, - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }), - true, - ); - assert.match(diagnostic, /Startup manifest key "_missing".*is missing/); -}); - -test('fails descriptively when a static import has no output file', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - before.manifest['src/_boot_.ts'].imports = ['_malformed']; - before.manifest._malformed = { name: 'malformed' }; - - const diagnostic = await runReport( - t, - before, - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }), - true, - ); - assert.match(diagnostic, /Startup manifest key "_malformed".*has no output file/); -}); - -test('fails descriptively when a static import resolves to a non-JavaScript output', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - before.manifest['src/_boot_.ts'].imports = ['_malformed']; - before.manifest._malformed = { file: 'assets/malformed.css', name: 'malformed' }; - - const diagnostic = await runReport( - t, - before, - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }), - true, - ); - assert.match(diagnostic, /Startup manifest key "_malformed".*non-JavaScript output "assets\/malformed\.css"/); -}); - -test('keeps source-backed additions and removals as individual rows', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - before.manifest._removed = { file: 'scripts/removed-before.js', src: 'src/removed.ts' }; - before.sizes['scripts/removed-before.js'] = 12; - const after = fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }); - after.manifest._added = { file: 'scripts/added-after.js', src: 'src/added.ts' }; - after.sizes['scripts/added-after.js'] = 13; - - const report = await runReport(t, before, after); - - assert.match(report, /`src\/added\.ts`<\/summary> `ja-JP\/added-after\.js` <\/details> \| 0 B \| 13 B \|/); - assert.match(report, /`src\/removed\.ts`<\/summary> `ja-JP\/removed-before\.js` <\/details> \| 12 B \| 0 B \|/); -}); - -test('counts an unmanifested localized JavaScript file in totals and the generated aggregate', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - before.sizes['scripts/unmanifested-before.js'] = 15; - - const report = await runReport( - t, - before, - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }), - ); - - assert.match(report, /\| \(total\) \| 235 B \| 275 B \|/); - assert.match(report, /\| \(other generated chunks\) \| 45 B \| 70 B \|/); -}); - -test('counts duplicate manifest entries for one physical output only once', async t => { - const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }); - before.manifest._generatedAlias = { file: 'scripts/generated-a-before.js', name: 'dist' }; - - const report = await runReport( - t, - before, - fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }), - ); - - assert.match(report, /\| \(total\) \| 220 B \| 275 B \|/); -}); - -test('does not count generated-aggregate-only changes as individual chunk changes', async t => { - const report = await runReport( - t, - fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }), - fixture('after', 'esm', { entry: 100, generatedA: 30, generatedB: 40, vue: 40, i18n: 50 }), - ); - - assert.match(report, /Chunk size diff \(0 updated, 0 added, 0 removed\)<\/summary>/); - assert.match(report, /Startup chunk size \(0 updated, 0 added, 0 removed\)<\/summary>/); - assert.equal(report.match(/\| \(other generated chunks\) \| 30 B \| 70 B \|/g)?.length, 2); -}); diff --git a/.github/workflows/frontend-bundle-report.yml b/.github/workflows/frontend-bundle-diagnostics.inspect.yml similarity index 89% rename from .github/workflows/frontend-bundle-report.yml rename to .github/workflows/frontend-bundle-diagnostics.inspect.yml index b5af3bb0e1..31faaed79b 100644 --- a/.github/workflows/frontend-bundle-report.yml +++ b/.github/workflows/frontend-bundle-diagnostics.inspect.yml @@ -1,4 +1,4 @@ -name: frontend-bundle-report +name: Frontend bundle diagnostics (inspect) on: pull_request: @@ -21,16 +21,16 @@ on: - 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 + - .github/scripts/frontend-bundle-diagnostics.render-md.mts + - .github/workflows/frontend-bundle-diagnostics.inspect.yml + - .github/workflows/frontend-bundle-diagnostics.report.yml permissions: contents: read pull-requests: read concurrency: - group: frontend-bundle-report-${{ github.event.pull_request.number }} + group: frontend-bundle-diagnostics-inspect-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: @@ -145,7 +145,7 @@ jobs: FRONTEND_BUNDLE_REPORT_ARTIFACT_URL: ${{ steps.upload-bundle-visualizer.outputs.artifact-url }} run: | REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report" - node after/.github/scripts/frontend-js-size.mts before after "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-js-size-report.md" + node after/.github/scripts/frontend-bundle-diagnostics.render-md.mts before after "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-bundle-diagnostics-report.md" printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt" printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt" printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt" @@ -157,8 +157,8 @@ jobs: REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report" test -s "$REPORT_DIR/before-stats.json" test -s "$REPORT_DIR/after-stats.json" - test -s "$REPORT_DIR/frontend-js-size-report.md" - cat "$REPORT_DIR/frontend-js-size-report.md" >> "$GITHUB_STEP_SUMMARY" + test -s "$REPORT_DIR/frontend-bundle-diagnostics-report.md" + cat "$REPORT_DIR/frontend-bundle-diagnostics-report.md" >> "$GITHUB_STEP_SUMMARY" - name: Upload bundle report if: steps.check-base-visualizer.outputs.supported == 'true' diff --git a/.github/workflows/frontend-bundle-diagnostics.report.yml b/.github/workflows/frontend-bundle-diagnostics.report.yml new file mode 100644 index 0000000000..260217ee53 --- /dev/null +++ b/.github/workflows/frontend-bundle-diagnostics.report.yml @@ -0,0 +1,179 @@ +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-bundle-diagnostics.render-md.mts + - .github/workflows/frontend-bundle-diagnostics.inspect.yml + - .github/workflows/frontend-bundle-diagnostics.report.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-diagnostics-report-${{ 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 + uses: thollander/actions-comment-pull-request@v3 + with: + pr-number: ${{ steps.load-pr-number.outputs.pr-number }} + comment-tag: frontend_bundle_diagnostics + file-path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-diagnostics-report.md diff --git a/.github/workflows/frontend-bundle-report-comment.yml b/.github/workflows/frontend-bundle-report-comment.yml deleted file mode 100644 index b93f8a320f..0000000000 --- a/.github/workflows/frontend-bundle-report-comment.yml +++ /dev/null @@ -1,318 +0,0 @@ -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, - }); - } diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 63f8684c33..63ae5c7397 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,9 +18,6 @@ on: - packages/misskey-reversi/** - packages/shared/eslint.config.js - scripts/check-dts*.mjs - - .github/scripts/frontend-js-size*.mts - - .github/scripts/memory-stability-util*.mts - - .github/scripts/utility.mts - .github/workflows/lint.yml - package.json pull_request: @@ -37,9 +34,6 @@ on: - packages/misskey-reversi/** - packages/shared/eslint.config.js - scripts/check-dts*.mjs - - .github/scripts/frontend-js-size*.mts - - .github/scripts/memory-stability-util*.mts - - .github/scripts/utility.mts - .github/workflows/lint.yml - package.json jobs: @@ -142,13 +136,3 @@ jobs: - run: pnpm i --frozen-lockfile - run: node --test scripts/check-dts.test.mjs - run: pnpm check-dts - - frontend-bundle-report-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6.0.3 - - uses: actions/setup-node@v6.4.0 - with: - node-version-file: '.node-version' - - run: node --test .github/scripts/frontend-js-size.test.mts - - run: node --test .github/scripts/memory-stability-util.test.mts