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 install --frozen-lockfile
bun run build 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 - name: Build AppImage
run: bun run build:appimage run: bun run build:appimage
env: env:
@@ -98,7 +87,6 @@ jobs:
release/*.AppImage release/*.AppImage
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/package-size-*.json
if-no-files-found: error if-no-files-found: error
build-macos: build-macos:
@@ -159,17 +147,6 @@ jobs:
bun install --frozen-lockfile bun install --frozen-lockfile
bun run build 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 - name: Build signed + notarized macOS artifacts
run: bun run build:mac run: bun run build:mac
env: env:
@@ -193,7 +170,6 @@ jobs:
release/*.zip release/*.zip
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/package-size-*.json
if-no-files-found: error if-no-files-found: error
build-windows: build-windows:
@@ -235,17 +211,6 @@ jobs:
bun install --frozen-lockfile bun install --frozen-lockfile
bun run build 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 - name: Verify managed Windows launcher
run: bun test src/main/runtime/managed-launcher.test.ts run: bun test src/main/runtime/managed-launcher.test.ts
@@ -270,5 +235,4 @@ jobs:
release/*.zip release/*.zip
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/package-size-*.json
if-no-files-found: error if-no-files-found: error
-2
View File
@@ -105,7 +105,6 @@ jobs:
run: | run: |
shopt -s nullglob 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/*.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 if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation." echo "No release artifacts found for checksum generation."
exit 1 exit 1
@@ -152,7 +151,6 @@ jobs:
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/SHA256SUMS.txt release/SHA256SUMS.txt
release/package-size-*.json
dist/launcher/subminer dist/launcher/subminer
dist/launcher/subminer.cmd dist/launcher/subminer.cmd
) )
-2
View File
@@ -106,7 +106,6 @@ jobs:
run: | run: |
shopt -s nullglob 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/*.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 if [ "${#files[@]}" -eq 0 ]; then
echo "No release artifacts found for checksum generation." echo "No release artifacts found for checksum generation."
exit 1 exit 1
@@ -171,7 +170,6 @@ jobs:
release/latest*.yml release/latest*.yml
release/*.blockmap release/*.blockmap
release/SHA256SUMS.txt release/SHA256SUMS.txt
release/package-size-*.json
dist/launcher/subminer dist/launcher/subminer
dist/launcher/subminer.cmd 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 `ANTHROPIC_API_KEY` works. Install from <https://claude.com/claude-code> if
you don't already have it. you don't already have it.
## Package contents and size checks ## Package contents checks
Stable and prerelease workflows share `.github/workflows/package-release.yml`. Stable and prerelease workflows share `.github/workflows/package-release.yml`.
Both callers explicitly pass the five required macOS signing/notarization 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`; `scripts/prepare-build-assets.mjs` stages into `dist/bundled-integration-keys.json`;
artifacts built without it simply require users to set `tmdb.apiKey`). artifacts built without it simply require users to set `tmdb.apiKey`).
`GITHUB_TOKEN` remains automatically available to the reusable workflow. `GITHUB_TOKEN` remains automatically available to the reusable workflow.
Each platform verifies its ASAR and external resources before signing, then Each platform verifies its ASAR and external resources before signing. Missing
measures the signed app and installers before upload. Missing runtime assets, runtime assets, foreign SQLite/Koffi binaries, duplicate UI fonts, demo media,
foreign SQLite/Koffi binaries, duplicate UI fonts, demo media, source maps, source maps, TypeScript files, and nested test or fixture directories fail the build.
TypeScript files, and nested test or fixture directories
fail the build. Size measurements are informational and do not block releases.
Current targets are Linux x64, macOS arm64, and Windows x64. Current targets are Linux x64, macOS arm64, and Windows x64.
The runtime allowlist includes `dist/`, `stats/dist/`, and 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 only its target Koffi binary; other platforms omit Koffi. Desktop UIs share the
original M PLUS 1 TTF in `dist/fonts/`. 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 The shared workflow runs `bun run test:package <resources-directory>` with the
pinned Electron runtime and temporary user data. On headless Linux, prefix it pinned Electron runtime and temporary user data. On headless Linux, prefix it
with `xvfb-run -a`. This checks packaged SQLite, Windows FFI loading/polling, 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` - Build/release scripts (`scripts/**`): `bun run test:scripts`
- Packaging: build the platform package, then run `bun run test:package <resources-directory>`. - 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`. 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 Content checks run inside the electron-builder afterPack hook. See the
[release guide](../RELEASING.md#package-contents-and-size-checks) for size reports [release guide](../RELEASING.md#package-contents-checks) for the
and the installed-app verification checklist. installed-app verification checklist.
- Coverage for the maintained source lane: `bun run test:coverage:src` - Coverage for the maintained source lane: `bun run test:coverage:src`
- Deep/local full gate: default handoff gate above - Deep/local full gate: default handoff gate above
+1 -2
View File
@@ -269,8 +269,7 @@
"from": "CHANGELOG.md", "from": "CHANGELOG.md",
"to": "CHANGELOG.md" "to": "CHANGELOG.md"
} }
], ]
"afterAllArtifactBuild": "scripts/package-audit.cjs"
}, },
"patchedDependencies": { "patchedDependencies": {
"@discordjs/rest@2.6.1": "patches/@discordjs%2Frest@2.6.1.patch" "@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 asar = require('@electron/asar');
const { Arch } = require('builder-util'); const { Arch } = require('builder-util');
const MIB = 1024 * 1024;
const currentReports = new Set();
const REQUIRED_APP_FILES = [ const REQUIRED_APP_FILES = [
'package.json', 'package.json',
'LICENSE', 'LICENSE',
@@ -41,13 +39,13 @@ const REQUIRED_RESOURCES = [
'CHANGELOG.md', 'CHANGELOG.md',
]; ];
// Do not follow framework symlinks or count ASAR unpacked entries twice. // Skip symlinks when checking resource contents.
function listFiles(root, prefix = '') { function listFiles(root, prefix = '') {
return fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap((entry) => { return fs.readdirSync(path.join(root, prefix), { withFileTypes: true }).flatMap((entry) => {
const name = prefix ? `${prefix}/${entry.name}` : entry.name; const name = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isSymbolicLink()) return []; if (entry.isSymbolicLink()) return [];
if (entry.isDirectory()) return listFiles(root, name); 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 native = entry.replace(/^[\\/]/, '');
const stat = asar.statFile(archive, native); const stat = asar.statFile(archive, native);
const name = native.replaceAll('\\', '/'); 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) { function verifyContents(archive, resources, platform, arch) {
const entries = listAppFiles(archive); 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_APP_FILES) assert(names.has(name), `Missing app file: ${name}`);
for (const name of REQUIRED_RESOURCES) { for (const name of REQUIRED_RESOURCES) {
assert(fs.statSync(path.join(resources, name)).size > 0, `Empty resource: ${name}`); 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'); 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 = { const libsqlPlatform = {
linux: `linux-${arch}-gnu`, linux: `linux-${arch}-gnu`,
darwin: `darwin-${arch}`, darwin: `darwin-${arch}`,
@@ -129,7 +127,7 @@ function verifyContents(archive, resources, platform, arch) {
} }
} }
for (const name of listFiles(path.join(resources, 'assets'))) { 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']) { for (const ui of ['renderer', 'settings', 'syncui']) {
const css = asar.extractFile(archive, path.join('dist', ui, 'style.css')).toString(); 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`) ? path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
: context.appOutDir; : context.appOutDir;
const resources = path.join(appRoot, platform === 'darwin' ? 'Contents/Resources' : 'resources'); const resources = path.join(appRoot, platform === 'darwin' ? 'Contents/Resources' : 'resources');
const appFiles = verifyContents(path.join(resources, 'app.asar'), resources, platform, arch); verifyContents(path.join(resources, 'app.asar'), resources, platform, arch);
const files = listFiles(appRoot); console.log(`Package contents verified: ${key}`);
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;
} }
module.exports = { module.exports = {
@@ -241,6 +155,4 @@ module.exports = {
verifyAppPath, verifyAppPath,
listFiles, listFiles,
listAppFiles, listAppFiles,
compareSizes,
default: afterAllArtifactBuild,
}; };
+4 -30
View File
@@ -6,7 +6,7 @@ import test from 'node:test';
import { createPackageFromStreams } from '@electron/asar'; import { createPackageFromStreams } from '@electron/asar';
import { FileMatcher, getFileMatchers } from 'app-builder-lib/out/fileMatcher'; import { FileMatcher, getFileMatchers } from 'app-builder-lib/out/fileMatcher';
import config from '../package.json'; 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', () => { test('platform packaging preserves the runtime allowlist after builder normalizes global filters', () => {
const root = process.cwd(); 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-')); const root = mkdtempSync(path.join(tmpdir(), 'subminer-audit-'));
try { try {
const input = path.join(root, 'input'); 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)), streamGenerator: () => createReadStream(path.join(input, name)),
})), })),
); );
assert.deepEqual(listAppFiles(archive), [ assert.deepEqual(listAppFiles(archive), ['main.js', 'native.node', 'dist/ai/client.js']);
{ path: 'main.js', bytes: 5 }, assert.deepEqual(listFiles(output).sort(), ['app.asar', 'app.asar.unpacked/native.node']);
{ 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,
);
} finally { } finally {
rmSync(root, { recursive: true, force: true }); 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 { readFileSync } from 'node:fs';
import { resolve } from 'node:path'; import { resolve } from 'node:path';
import { import {
executableRunLines,
jobSteps, jobSteps,
readWorkflow, readWorkflow,
stepRunsCommand, stepRunsCommand,
@@ -109,20 +108,12 @@ test('prerelease workflow builds and uploads all release platforms', () => {
...paths, ...paths,
'release/latest*.yml', 'release/latest*.yml',
'release/*.blockmap', 'release/*.blockmap',
'release/package-size-*.json',
]); ]);
const download = jobSteps(parsedPrereleaseWorkflow, 'release').find( const download = jobSteps(parsedPrereleaseWorkflow, 'release').find(
(step) => step.uses === 'actions/download-artifact@v4' && step.with?.name === name, (step) => step.uses === 'actions/download-artifact@v4' && step.with?.name === name,
); );
assert.equal(download?.with?.path, 'release'); 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', () => { 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]) { for (const workflow of [releaseWorkflow, prerelease]) {
assert.match(workflow, /uses: \.\/\.github\/workflows\/package-release\.yml/); assert.match(workflow, /uses: \.\/\.github\/workflows\/package-release\.yml/);
assert.match(workflow, /needs: \[package\]/); assert.match(workflow, /needs: \[package\]/);
assert.match(workflow, /release\/package-size-\*\.json/);
} }
assert.deepEqual( assert.deepEqual(
templateExpressionsInRunBodies( templateExpressionsInRunBodies(