chore(release): remove package size reports

- Drop the afterAllArtifactBuild size reporting hook and the previous-release baseline downloads in CI
- Stop uploading, checksumming, and publishing package-size-*.json
- Keep the afterPack package content audit, now without size tracking
- Update RELEASING.md and the verification docs, and add a changelog fragment
This commit is contained in:
2026-09-23 21:50:19 -07:00
parent 285b19ccc3
commit 5096ca217b
11 changed files with 24 additions and 195 deletions
-36
View File
@@ -59,17 +59,6 @@ jobs:
bun install --frozen-lockfile
bun run build
- name: Download previous package size reports
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p .tmp/package-baseline
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
if [ -n "$previous" ]; then
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
fi
- name: Build AppImage
run: bun run build:appimage
env:
@@ -98,7 +87,6 @@ jobs:
release/*.AppImage
release/latest*.yml
release/*.blockmap
release/package-size-*.json
if-no-files-found: error
build-macos:
@@ -159,17 +147,6 @@ jobs:
bun install --frozen-lockfile
bun run build
- name: Download previous package size reports
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p .tmp/package-baseline
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
if [ -n "$previous" ]; then
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
fi
- name: Build signed + notarized macOS artifacts
run: bun run build:mac
env:
@@ -193,7 +170,6 @@ jobs:
release/*.zip
release/latest*.yml
release/*.blockmap
release/package-size-*.json
if-no-files-found: error
build-windows:
@@ -235,17 +211,6 @@ jobs:
bun install --frozen-lockfile
bun run build
- name: Download previous package size reports
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mkdir -p .tmp/package-baseline
previous=$(gh api "repos/$GITHUB_REPOSITORY/releases" --jq '[.[] | select(.draft == false and .tag_name != env.GITHUB_REF_NAME)] | sort_by(.published_at) | last | .tag_name // empty')
if [ -n "$previous" ]; then
gh release download "$previous" --pattern 'package-size-*.json' --dir .tmp/package-baseline || echo 'Previous release has no package size reports; size comparison will be skipped.'
fi
- name: Verify managed Windows launcher
run: bun test src/main/runtime/managed-launcher.test.ts
@@ -270,5 +235,4 @@ jobs:
release/*.zip
release/latest*.yml
release/*.blockmap
release/package-size-*.json
if-no-files-found: error
-2
View File
@@ -105,7 +105,6 @@ jobs:
run: |
shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
files+=(release/package-size-*.json)
if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation."
exit 1
@@ -152,7 +151,6 @@ jobs:
release/latest*.yml
release/*.blockmap
release/SHA256SUMS.txt
release/package-size-*.json
dist/launcher/subminer
dist/launcher/subminer.cmd
)
-2
View File
@@ -106,7 +106,6 @@ jobs:
run: |
shopt -s nullglob
files=(release/*.AppImage release/*.dmg release/*.exe release/*.zip release/*.tar.gz release/latest*.yml release/*.blockmap dist/launcher/subminer dist/launcher/subminer.cmd)
files+=(release/package-size-*.json)
if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation."
exit 1
@@ -171,7 +170,6 @@ jobs:
release/latest*.yml
release/*.blockmap
release/SHA256SUMS.txt
release/package-size-*.json
dist/launcher/subminer
dist/launcher/subminer.cmd
)
+4
View File
@@ -0,0 +1,4 @@
type: changed
area: release
- Removed package-size JSON reports from future releases and their CI size comparisons. Package-content validation remains enabled.
+4 -14
View File
@@ -11,7 +11,7 @@
`ANTHROPIC_API_KEY` works. Install from <https://claude.com/claude-code> if
you don't already have it.
## Package contents and size checks
## Package contents checks
Stable and prerelease workflows share `.github/workflows/package-release.yml`.
Both callers explicitly pass the five required macOS signing/notarization
@@ -19,11 +19,9 @@ secrets plus the optional `SUBMINER_TMDB_API_KEY` (the project TMDB key that
`scripts/prepare-build-assets.mjs` stages into `dist/bundled-integration-keys.json`;
artifacts built without it simply require users to set `tmdb.apiKey`).
`GITHUB_TOKEN` remains automatically available to the reusable workflow.
Each platform verifies its ASAR and external resources before signing, then
measures the signed app and installers before upload. Missing runtime assets,
foreign SQLite/Koffi binaries, duplicate UI fonts, demo media, source maps,
TypeScript files, and nested test or fixture directories
fail the build. Size measurements are informational and do not block releases.
Each platform verifies its ASAR and external resources before signing. Missing
runtime assets, foreign SQLite/Koffi binaries, duplicate UI fonts, demo media,
source maps, TypeScript files, and nested test or fixture directories fail the build.
Current targets are Linux x64, macOS arm64, and Windows x64.
The runtime allowlist includes `dist/`, `stats/dist/`, and
@@ -34,14 +32,6 @@ exclusion-only platform list as a separate include-all matcher. Windows keeps
only its target Koffi binary; other platforms omit Koffi. Desktop UIs share the
original M PLUS 1 TTF in `dist/fonts/`.
`release/package-size-<platform>-<arch>.json` reports unpacked bytes, largest
files inside and outside ASAR, native binaries, and compressed artifact sizes.
Framework symlinks are not counted twice. Reports are checksummed and published.
CI downloads the preceding release's reports for comparison; older releases
without reports skip comparison. Review the inventory and reason for growth
when comparing releases. An AppImage normally
runs compressed; its extracted size is a separate measurement.
The shared workflow runs `bun run test:package <resources-directory>` with the
pinned Electron runtime and temporary user data. On headless Linux, prefix it
with `xvfb-run -a`. This checks packaged SQLite, Windows FFI loading/polling,
+3 -3
View File
@@ -60,9 +60,9 @@ bun run docs:build
- Build/release scripts (`scripts/**`): `bun run test:scripts`
- Packaging: build the platform package, then run `bun run test:package <resources-directory>`.
On headless Linux: `xvfb-run -a bun run test:package release/linux-unpacked/resources`.
Content checks and informational size reporting run inside electron-builder hooks. See the
[release guide](../RELEASING.md#package-contents-and-size-checks) for size reports
and the installed-app verification checklist.
Content checks run inside the electron-builder afterPack hook. See the
[release guide](../RELEASING.md#package-contents-checks) for the
installed-app verification checklist.
- Coverage for the maintained source lane: `bun run test:coverage:src`
- Deep/local full gate: default handoff gate above
+1 -2
View File
@@ -269,8 +269,7 @@
"from": "CHANGELOG.md",
"to": "CHANGELOG.md"
}
],
"afterAllArtifactBuild": "scripts/package-audit.cjs"
]
},
"patchedDependencies": {
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch"
+8 -96
View File
@@ -4,8 +4,6 @@ const assert = require('node:assert/strict');
const asar = require('@electron/asar');
const { Arch } = require('builder-util');
const MIB = 1024 * 1024;
const currentReports = new Set();
const REQUIRED_APP_FILES = [
'package.json',
'LICENSE',
@@ -41,13 +39,13 @@ const REQUIRED_RESOURCES = [
'CHANGELOG.md',
];
// Do not follow framework symlinks or count ASAR unpacked entries twice.
// Skip symlinks when checking resource contents.
function listFiles(root, prefix = '') {
return fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap((entry) => {
const name = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isSymbolicLink()) return [];
if (entry.isDirectory()) return listFiles(root, name);
return [{ path: name, bytes: fs.statSync(path.join(root, name)).size }];
return [name];
});
}
@@ -58,7 +56,7 @@ function listAppFiles(archive) {
const native = entry.replace(/^[\\/]/, '');
const stat = asar.statFile(archive, native);
const name = native.replaceAll('\\', '/');
return 'size' in stat ? [{ path: name, bytes: stat.size }] : [];
return 'size' in stat ? [name] : [];
});
}
@@ -99,13 +97,13 @@ function verifyAppPath(name, platform, arch) {
function verifyContents(archive, resources, platform, arch) {
const entries = listAppFiles(archive);
const names = new Set(entries.map((entry) => entry.path));
const names = new Set(entries);
for (const name of REQUIRED_APP_FILES) assert(names.has(name), `Missing app file: ${name}`);
for (const name of REQUIRED_RESOURCES) {
assert(fs.statSync(path.join(resources, name)).size > 0, `Empty resource: ${name}`);
}
assert(listFiles(path.join(resources, 'yomitan-jlpt-vocab')).length > 0, 'Missing JLPT data');
for (const { path: name } of entries) verifyAppPath(name, platform, arch);
for (const name of entries) verifyAppPath(name, platform, arch);
const libsqlPlatform = {
linux: `linux-${arch}-gnu`,
darwin: `darwin-${arch}`,
@@ -129,7 +127,7 @@ function verifyContents(archive, resources, platform, arch) {
}
}
for (const name of listFiles(path.join(resources, 'assets'))) {
assert(!name.path.startsWith('minecard'), `Demo media shipped: ${name.path}`);
assert(!name.startsWith('minecard'), `Demo media shipped: ${name}`);
}
for (const ui of ['renderer', 'settings', 'syncui']) {
const css = asar.extractFile(archive, path.join('dist', ui, 'style.css')).toString();
@@ -147,92 +145,8 @@ async function auditPackage(context) {
? path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
: context.appOutDir;
const resources = path.join(appRoot, platform === 'darwin' ? 'Contents/Resources' : 'resources');
const appFiles = verifyContents(path.join(resources, 'app.asar'), resources, platform, arch);
const files = listFiles(appRoot);
const unpackedBytes = files.reduce((sum, entry) => sum + entry.bytes, 0);
const report = {
version: context.packager.appInfo.version,
platform,
arch,
unpackedBytes,
appDirectory: path.relative(context.outDir, appRoot),
largestFiles: [...files].sort((a, b) => b.bytes - a.bytes).slice(0, 25),
largestAppFiles: [...appFiles].sort((a, b) => b.bytes - a.bytes).slice(0, 25),
nativeBinaries: files.filter((entry) => /\.(node|dll|dylib)$|\.so(?:\.|$)/.test(entry.path)),
artifacts: [],
};
const output = path.join(context.outDir, `package-size-${key}.json`);
fs.mkdirSync(path.dirname(output), { recursive: true });
fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`);
currentReports.add(output);
console.log(
`Package contents verified: ${key}, ${(unpackedBytes / MIB).toFixed(2)} MiB unpacked`,
);
}
function artifactKind(name) {
if (name.endsWith('-mac.zip')) return 'mac.zip';
if (name.endsWith('-win.zip')) return 'win.zip';
const extension = path.extname(name).slice(1);
return ['AppImage', 'dmg', 'exe'].includes(extension) ? extension : undefined;
}
function compareSizes(report, previous) {
assert.equal(previous.platform, report.platform);
assert.equal(previous.arch, report.arch);
assert(Number.isFinite(previous.unpackedBytes), 'Invalid previous size report');
const previousArtifacts = Array.isArray(previous.artifacts) ? previous.artifacts : [];
return {
version: previous.version,
unpackedDeltaBytes: report.unpackedBytes - previous.unpackedBytes,
artifacts: report.artifacts.flatMap((artifact) => {
const old = previousArtifacts.find(
(entry) => entry && entry.kind === artifact.kind && Number.isFinite(entry.bytes),
);
return old ? [{ kind: artifact.kind, deltaBytes: artifact.bytes - old.bytes }] : [];
}),
};
}
// Runs after signing and installer creation, before release upload.
async function afterAllArtifactBuild(result) {
const reports = [];
for (const reportPath of currentReports) {
const filename = path.basename(reportPath);
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const key = `${report.platform}-${report.arch}`;
const files = listFiles(path.join(result.outDir, report.appDirectory));
report.unpackedBytes = files.reduce((sum, entry) => sum + entry.bytes, 0);
report.largestFiles = [...files].sort((a, b) => b.bytes - a.bytes).slice(0, 25);
report.artifacts = result.artifactPaths.flatMap((file) => {
const kind = artifactKind(file);
if (!kind) return [];
const bytes = fs.statSync(file).size;
return [{ name: path.basename(file), kind, bytes }];
});
const previousPath = path.join(result.outDir, '..', '.tmp', 'package-baseline', filename);
if (fs.existsSync(previousPath)) {
const previous = JSON.parse(fs.readFileSync(previousPath, 'utf8'));
report.comparison = compareSizes(report, previous);
}
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
const summary = [
`### Package size: ${key}`,
'',
`Unpacked: ${(report.unpackedBytes / MIB).toFixed(2)} MiB`,
...report.artifacts.map((entry) => `${entry.name}: ${(entry.bytes / MIB).toFixed(2)} MiB`),
report.comparison
? `Change from ${report.comparison.version}: ${(report.comparison.unpackedDeltaBytes / MIB).toFixed(2)} MiB unpacked`
: 'No previous size report available.',
'',
].join('\n');
console.log(summary);
if (process.env.GITHUB_STEP_SUMMARY)
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
reports.push(reportPath);
}
assert(reports.length > 0, 'No package size reports generated by afterPack');
return reports;
verifyContents(path.join(resources, 'app.asar'), resources, platform, arch);
console.log(`Package contents verified: ${key}`);
}
module.exports = {
@@ -241,6 +155,4 @@ module.exports = {
verifyAppPath,
listFiles,
listAppFiles,
compareSizes,
default: afterAllArtifactBuild,
};
+4 -30
View File
@@ -6,7 +6,7 @@ import test from 'node:test';
import { createPackageFromStreams } from '@electron/asar';
import { FileMatcher, getFileMatchers } from 'app-builder-lib/out/fileMatcher';
import config from '../package.json';
import { listAppFiles, listFiles, compareSizes, verifyAppPath } from './package-audit.cjs';
import { listAppFiles, listFiles, verifyAppPath } from './package-audit.cjs';
test('platform packaging preserves the runtime allowlist after builder normalizes global filters', () => {
const root = process.cwd();
@@ -125,7 +125,7 @@ test('content audit rejects development files beneath approved roots', () => {
}
});
test('archive inventory handles native files without counting them twice on disk', async () => {
test('content inventory includes packed and unpacked native files', async () => {
const root = mkdtempSync(path.join(tmpdir(), 'subminer-audit-'));
try {
const input = path.join(root, 'input');
@@ -147,35 +147,9 @@ test('archive inventory handles native files without counting them twice on disk
streamGenerator: () => createReadStream(path.join(input, name)),
})),
);
assert.deepEqual(listAppFiles(archive), [
{ path: 'main.js', bytes: 5 },
{ path: 'native.node', bytes: 6 },
{ path: 'dist/ai/client.js', bytes: 6 },
]);
assert.equal(
listFiles(output).reduce((sum: number, entry: { bytes: number }) => sum + entry.bytes, 0),
statSync(archive).size + 6,
);
assert.deepEqual(listAppFiles(archive), ['main.js', 'native.node', 'dist/ai/client.js']);
assert.deepEqual(listFiles(output).sort(), ['app.asar', 'app.asar.unpacked/native.node']);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('size comparison tolerates older reports without artifact measurements', () => {
const previous = { version: '0.19.6', platform: 'linux', arch: 'x64', unpackedBytes: 100 };
const current = { ...previous, unpackedBytes: 80, artifacts: [{ kind: 'AppImage', bytes: 40 }] };
assert.deepEqual(compareSizes(current, previous), {
version: '0.19.6',
unpackedDeltaBytes: -20,
artifacts: [],
});
assert.deepEqual(
compareSizes(current, { ...previous, artifacts: [null, { kind: 'AppImage', bytes: 50 }] })
.artifacts,
[{ kind: 'AppImage', deltaBytes: -10 }],
);
assert.throws(
() => compareSizes(current, { ...previous, unpackedBytes: 'unknown' }),
/Invalid previous size report/,
);
});
-9
View File
@@ -3,7 +3,6 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
executableRunLines,
jobSteps,
readWorkflow,
stepRunsCommand,
@@ -109,20 +108,12 @@ test('prerelease workflow builds and uploads all release platforms', () => {
...paths,
'release/latest*.yml',
'release/*.blockmap',
'release/package-size-*.json',
]);
const download = jobSteps(parsedPrereleaseWorkflow, 'release').find(
(step) => step.uses === 'actions/download-artifact@v4' && step.with?.name === name,
);
assert.equal(download?.with?.path, 'release');
}
const steps = jobSteps(parsedPrereleaseWorkflow, 'release');
const checksum = steps.find((step) => step.name === 'Generate checksums');
const publish = steps.find((step) => step.name === 'Publish Prerelease');
assert.ok(checksum);
assert.ok(publish);
assert.ok(executableRunLines(checksum).includes('files+=(release/package-size-*.json)'));
assert.ok(executableRunLines(publish).includes('release/package-size-*.json'));
});
test('release callers pass only the declared packaging secrets', () => {
-1
View File
@@ -289,7 +289,6 @@ test('stable and prerelease builds use the same packaging gate', () => {
for (const workflow of [releaseWorkflow, prerelease]) {
assert.match(workflow, /uses: \.\/\.github\/workflows\/package-release\.yml/);
assert.match(workflow, /needs: \[package\]/);
assert.match(workflow, /release\/package-size-\*\.json/);
}
assert.deepEqual(
templateExpressionsInRunBodies(