CI / verify (push) Failing after 21m12s
Preserve uncertain remote commits and destructive source-cleanup requirements across cancellation, persistence failures, retries, and restart recovery. Revalidate account availability after admission waits and prevent late upload results from bypassing cancellation state. Strengthen release argument, CI tag, source allowlist, and secret scanning gates. Run the Electron smoke suite in a fully hidden offscreen harness that cannot reveal, focus, or elevate native windows.
585 lines
22 KiB
JavaScript
585 lines
22 KiB
JavaScript
import { execFile } from 'node:child_process';
|
|
import { lstat, readFile, readdir } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import process from 'node:process';
|
|
import { promisify } from 'node:util';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const args = process.argv.slice(2);
|
|
const failures = new Map();
|
|
const execFileAsync = promisify(execFile);
|
|
const publicActionsDir = `.${['git', 'hub'].join('')}`;
|
|
const privateActionsDir = `.${['gi', 'tea'].join('')}`;
|
|
const sourceFiles = [
|
|
'.gitignore',
|
|
'README.md',
|
|
'SECURITY.md',
|
|
'assets/app_icon.ico',
|
|
'assets/app_icon.png',
|
|
'eslint.config.mjs',
|
|
`${privateActionsDir}/workflows/ci.yml`,
|
|
`${publicActionsDir}/workflows/ci.yml`,
|
|
'lib/account-auth.js',
|
|
'lib/account-rotation.js',
|
|
'lib/backup-crypto.js',
|
|
'lib/batch-mutation-gate.js',
|
|
'lib/clouddrop-upload.js',
|
|
'lib/coalesced-set.js',
|
|
'lib/config-store.js',
|
|
'lib/diagnostics-agent.js',
|
|
'lib/diagnostics-collectors.js',
|
|
'lib/doodstream-upload.js',
|
|
'lib/file-probe.js',
|
|
'lib/file-discovery.js',
|
|
'lib/folder-monitor.js',
|
|
'lib/hosters.js',
|
|
'lib/hoster-transport-error.js',
|
|
'lib/ip-allowlist.js',
|
|
'lib/log-mode.js',
|
|
'lib/log-policy.js',
|
|
'lib/log-rotation.js',
|
|
'lib/online-backup.js',
|
|
'lib/orphan-tmp.js',
|
|
'lib/queue-dedup.js',
|
|
'lib/queue-prune.js',
|
|
'lib/remote-capture-preload.js',
|
|
'lib/remote-capture.html',
|
|
'lib/remote-server.js',
|
|
'lib/secret-store.js',
|
|
'lib/semaphore.js',
|
|
'lib/serialized-runner.js',
|
|
'lib/settings-backup.js',
|
|
'lib/settings-import-gate.js',
|
|
'lib/source-cleanup-policy.js',
|
|
'lib/source-delete-journal.js',
|
|
'lib/source-file-cleanup.js',
|
|
'lib/speed-history.js',
|
|
'lib/startup-renderer.js',
|
|
'lib/stats.js',
|
|
'lib/support-bundle.js',
|
|
'lib/session-report.js',
|
|
'lib/throttle-timer.js',
|
|
'lib/throttle.js',
|
|
'lib/throttled-cache.js',
|
|
'lib/updater.js',
|
|
'lib/upload-audit.js',
|
|
'lib/upload-log.js',
|
|
'lib/upload-confirmation.js',
|
|
'lib/upload-diagnostics.js',
|
|
'lib/upload-manager.js',
|
|
'lib/upload-recovery.js',
|
|
'lib/upload-start-reservation.js',
|
|
'lib/vidmoly-upload.js',
|
|
'lib/voe-upload.js',
|
|
'lib/webhook-notify.js',
|
|
'main.js',
|
|
'package-lock.json',
|
|
'package.json',
|
|
'preload-drop-target.js',
|
|
'preload.js',
|
|
'renderer/account-status.js',
|
|
'renderer/account-submit.js',
|
|
'renderer/app.js',
|
|
'renderer/auto-resume.js',
|
|
'renderer/drop-target.html',
|
|
'renderer/i18n.js',
|
|
'renderer/index.html',
|
|
'renderer/history-status.js',
|
|
'renderer/queue-stats.js',
|
|
'renderer/styles.css',
|
|
'scripts/afterPack.cjs',
|
|
'scripts/dev-runner.cjs',
|
|
'scripts/release-plan.mjs',
|
|
'scripts/verify-public-release.mjs',
|
|
'services/backup-api/package-lock.json',
|
|
'services/backup-api/package.json',
|
|
'services/backup-api/src/cli.mjs',
|
|
'services/backup-api/src/server.mjs',
|
|
'services/backup-api/test/server.test.mjs',
|
|
'tests/account-auth.test.js',
|
|
'tests/account-rotation.test.js',
|
|
'tests/account-status.test.js',
|
|
'tests/auto-resume.test.js',
|
|
'tests/backup-crypto.test.js',
|
|
'tests/batch-mutation-gate.test.js',
|
|
'tests/byse-reject-recovery.test.js',
|
|
'tests/coalesced-set.test.js',
|
|
'tests/config-store.test.js',
|
|
'tests/diagnostics-agent.test.js',
|
|
'tests/diagnostics-collectors.test.js',
|
|
'tests/diagnostics-protocol.test.js',
|
|
'tests/dev-runner.test.js',
|
|
'tests/doodstream-api-upload.test.js',
|
|
'tests/doodstream-upload.test.js',
|
|
'tests/file-probe.test.js',
|
|
'tests/file-discovery.test.js',
|
|
'tests/folder-monitor.test.js',
|
|
'tests/history-status.test.js',
|
|
'tests/history-retention.test.js',
|
|
'tests/hidden-electron-window.test.js',
|
|
'tests/hosters.test.js',
|
|
'tests/hoster-recovery-provenance.test.js',
|
|
'tests/hoster-recovery-safety.test.js',
|
|
'tests/i18n.test.js',
|
|
'tests/ip-allowlist.test.js',
|
|
'tests/log-mode.test.js',
|
|
'tests/log-policy.test.js',
|
|
'tests/log-rotation.test.js',
|
|
'tests/main-remote-input.test.js',
|
|
'tests/online-backup-service.test.js',
|
|
'tests/online-backup.test.js',
|
|
'tests/orphan-tmp.test.js',
|
|
'tests/package-build-files.test.js',
|
|
'tests/public-release-verifier.test.js',
|
|
'tests/release-plan.test.js',
|
|
'tests/queue-dedup-property.test.js',
|
|
'tests/queue-dedup.test.js',
|
|
'tests/queue-persistence-scenario.test.js',
|
|
'tests/queue-prune.test.js',
|
|
'tests/queue-stats.test.js',
|
|
'tests/remote-config.test.js',
|
|
'tests/remote-server.test.js',
|
|
'tests/semaphore.test.js',
|
|
'tests/secret-store.test.js',
|
|
'tests/serialized-runner.test.js',
|
|
'tests/settings-backup.test.js',
|
|
'tests/settings-import-gate.test.js',
|
|
'tests/speed-history.test.js',
|
|
'tests/source-cleanup-policy.test.js',
|
|
'tests/source-delete-journal.test.js',
|
|
'tests/source-file-cleanup.test.js',
|
|
'tests/startup-renderer.test.js',
|
|
'tests/stats.test.js',
|
|
'tests/support-bundle.test.js',
|
|
'tests/support/hidden-electron-window.js',
|
|
'tests/support/ui-network-safety.js',
|
|
'tests/suspect-reject-alternates.test.js',
|
|
'tests/throttle-timer.test.js',
|
|
'tests/throttle.test.js',
|
|
'tests/throttled-cache.test.js',
|
|
'tests/ui-network-safety.test.js',
|
|
'tests/ui-smoke.js',
|
|
'tests/updater-version.test.js',
|
|
'tests/upload-audit.test.js',
|
|
'tests/upload-log.test.js',
|
|
'tests/upload-confirmation.test.js',
|
|
'tests/upload-diagnostics.test.js',
|
|
'tests/upload-finalization-coordinator.test.js',
|
|
'tests/upload-manager.test.js',
|
|
'tests/upload-manager-recovery-claims.test.js',
|
|
'tests/upload-recovery.test.js',
|
|
'tests/upload-start-reservation.test.js',
|
|
'tests/session-report.test.js',
|
|
'tests/validate-credentials.test.js',
|
|
'tests/webhook-notify.test.js'
|
|
];
|
|
const screenshotFiles = [
|
|
'assets/product-overview.png',
|
|
'docs/screenshots/upload-workspace.png',
|
|
'docs/screenshots/account-management.png',
|
|
'docs/screenshots/automation-settings.png',
|
|
'docs/screenshots/history.png'
|
|
];
|
|
const allowedFiles = new Set([...sourceFiles, ...screenshotFiles]);
|
|
const textExtensions = new Set(['.cjs', '.css', '.html', '.js', '.json', '.md', '.mjs', '.txt', '.yaml', '.yml']);
|
|
const binaryExtensions = new Set(['.ico', '.png']);
|
|
const expectedScripts = {
|
|
start: 'electron .',
|
|
dev: 'node scripts/dev-runner.cjs',
|
|
test: 'npm run test:unit && npm run test:ui',
|
|
'test:unit': 'node --test tests/*.test.js',
|
|
'test:ui': 'node --test tests/ui-smoke.js',
|
|
'test:backup-api': 'npm --prefix services/backup-api test',
|
|
'verify:public-source': 'node scripts/verify-public-release.mjs --source-only --tracked --package-version',
|
|
verify: 'npm run lint && npm test && npm run test:backup-api && npm audit --omit=dev',
|
|
lint: 'eslint .',
|
|
dist: 'electron-builder --publish never --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 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',
|
|
['release_', ['gi', 'tea'].join(''), '.mjs'].join('')
|
|
]);
|
|
const aiTerms = [
|
|
['clau', 'de'].join(''),
|
|
['co', 'dex'].join(''),
|
|
['chat', 'gpt'].join('')
|
|
].join('|');
|
|
const personalTerms = [
|
|
['pl', 'oet'].join(''),
|
|
['baker', 'edwin318'].join('')
|
|
].join('|');
|
|
const internalTerms = [
|
|
['internal', ' investigation'].join(''),
|
|
['interne', ' untersuchung'].join(''),
|
|
['audit', ' method'].join(''),
|
|
['test', ' chronicle'].join(''),
|
|
['generated', ' by'].join(''),
|
|
['co-authored', '-by'].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 forbiddenInternalPattern = new RegExp(`\\b(?:${internalTerms})\\b`, 'i');
|
|
const credentialPatterns = [
|
|
new RegExp(`\\b${['gh', '[pousr]_'].join('')}[A-Za-z0-9]{36,}\\b`),
|
|
/\bgithub_pat_[A-Za-z0-9_]{60,}\b/,
|
|
/\bnpm_[A-Za-z0-9]{36,}\b/,
|
|
/\bxox[baprs]-[A-Za-z0-9-]{24,}\b/,
|
|
/\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
|
|
/\bBearer\s+[A-Za-z0-9._~+/=-]{16,}(?=$|[\s"'`,;}\]])/i,
|
|
new RegExp(['-----BEGIN ', '(?:RSA |EC |DSA |OPENSSH )?', 'PRIVATE KEY-----'].join('')),
|
|
new RegExp(`\\b${['aws', '_secret_access_key'].join('')}\\b\\s*[:=]\\s*["']?[A-Za-z0-9+/]{40}["']?`, 'i')
|
|
];
|
|
const credentialAssignmentPattern = /\b(?:password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|token|secret|cookie|session(?:id|token)?|authorization)\b\s*[:=]\s*["'`]([^"'`\r\n]{8,})["'`]/giu;
|
|
const unquotedCredentialAssignmentPattern = /\b(?:password|passwd|api[_-]?key|access[_-]?token|auth[_-]?token|token|secret|cookie|session(?:id|token)?|authorization)\b\s*[:=]\s*([A-Za-z0-9._~+/=-]{12,})\s*(?:#.*)?$/gimu;
|
|
const updaterOnlyTerms = [
|
|
'ed.cisum-42.tig'.split('').reverse().join('').replace(/[^a-z0-9]/g, ''),
|
|
'daolpu-retsoh-itlum/rotartsinimda'.split('').reverse().join('').replace(/[^a-z0-9]/g, '')
|
|
];
|
|
|
|
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 buildAllowedDirectories(files) {
|
|
const directories = new Set();
|
|
for (const file of files) {
|
|
let current = path.posix.dirname(file);
|
|
while (current && current !== '.') {
|
|
directories.add(current);
|
|
current = path.posix.dirname(current);
|
|
}
|
|
}
|
|
return directories;
|
|
}
|
|
|
|
const allowedDirectories = buildAllowedDirectories(allowedFiles);
|
|
|
|
function isDeniedBasename(basename) {
|
|
const lower = basename.toLowerCase();
|
|
return deniedBasenames.has(lower)
|
|
|| /^\.env(?:\.|$)/i.test(basename)
|
|
|| /\.(?:bak|db|log|sqlite|sqlite3|tmp)$/i.test(basename);
|
|
}
|
|
|
|
function parseArguments(packageVersion) {
|
|
const sourceOnlyCount = args.filter((arg) => arg === '--source-only').length;
|
|
const trackedCount = args.filter((arg) => arg === '--tracked').length;
|
|
const packageVersionCount = args.filter((arg) => arg === '--package-version').length;
|
|
const versionFlagIndexes = args.map((arg, index) => arg === '--version' ? index : -1).filter((index) => index >= 0);
|
|
const versionIndex = versionFlagIndexes[0] ?? -1;
|
|
const explicitVersion = versionIndex >= 0 ? args[versionIndex + 1] : '';
|
|
const expectedVersion = packageVersionCount === 1 ? packageVersion : explicitVersion;
|
|
const consumed = new Set();
|
|
|
|
if (sourceOnlyCount === 1) consumed.add(args.indexOf('--source-only'));
|
|
if (sourceOnlyCount > 1) addFailure('scripts/verify-public-release.mjs', 'duplicate-source-only');
|
|
if (trackedCount === 1) consumed.add(args.indexOf('--tracked'));
|
|
if (trackedCount > 1 || (trackedCount === 1 && sourceOnlyCount !== 1)) {
|
|
addFailure('scripts/verify-public-release.mjs', 'tracked-source-argument');
|
|
}
|
|
if (packageVersionCount === 1) consumed.add(args.indexOf('--package-version'));
|
|
if (packageVersionCount > 1 || packageVersionCount + versionFlagIndexes.length !== 1 || !/^\d+\.\d+\.\d+$/.test(expectedVersion || '')) {
|
|
addFailure('scripts/verify-public-release.mjs', 'expected-version-argument');
|
|
} else if (versionFlagIndexes.length === 1) {
|
|
consumed.add(versionIndex);
|
|
consumed.add(versionIndex + 1);
|
|
}
|
|
|
|
for (let index = 0; index < args.length; index++) {
|
|
if (!consumed.has(index)) addFailure('scripts/verify-public-release.mjs', 'argument-allowlist');
|
|
}
|
|
|
|
return { sourceOnly: sourceOnlyCount === 1, tracked: trackedCount === 1, expectedVersion };
|
|
}
|
|
|
|
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));
|
|
if (relativePath === '.git') continue;
|
|
const absolutePath = path.join(directory, entry.name);
|
|
const stats = await lstat(absolutePath);
|
|
|
|
if (stats.isSymbolicLink()) {
|
|
addFailure(relativePath, 'unsupported-file-type');
|
|
continue;
|
|
}
|
|
|
|
if (entry.isDirectory()) {
|
|
if (!allowedDirectories.has(relativePath)) {
|
|
addFailure(relativePath, 'source-layout-allowlist');
|
|
continue;
|
|
}
|
|
files.push(...await enumerate(absolutePath, relativePath));
|
|
continue;
|
|
}
|
|
|
|
if (!entry.isFile()) {
|
|
addFailure(relativePath, 'unsupported-file-type');
|
|
continue;
|
|
}
|
|
|
|
if (isDeniedBasename(entry.name)) addFailure(relativePath, 'denied-basename');
|
|
if (!allowedFiles.has(relativePath)) addFailure(relativePath, 'source-layout-allowlist');
|
|
files.push(relativePath);
|
|
}
|
|
|
|
return files;
|
|
}
|
|
|
|
async function enumerateTracked() {
|
|
let stdout = '';
|
|
try {
|
|
({ stdout } = await execFileAsync('git', ['ls-files', '-z'], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
maxBuffer: 8 * 1024 * 1024
|
|
}));
|
|
} catch {
|
|
addFailure('.git', 'tracked-source-enumeration');
|
|
return [];
|
|
}
|
|
|
|
const files = new Set(stdout.split('\0').filter(Boolean).map(normalizeRelative));
|
|
const buildDirectories = ['lib', 'renderer'];
|
|
for (const relativeDirectory of buildDirectories) {
|
|
const buildFiles = await enumerateBuildDirectory(path.join(root, relativeDirectory), relativeDirectory);
|
|
for (const relativePath of buildFiles) files.add(relativePath);
|
|
}
|
|
|
|
const result = [...files];
|
|
for (const relativePath of result) {
|
|
let stats;
|
|
try {
|
|
stats = await lstat(path.join(root, relativePath));
|
|
} catch {
|
|
addFailure(relativePath, 'required-source-file');
|
|
continue;
|
|
}
|
|
if (!stats.isFile() || stats.isSymbolicLink()) addFailure(relativePath, 'unsupported-file-type');
|
|
if (isDeniedBasename(path.basename(relativePath))) addFailure(relativePath, 'denied-basename');
|
|
if (!allowedFiles.has(relativePath)) addFailure(relativePath, 'source-layout-allowlist');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function enumerateBuildDirectory(directory, relativeDirectory) {
|
|
const files = [];
|
|
let entries;
|
|
try {
|
|
entries = await readdir(directory, { withFileTypes: true });
|
|
} catch {
|
|
addFailure(relativeDirectory, 'build-input-enumeration');
|
|
return files;
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const relativePath = normalizeRelative(path.join(relativeDirectory, entry.name));
|
|
const absolutePath = path.join(directory, entry.name);
|
|
const stats = await lstat(absolutePath);
|
|
if (stats.isSymbolicLink()) {
|
|
addFailure(relativePath, 'unsupported-file-type');
|
|
} else if (entry.isDirectory()) {
|
|
files.push(...await enumerateBuildDirectory(absolutePath, relativePath));
|
|
} else if (entry.isFile()) {
|
|
files.push(relativePath);
|
|
} else {
|
|
addFailure(relativePath, 'unsupported-file-type');
|
|
}
|
|
}
|
|
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 value = await readFile(path.join(root, relativePath), 'utf8');
|
|
if (forbiddenPersonalPattern.test(value)) addFailure(relativePath, 'forbidden-personal-term');
|
|
if (forbiddenAiPattern.test(value)) addFailure(relativePath, 'forbidden-ai-term');
|
|
if (forbiddenInternalPattern.test(value)) addFailure(relativePath, 'forbidden-internal-term');
|
|
credentialAssignmentPattern.lastIndex = 0;
|
|
unquotedCredentialAssignmentPattern.lastIndex = 0;
|
|
const credentialAssignment = [...value.matchAll(credentialAssignmentPattern)]
|
|
.some((match) => !isCredentialPlaceholder(match[1], relativePath))
|
|
|| [...value.matchAll(unquotedCredentialAssignmentPattern)]
|
|
.some((match) => !isCredentialPlaceholder(match[1], relativePath));
|
|
if (credentialPatterns.some((pattern) => pattern.test(value)) || credentialAssignment) {
|
|
addFailure(relativePath, 'credential-pattern');
|
|
}
|
|
const normalizedValue = value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
if (relativePath !== 'lib/updater.js' && updaterOnlyTerms.some((term) => normalizedValue.includes(term))) {
|
|
addFailure(relativePath, 'updater-endpoint-scope');
|
|
}
|
|
}
|
|
}
|
|
|
|
function isCredentialPlaceholder(value, relativePath) {
|
|
const normalized = String(value || '').trim().toLowerCase();
|
|
if (!normalized
|
|
|| /^(?:\[?redacted\]?|none|null|undefined)$/i.test(normalized)
|
|
|| /(?:dummy|example|fake|invalid|mock|notreal|placeholder|synthetic|test)[-_ ]?/i.test(normalized)
|
|
|| /\$\{[^}]+\}/.test(normalized)
|
|
|| /^<[^>]+>$/.test(normalized)) {
|
|
return true;
|
|
}
|
|
if (!relativePath.startsWith('tests/')) return false;
|
|
const longHex = /^[a-f0-9]{32,}$/i.test(normalized);
|
|
const opaqueLetters = /^[a-z]{16,}$/i.test(normalized);
|
|
const highEntropy = normalized.length >= 24
|
|
&& /[a-z]/.test(normalized)
|
|
&& /\d/.test(normalized)
|
|
&& !/[-_ ](?:key|token|secret|password|session|account|fixture)(?:[-_ ]|$)/i.test(normalized);
|
|
return !longHex && !opaqueLetters && !highEntropy;
|
|
}
|
|
|
|
function validatePackage(packageJson, packageLock, files, expectedVersion) {
|
|
if (!packageJson) return;
|
|
if (packageJson.version !== expectedVersion) addFailure('package.json', 'package-version-target');
|
|
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');
|
|
}
|
|
if (packageJson.build?.afterPack !== 'scripts/afterPack.cjs') addFailure('scripts/afterPack.cjs', 'build-hook-entry');
|
|
|
|
if (packageLock) {
|
|
const lockRoot = packageLock.packages?.[''];
|
|
if (packageLock.version !== expectedVersion || lockRoot?.version !== expectedVersion) {
|
|
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 validateServicePackage(packageJson, packageLock) {
|
|
if (!packageJson || !packageLock) return;
|
|
const lockRoot = packageLock.packages?.[''];
|
|
if (packageLock.version !== packageJson.version || lockRoot?.version !== packageJson.version) {
|
|
addFailure('services/backup-api/package-lock.json', 'service-lock-version');
|
|
}
|
|
if (!lockRoot || lockRoot.name !== packageJson.name || stableJson(lockRoot.dependencies) !== stableJson(packageJson.dependencies)) {
|
|
addFailure('services/backup-api/package-lock.json', 'service-lock-root-metadata');
|
|
}
|
|
}
|
|
|
|
async function validateScreenshots(sourceOnly) {
|
|
if (sourceOnly) return;
|
|
for (const screenshotFile of screenshotFiles) {
|
|
try {
|
|
const data = await readFile(path.join(root, screenshotFile));
|
|
const signature = data.subarray(0, 8).toString('hex');
|
|
const width = data.length >= 24 ? data.readUInt32BE(16) : 0;
|
|
const height = data.length >= 24 ? data.readUInt32BE(20) : 0;
|
|
if (signature !== '89504e470d0a1a0a' || width < 1000 || height < 650) {
|
|
addFailure(screenshotFile, 'product-screenshot');
|
|
}
|
|
} catch {
|
|
addFailure(screenshotFile, 'required-screenshot');
|
|
}
|
|
}
|
|
}
|
|
|
|
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() {
|
|
const packageJson = await readJson('package.json', 'package-json');
|
|
const { sourceOnly, tracked, expectedVersion } = parseArguments(packageJson?.version);
|
|
const files = tracked ? await enumerateTracked() : await enumerate();
|
|
const requiredFiles = sourceOnly ? sourceFiles : [...sourceFiles, ...screenshotFiles];
|
|
for (const requiredFile of requiredFiles) {
|
|
if (!files.includes(requiredFile)) addFailure(requiredFile, 'required-source-file');
|
|
}
|
|
|
|
await validateTextFiles(files);
|
|
const packageLock = await readJson('package-lock.json', 'package-lock-json');
|
|
const servicePackage = await readJson('services/backup-api/package.json', 'service-package-json');
|
|
const serviceLock = await readJson('services/backup-api/package-lock.json', 'service-package-lock-json');
|
|
validatePackage(packageJson, packageLock, files, expectedVersion);
|
|
validateServicePackage(servicePackage, serviceLock);
|
|
await validateScreenshots(sourceOnly);
|
|
|
|
if (failures.size > 0) {
|
|
printFailures();
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
process.stdout.write(`public-release-source-ok files=${files.length} denied-paths=0 internal-terms=0 version=${packageJson.version} scripts=${Object.keys(packageJson.scripts).length} build-files=${packageJson.build.files.length} layout=exact screenshot=${sourceOnly ? 'deferred' : 'valid'}\n`);
|
|
}
|
|
|
|
main().catch(() => {
|
|
process.stderr.write('scripts/verify-public-release.mjs\tverifier-runtime\n');
|
|
process.exitCode = 1;
|
|
});
|