Release Multi-Hoster Uploader 3.3.108
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const args = process.argv.slice(2);
|
||||
const sourceOnly = args.includes('--source-only');
|
||||
const failures = new Map();
|
||||
|
||||
const requiredFiles = [
|
||||
'.gitignore',
|
||||
'README.md',
|
||||
'SECURITY.md',
|
||||
'package.json',
|
||||
'package-lock.json',
|
||||
'eslint.config.mjs',
|
||||
'main.js',
|
||||
'preload.js',
|
||||
'preload-drop-target.js',
|
||||
'assets/app_icon.ico',
|
||||
'assets/app_icon.png',
|
||||
'scripts/afterPack.cjs',
|
||||
'scripts/verify-public-release.mjs'
|
||||
];
|
||||
|
||||
const allowedFiles = new Set([...requiredFiles, 'assets/product-overview.png']);
|
||||
const allowedPrefixes = ['lib/', 'renderer/', 'tests/'];
|
||||
const ignoredDirectories = new Set(['.git', 'node_modules', 'release']);
|
||||
const deniedDirectories = new Set([
|
||||
`.${['clau', 'de'].join('')}`,
|
||||
`.${['co', 'dex'].join('')}`,
|
||||
'.playwright-mcp',
|
||||
'.superpowers',
|
||||
'__pycache__',
|
||||
'backups',
|
||||
'docs',
|
||||
'gateway',
|
||||
'logs',
|
||||
'memories',
|
||||
'prompts',
|
||||
'tasks'
|
||||
]);
|
||||
const deniedBasenames = new Set([
|
||||
'agents.md',
|
||||
'app.py',
|
||||
`${['clau', 'de'].join('')}.md`,
|
||||
'credentials.json',
|
||||
'gemini.md',
|
||||
'hosters.py',
|
||||
'memory.md',
|
||||
'memory_summary.md',
|
||||
'raw_memories.md',
|
||||
'requirements.txt'
|
||||
]);
|
||||
const textExtensions = new Set(['.cjs', '.css', '.html', '.js', '.json', '.md', '.mjs', '.txt', '.yaml', '.yml']);
|
||||
const binaryExtensions = new Set(['.ico', '.png']);
|
||||
const expectedScripts = {
|
||||
start: 'electron .',
|
||||
test: 'node --test tests/*.test.js tests/ui-smoke.js',
|
||||
lint: 'eslint .',
|
||||
dist: 'electron-builder --win',
|
||||
'release:win': 'electron-builder --publish never --win nsis portable'
|
||||
};
|
||||
const expectedBuildFiles = [
|
||||
'main.js',
|
||||
'preload.js',
|
||||
'preload-drop-target.js',
|
||||
'lib/**/*',
|
||||
'renderer/**/*',
|
||||
'assets/app_icon.ico',
|
||||
'assets/app_icon.png'
|
||||
];
|
||||
const aiTerms = [
|
||||
['clau', 'de'].join(''),
|
||||
['co', 'dex'].join(''),
|
||||
['chat', 'gpt'].join('')
|
||||
].join('|');
|
||||
const personalTerms = [
|
||||
['pl', 'oet'].join(''),
|
||||
['baker', 'edwin318'].join('')
|
||||
].join('|');
|
||||
const forbiddenAiPattern = new RegExp(`\\b(?:${aiTerms}|multi[\\s-]+agents?)\\b`, 'i');
|
||||
const forbiddenPersonalPattern = new RegExp(`(?:[a-z]:[\\\\/]+users[\\\\/]+|\\b(?:${personalTerms})\\b|\\bdesktop-[a-z0-9-]+\\b)`, 'i');
|
||||
const forbiddenInvestigationPattern = new RegExp(`\\b(?:${['internal', 'investigation'].join(' ')}|${['interne', 'untersuchung'].join(' ')}|${['audit', 'method'].join(' ')}|${['test', 'chronicle'].join(' ')})\\b`, 'i');
|
||||
const updaterOnlyPattern = new RegExp([
|
||||
['gi', 'tea'].join(''),
|
||||
['git', '24-music', 'de'].join('\\.'),
|
||||
[['Admin', 'istrator'].join(''), 'Multi-Hoster-Upload'].join('\\/')
|
||||
].join('|'), 'i');
|
||||
|
||||
function addFailure(file, rule) {
|
||||
if (!failures.has(file)) failures.set(file, new Set());
|
||||
failures.get(file).add(rule);
|
||||
}
|
||||
|
||||
function normalizeRelative(value) {
|
||||
return value.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function isAllowedFile(relativePath) {
|
||||
return allowedFiles.has(relativePath) || allowedPrefixes.some((prefix) => relativePath.startsWith(prefix));
|
||||
}
|
||||
|
||||
function isDeniedBasename(basename) {
|
||||
const lower = basename.toLowerCase();
|
||||
return deniedBasenames.has(lower)
|
||||
|| /^\.env(?:\.|$)/i.test(basename)
|
||||
|| /\.(?:bak|db|log|sqlite|sqlite3|tmp)$/i.test(basename);
|
||||
}
|
||||
|
||||
async function enumerate(directory = root, relativeDirectory = '') {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = normalizeRelative(path.join(relativeDirectory, entry.name));
|
||||
const lowerName = entry.name.toLowerCase();
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (ignoredDirectories.has(lowerName)) continue;
|
||||
if (deniedDirectories.has(lowerName)) addFailure(relativePath, 'denied-directory');
|
||||
files.push(...await enumerate(path.join(directory, entry.name), relativePath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.isFile()) {
|
||||
addFailure(relativePath, 'unsupported-file-type');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDeniedBasename(entry.name)) addFailure(relativePath, 'denied-basename');
|
||||
if (!isAllowedFile(relativePath)) addFailure(relativePath, 'source-layout-allowlist');
|
||||
files.push(relativePath);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function readJson(relativePath, rule) {
|
||||
try {
|
||||
return JSON.parse(await readFile(path.join(root, relativePath), 'utf8'));
|
||||
} catch {
|
||||
addFailure(relativePath, rule);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function stableJson(value) {
|
||||
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
||||
if (value && typeof value === 'object') {
|
||||
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
async function validateTextFiles(files) {
|
||||
for (const relativePath of files) {
|
||||
const extension = path.extname(relativePath).toLowerCase();
|
||||
const isTextFile = textExtensions.has(extension) || relativePath === '.gitignore';
|
||||
if (!isTextFile) {
|
||||
if (!binaryExtensions.has(extension)) addFailure(relativePath, 'source-extension-allowlist');
|
||||
continue;
|
||||
}
|
||||
|
||||
const text = await readFile(path.join(root, relativePath), 'utf8');
|
||||
if (forbiddenPersonalPattern.test(text)) addFailure(relativePath, 'forbidden-personal-term');
|
||||
if (forbiddenAiPattern.test(text)) addFailure(relativePath, 'forbidden-ai-term');
|
||||
if (forbiddenInvestigationPattern.test(text)) addFailure(relativePath, 'forbidden-investigation-term');
|
||||
if (relativePath !== 'lib/updater.js' && updaterOnlyPattern.test(text)) addFailure(relativePath, 'updater-endpoint-scope');
|
||||
}
|
||||
}
|
||||
|
||||
function validatePackage(packageJson, packageLock, files) {
|
||||
if (!packageJson) return;
|
||||
|
||||
if (packageJson.version !== '3.3.108') addFailure('package.json', 'package-version');
|
||||
if (stableJson(packageJson.scripts) !== stableJson(expectedScripts)) addFailure('package.json', 'package-script-allowlist');
|
||||
|
||||
const buildFiles = packageJson.build?.files;
|
||||
if (!Array.isArray(buildFiles) || stableJson(buildFiles) !== stableJson(expectedBuildFiles)) {
|
||||
addFailure('package.json', 'build-file-allowlist');
|
||||
}
|
||||
|
||||
for (const requiredEntry of expectedBuildFiles) {
|
||||
if (!Array.isArray(buildFiles) || !buildFiles.includes(requiredEntry)) addFailure(requiredEntry.replace('/**/*', ''), 'build-file-entry');
|
||||
}
|
||||
|
||||
if (packageJson.build?.afterPack !== 'scripts/afterPack.cjs') addFailure('scripts/afterPack.cjs', 'build-hook-entry');
|
||||
|
||||
if (packageLock) {
|
||||
const lockRoot = packageLock.packages?.[''];
|
||||
if (packageLock.version !== '3.3.108' || lockRoot?.version !== '3.3.108') {
|
||||
addFailure('package-lock.json', 'package-lock-version');
|
||||
}
|
||||
if (!lockRoot
|
||||
|| lockRoot.name !== packageJson.name
|
||||
|| stableJson(lockRoot.dependencies) !== stableJson(packageJson.dependencies)
|
||||
|| stableJson(lockRoot.devDependencies) !== stableJson(packageJson.devDependencies)) {
|
||||
addFailure('package-lock.json', 'package-lock-root-metadata');
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of expectedBuildFiles) {
|
||||
if (entry.endsWith('/**/*')) {
|
||||
const prefix = entry.slice(0, -4);
|
||||
if (!files.some((file) => file.startsWith(prefix))) addFailure(entry.slice(0, -5), 'build-entry-target');
|
||||
} else if (!files.includes(entry)) {
|
||||
addFailure(entry, 'build-entry-target');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printFailures() {
|
||||
for (const file of [...failures.keys()].sort()) {
|
||||
for (const rule of [...failures.get(file)].sort()) {
|
||||
process.stderr.write(`${file}\t${rule}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (args.some((arg) => arg !== '--source-only') || args.filter((arg) => arg === '--source-only').length > 1) {
|
||||
addFailure('scripts/verify-public-release.mjs', 'argument-allowlist');
|
||||
}
|
||||
|
||||
const files = await enumerate();
|
||||
|
||||
for (const requiredFile of requiredFiles) {
|
||||
if (!files.includes(requiredFile)) addFailure(requiredFile, 'required-source-file');
|
||||
}
|
||||
if (!sourceOnly && !files.includes('assets/product-overview.png')) {
|
||||
addFailure('assets/product-overview.png', 'required-screenshot');
|
||||
}
|
||||
|
||||
await validateTextFiles(files);
|
||||
const packageJson = await readJson('package.json', 'package-json');
|
||||
const packageLock = await readJson('package-lock.json', 'package-lock-json');
|
||||
validatePackage(packageJson, packageLock, files);
|
||||
|
||||
if (failures.size > 0) {
|
||||
printFailures();
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(`public-release-source-ok files=${files.length} denied-paths=0 forbidden-terms=0 version=${packageJson.version} scripts=${Object.keys(packageJson.scripts).length} build-files=${packageJson.build.files.length} layout=valid screenshot=${sourceOnly ? 'deferred' : 'present'}\n`);
|
||||
}
|
||||
|
||||
main().catch(() => {
|
||||
process.stderr.write('scripts/verify-public-release.mjs\tverifier-runtime\n');
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user