/**
* 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()),
});
})();