Harden public release gates

This commit is contained in:
Sucukdeluxe
2026-08-14 00:06:29 +02:00
parent 21840cef94
commit e681338631
7 changed files with 129 additions and 53 deletions
+22 -43
View File
@@ -6,19 +6,6 @@ const path = require('node:path');
const { spawnSync } = require('node:child_process');
const root = path.resolve(__dirname, '..');
const rootFiles = [
'.gitignore',
'README.md',
'SECURITY.md',
'eslint.config.mjs',
'main.js',
'package-lock.json',
'package.json',
'preload-drop-target.js',
'preload.js'
];
const directoryRoots = [`.${['gi', 'tea'].join('')}`, `.${['git', 'hub'].join('')}`, 'assets', 'docs', 'lib', 'renderer', 'services/backup-api', 'tests'];
const scriptFiles = ['scripts/afterPack.cjs', 'scripts/dev-runner.cjs', 'scripts/release-plan.mjs', 'scripts/verify-public-release.mjs'];
const screenshotFiles = [
'assets/product-overview.png',
'docs/screenshots/upload-workspace.png',
@@ -28,42 +15,19 @@ const screenshotFiles = [
];
const currentVersion = require('../package.json').version;
function copyDirectory(source, destination) {
fs.mkdirSync(destination, { recursive: true });
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
if (/^_ui-inject\..+\.tmp\.js$/.test(entry.name)) continue;
const sourcePath = path.join(source, entry.name);
const destinationPath = path.join(destination, entry.name);
if (entry.isDirectory()) copyDirectory(sourcePath, destinationPath);
else if (entry.isFile()) fs.copyFileSync(sourcePath, destinationPath);
}
}
function copyDocumentationScreenshots(stage) {
for (const relativePath of screenshotFiles.slice(1)) {
const destination = path.join(stage, relativePath);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(path.join(root, screenshotFiles[0]), destination);
}
}
function createStage() {
const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'mhu-public-verifier-'));
for (const relativePath of rootFiles) {
const tracked = spawnSync('git', ['ls-files', '-z'], {
cwd: root,
encoding: 'buffer'
});
assert.equal(tracked.status, 0, tracked.stderr?.toString('utf8'));
const trackedFiles = tracked.stdout.toString('utf8').split('\0').filter(Boolean);
for (const relativePath of trackedFiles) {
const destination = path.join(stage, relativePath);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(path.join(root, relativePath), destination);
}
for (const relativePath of directoryRoots) {
if (relativePath === 'docs') copyDocumentationScreenshots(stage);
else copyDirectory(path.join(root, relativePath), path.join(stage, relativePath));
}
for (const relativePath of scriptFiles) {
const destination = path.join(stage, relativePath);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.copyFileSync(path.join(root, relativePath), destination);
}
fs.rmSync(path.join(stage, 'assets', 'product-overview.png'), { force: true });
return stage;
}
@@ -102,6 +66,21 @@ test('public release verifier accepts only the exact source manifest and target
assert.match(wrongVersion.stderr, /package\.json\tpackage-version-target/);
});
test('public source verification runs against the actual checkout at its package version', () => {
const executable = process.platform === 'win32' ? process.env.ComSpec : 'npm';
const args = process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run --silent verify:public-source']
: ['run', '--silent', 'verify:public-source'];
const result = spawnSync(executable, args, {
cwd: root,
encoding: 'utf8'
});
assert.equal(result.status, 0, result.error?.message || result.stderr || result.stdout);
assert.match(result.stdout, new RegExp(`version=${currentVersion.replaceAll('.', '\\.')}\\b`));
assert.match(result.stdout, /layout=exact/);
});
test('public release verifier requires and validates every approved screenshot', (t) => {
const stage = createStage();
t.after(() => fs.rmSync(stage, { recursive: true, force: true }));
+43
View File
@@ -0,0 +1,43 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
const releasePlanUrl = pathToFileURL(path.resolve(__dirname, '../scripts/release-plan.mjs')).href;
test('release planning rejects omitted or blank English release notes', async () => {
const { parseReleaseArgs } = await import(releasePlanUrl);
assert.throws(
() => parseReleaseArgs(['2.1.20', '--transport-tag', 'v2.1.20']),
/English release notes are required/
);
assert.throws(
() => parseReleaseArgs(['2.1.20', '--transport-tag', 'v2.1.20', ' ']),
/English release notes are required/
);
});
test('release planning preserves dual-host asset names with English release notes', async () => {
const { createReleasePlan, parseReleaseArgs } = await import(releasePlanUrl);
const plan = createReleasePlan(parseReleaseArgs([
'2.1.20',
'--transport-tag',
'v2.1.20',
'Security hardening and reliability fixes.'
]));
assert.equal(plan.releaseBody, 'Security hardening and reliability fixes.');
assert.deepEqual(plan.expectedArtifacts, [
'Multi-Hoster-Upload Setup 2.1.20.exe',
'Multi-Hoster-Upload 2.1.20.exe',
'Multi-Hoster-Upload Setup 2.1.20.exe.blockmap',
'latest.yml'
]);
assert.deepEqual(plan.githubExpectedArtifacts, [
'Multi-Hoster-Upload.Setup.2.1.20.exe',
'Multi-Hoster-Upload.2.1.20.exe',
'Multi-Hoster-Upload.Setup.2.1.20.exe.blockmap',
'latest.yml'
]);
});