mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 06:14:50 +02:00
refacotr(gh): refactor
This commit is contained in:
@@ -7,7 +7,7 @@ import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as util from './utility.mts';
|
||||
|
||||
const marker = '<!-- misskey-frontend-js-size -->';
|
||||
const marker = '<!-- misskey-frontend-bundle-diagnostics -->';
|
||||
|
||||
const locale = process.env.FRONTEND_JS_SIZE_LOCALE ?? 'ja-JP';
|
||||
|
||||
252
.github/scripts/frontend-js-size.test.mts
vendored
252
.github/scripts/frontend-js-size.test.mts
vendored
@@ -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<string, {
|
||||
file?: string;
|
||||
src?: string;
|
||||
name?: string;
|
||||
isEntry?: boolean;
|
||||
imports?: string[];
|
||||
}>;
|
||||
|
||||
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<string, number>) {
|
||||
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<string, number> }, after: { manifest: Manifest; sizes: Record<string, number> }, 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<string>((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, /<summary>`(?:dist|esm)`<\/summary>/);
|
||||
assert.match(report, /<summary>`src\/_boot_\.ts`<\/summary>/);
|
||||
assert.match(report, /<summary>`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, /<summary>Chunk size diff \(2 updated, 1 added, 1 removed\)<\/summary>/);
|
||||
assert.match(report, /<summary>Startup chunk size \(2 updated, 1 added, 1 removed\)<\/summary>/);
|
||||
assert.equal(report.match(/<summary>`src\/_boot_\.ts`<\/summary>/g)?.length, 2);
|
||||
assert.doesNotMatch(report, /<summary>`(?:vue|i18n|src\/added-small\.ts|src\/removed-small\.ts)`<\/summary>/);
|
||||
assert.match(report, /\| \(total\) \| 225 B \| 276 B \|[^\n]*\n\| \| \| \| \| \|\n\| <details><summary>`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\| <details><summary>`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, /<summary>`src\/added\.ts`<\/summary> `ja-JP\/added-after\.js` <\/details> \| 0 B \| 13 B \|/);
|
||||
assert.match(report, /<summary>`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, /<summary>Chunk size diff \(0 updated, 0 added, 0 removed\)<\/summary>/);
|
||||
assert.match(report, /<summary>Startup chunk size \(0 updated, 0 added, 0 removed\)<\/summary>/);
|
||||
assert.equal(report.match(/\| \(other generated chunks\) \| 30 B \| 70 B \|/g)?.length, 2);
|
||||
});
|
||||
@@ -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'
|
||||
179
.github/workflows/frontend-bundle-diagnostics.report.yml
vendored
Normal file
179
.github/workflows/frontend-bundle-diagnostics.report.yml
vendored
Normal file
@@ -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
|
||||
318
.github/workflows/frontend-bundle-report-comment.yml
vendored
318
.github/workflows/frontend-bundle-report-comment.yml
vendored
@@ -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 = '<!-- 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 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,
|
||||
});
|
||||
}
|
||||
16
.github/workflows/lint.yml
vendored
16
.github/workflows/lint.yml
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user