mirror of
https://github.com/react/react.git
synced 2026-09-28 21:25:11 +08:00
The build size comparison comment was posted by Danger, which authenticated with a personal access token hardcoded in `scripts/tasks/danger.js`. That token has since been revoked, so sizebot has been posting nothing at all (due to e.g. https://github.com/react/react/actions/runs/32181295467/job/95855395224?pr=37315). This change rebuilds it on the short-lived `GITHUB_TOKEN` that Actions mints per run and a new workflow only responsible for rendering untrusted JSON input as markdown in a PR comment. A straight token swap would not have worked. Fork pull requests did receive sizebot comments, but only because the token was in checked-out source: the sizebot job runs on the `pull_request` trigger, where a fork's `GITHUB_TOKEN` is read-only and cannot comment. The comment therefore moves to a new `workflow_run` workflow, `runtime_sizebot_comment.yml`, which runs in this repository with a writable token no matter where the pull request came from. It posts a placeholder when a build is requested and rewrites it in place when the build completes, fails, is cancelled, or is held for maintainer approval. The measurement stays on the unprivileged side of that boundary which are recorded as raw sizes into a `sizebot-results` artifact, and the new workflow downloads only that JSON and renders it from a default-branch checkout. The job holding `pull-requests: write` never unpacks a build produced by a fork, which matters because the existing base-build download justifies using an unverified artifact on the grounds that the job has restricted permissions. Thresholds, the critical bundle list, and the comment template all live on the trusted side, and the renderer validates every field it reads out of the artifact so that a crafted build path cannot inject markdown. The pull request number is resolved from the API rather than from the artifact, since a number read from fork-controlled data would let any contributor post a bot comment on an arbitrary pull request. Resolving that number needs a branch lookup rather than any of the obvious approaches. `workflow_run.pull_requests` is empty for fork runs, and neither `commits/{sha}/pulls` nor the search API indexes fork pull request head commits, so the workflow looks the pull request up by `owner:ref` instead. A comment is only ever left alone in one situation: when it already describes the pull request's current head and the event being handled belongs to an older commit. Everything else is written, and marked stale whenever the report does not describe the current head. That single rule covers both an old run finishing after a force push and a new build superseding a report already on display, and in the latter case the previous numbers stay visible instead of being blanked back to a placeholder. The results file carries a `version` field. Its writer is whatever `compare-sizes.js` a pull request branch happens to carry, while its reader is on the default branch, so the two can mismatch and the renderer needs to be able to say so instead of misrendering a table. Porting the table fixed a longstanding bug in `change()`. Testing `decimal < 0.0001` reported every size decrease as unchanged, which is why `signDisplay: 'exceptZero'` never had a negative number to render: a 709.04 kB to 708.68 kB drop printed as `=`. It now compares the magnitude. Co-authored-by: Claude Code (kimi-k3[1m]) <noreply@anthropic.com>
115 lines
3.8 KiB
JavaScript
115 lines
3.8 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/* eslint-disable no-for-of-loops/no-for-of-loops */
|
|
|
|
// Measures every build artifact in `build` against the base revision's artifacts
|
|
// in `base-build` and writes the raw numbers to `sizebot-results.json`.
|
|
//
|
|
// This runs in the `pull_request` half of CI, where the GitHub token is
|
|
// read-only, so it never talks to the API and never renders anything
|
|
// user-facing. `render-comment.js` turns this JSON into the pull request comment
|
|
// from a trusted checkout. See `.github/workflows/runtime_sizebot_comment.yml`
|
|
// for why the two halves are separate.
|
|
|
|
const {promisify} = require('util');
|
|
const glob = promisify(require('glob'));
|
|
const gzipSize = require('gzip-size');
|
|
const {readFileSync, statSync, writeFileSync} = require('fs');
|
|
|
|
// Bump on any incompatible change to the JSON below: added required fields,
|
|
// renamed or removed fields, or a changed meaning for an existing one. Purely
|
|
// additive optional fields do not need a bump. The reader lives on the default
|
|
// branch while the writer lives on the pull request branch, so the two can
|
|
// legitimately disagree and `render-comment.js` needs to be able to tell.
|
|
const RESULTS_VERSION = 1;
|
|
|
|
const RESULTS_PATH = 'sizebot-results.json';
|
|
const BASE_DIR = 'base-build';
|
|
const HEAD_DIR = 'build';
|
|
|
|
function measure(dir, artifactPath) {
|
|
const file = dir + '/' + artifactPath;
|
|
return {
|
|
size: statSync(file).size,
|
|
sizeGzip: gzipSize.fileSync(file),
|
|
};
|
|
}
|
|
|
|
function writeResults(results) {
|
|
writeFileSync(RESULTS_PATH, JSON.stringify(results, null, 2) + '\n');
|
|
}
|
|
|
|
(async function () {
|
|
let headSha;
|
|
let baseSha;
|
|
try {
|
|
headSha = String(readFileSync(HEAD_DIR + '/COMMIT_SHA')).trim();
|
|
baseSha = String(readFileSync(BASE_DIR + '/COMMIT_SHA')).trim();
|
|
} catch {
|
|
// Let the renderer explain this one. It is expected to happen whenever the
|
|
// build configuration changes upstream, which is not a CI failure.
|
|
writeResults({
|
|
version: RESULTS_VERSION,
|
|
status: 'base-artifacts-unavailable',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// A missing size is recorded as null rather than 0, so the renderer can tell
|
|
// "this artifact does not exist on that side" apart from "this artifact is
|
|
// empty". It derives the new-file and deleted-file cases from those nulls.
|
|
const artifactsByPath = new Map();
|
|
|
|
const headArtifactPaths = await glob('**/*.js', {cwd: HEAD_DIR});
|
|
for (const artifactPath of headArtifactPaths) {
|
|
let base;
|
|
try {
|
|
base = measure(BASE_DIR, artifactPath);
|
|
} catch {
|
|
// There's no matching base artifact. This is a new file.
|
|
base = null;
|
|
}
|
|
const head = measure(HEAD_DIR, artifactPath);
|
|
artifactsByPath.set(artifactPath, {
|
|
path: artifactPath,
|
|
baseSize: base === null ? null : base.size,
|
|
baseSizeGzip: base === null ? null : base.sizeGzip,
|
|
headSize: head.size,
|
|
headSizeGzip: head.sizeGzip,
|
|
});
|
|
}
|
|
|
|
// Check for base artifacts that were deleted in the head.
|
|
const baseArtifactPaths = await glob('**/*.js', {cwd: BASE_DIR});
|
|
for (const artifactPath of baseArtifactPaths) {
|
|
if (!artifactsByPath.has(artifactPath)) {
|
|
const base = measure(BASE_DIR, artifactPath);
|
|
artifactsByPath.set(artifactPath, {
|
|
path: artifactPath,
|
|
baseSize: base.size,
|
|
baseSizeGzip: base.sizeGzip,
|
|
headSize: null,
|
|
headSizeGzip: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Every artifact is reported, with no threshold filtering. The thresholds and
|
|
// the critical bundle list belong to the renderer, so that a pull request
|
|
// cannot quietly widen them to hide a regression.
|
|
writeResults({
|
|
version: RESULTS_VERSION,
|
|
status: 'ok',
|
|
baseSha,
|
|
headSha,
|
|
artifacts: Array.from(artifactsByPath.values()),
|
|
});
|
|
})();
|