import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { readAndValidateManifest, REQUIRED_RELEASE_FILES } from './release-manifest-lib.mjs'; const root = resolve(import.meta.dirname, '..'); const failures = []; const required = [ 'UPDATE_VENDOO.bat', '.gitattributes', '.editorconfig', 'scripts/update-vendoo-gui.ps1', 'scripts/updater/Vendoo.Updater.Preflight.ps1', 'scripts/updater/Vendoo.Updater.UI.ps1', 'scripts/updater/Vendoo.Updater.Host.ps1', 'scripts/updater/Vendoo.Updater.Worker.ps1', 'release-manifest.json', 'tools/release-manifest-lib.mjs', 'tools/generate-release-manifest.mjs', 'tools/apply-release-manifest.mjs', 'tools/verify-release-manifest-staging-3.0.mjs', 'docs/PRODUCTION_UPDATER_3_2.md', 'db/schema.sql', ]; for (const file of required) if (!existsSync(resolve(root, file))) failures.push(`Pflichtdatei fehlt: ${file}`); const batPath = resolve(root, 'UPDATE_VENDOO.bat'); const bat = readFileSync(batPath); const batText = bat.toString('ascii'); if (bat.length < 16) failures.push('UPDATE_VENDOO.bat ist unerwartet leer.'); if (bat[0] === 0xef && bat[1] === 0xbb && bat[2] === 0xbf) failures.push('UPDATE_VENDOO.bat darf keine UTF-8-BOM besitzen.'); for (let i = 0; i < bat.length; i += 1) { if (bat[i] > 0x7f) { failures.push(`UPDATE_VENDOO.bat ist nicht reines ASCII (Byte ${i}).`); break; } if (bat[i] === 0x0a && (i === 0 || bat[i - 1] !== 0x0d)) { failures.push(`UPDATE_VENDOO.bat enthält LF ohne CR an Byte ${i}.`); break; } if (bat[i] === 0x0d && (i + 1 >= bat.length || bat[i + 1] !== 0x0a)) { failures.push(`UPDATE_VENDOO.bat enthält CR ohne LF an Byte ${i}.`); break; } } if (!batText.startsWith('@echo off\r\n')) failures.push('UPDATE_VENDOO.bat startet nicht exakt mit @echo off + CRLF.'); for (const needle of ['cd /d "%~dp0"', '%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'Vendoo.Updater.Preflight.ps1', 'update-vendoo-gui.ps1', 'pause']) { if (!batText.includes(needle)) failures.push(`BAT-Startvertrag fehlt: ${needle}`); } for (const forbidden of ['chcp ', '-PackageRoot', 'start "', 'ä', 'ö', 'ü', 'ß']) if (batText.includes(forbidden)) failures.push(`BAT enthält verbotene Regression: ${forbidden}`); const attrs = readFileSync(resolve(root, '.gitattributes'), 'ascii'); if (!attrs.includes('*.bat text eol=crlf')) failures.push('.gitattributes erzwingt CRLF für BAT nicht.'); if (!attrs.includes('*.cmd text eol=crlf')) failures.push('.gitattributes erzwingt CRLF für CMD nicht.'); for (const rel of required.filter(file => file.endsWith('.ps1'))) { const bytes = readFileSync(resolve(root, rel)); if (!(bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf)) failures.push(`${rel} besitzt keine UTF-8-BOM.`); const text = bytes.subarray(3).toString('utf8'); if (rel.includes('updater/Vendoo.Updater.') && !text.includes('3.3.0')) failures.push(`${rel} enthält nicht Updater-Version 3.3.0.`); } const workerText = readFileSync(resolve(root, 'scripts/updater/Vendoo.Updater.Worker.ps1'), 'utf8'); for (const marker of [ "release/$TargetVersion-production-update-v3", 'release-manifest.json', 'tools\\apply-release-manifest.mjs', 'overlay-with-explicit-removals', 'Veralteter oder anderer Updater-Sitzungsstand wird nicht übernommen.', 'StandardOutputEncoding', 'StandardErrorEncoding', 'Get-ProcessFailureSummary', 'Fehlerausgabe:', 'Letzte Standardausgabe:', 'Coolify Auto Deploy', '/readyz', ]) if (!workerText.includes(marker)) failures.push(`Updater-3.3-Vertrag fehlt: ${marker}`); for (const forbidden of [ "tools\\prepare-git-staging.mjs'),$PackageRoot,$CloneDir", 'cleanDestination', 'COOLIFY_DEPLOY_WEBHOOK_URL', 'Request-Secret', ]) if (workerText.includes(forbidden)) failures.push(`Verbotene 2.x-Regression im Worker: ${forbidden}`); const applyText = readFileSync(resolve(root, 'tools/apply-release-manifest.mjs'), 'utf8'); const libText = readFileSync(resolve(root, 'tools/release-manifest-lib.mjs'), 'utf8'); for (const marker of ['applyReleaseManifest', 'overlay-with-explicit-removals', 'removedFiles', 'sha256File', 'db/schema.sql', 'Zielprüfung nach Kopieren', 'CONTROL_RELEASE_FILES', 'Steuerdatei-Prüfung nach Kopieren', 'allowExtraFiles']) { if (!(applyText + libText).includes(marker)) failures.push(`Manifest-Release-Vertrag fehlt: ${marker}`); } for (const forbidden of ['cleanDestination(destination)', 'for (const name of readdirSync(destination))']) { if (libText.includes(forbidden)) failures.push(`Manifest-Applier darf die Baseline nicht leeren: ${forbidden}`); } try { const manifest = readAndValidateManifest(root, { allowExtraFiles: true }); if (manifest.version !== '1.43.0') failures.push(`Release-Manifest-Version falsch: ${manifest.version}`); if (manifest.updaterVersion !== '3.3.0') failures.push(`Release-Manifest-Updaterversion falsch: ${manifest.updaterVersion}`); for (const requiredFile of REQUIRED_RELEASE_FILES) { if (!manifest.files.some(entry => entry.path === requiredFile)) failures.push(`Release-Manifest enthält Pflichtdatei nicht: ${requiredFile}`); } } catch (error) { failures.push(`Release-Manifest ist ungültig: ${error.message}`); } const ciText = readFileSync(resolve(root, '.github/workflows/ci.yml'), 'utf8'); for (const marker of ['windows-release-contract', 'runs-on: windows-latest', 'npm run verify:all', 'verify-powershell-parser.ps1']) { if (!ciText.includes(marker)) failures.push(`Windows-CI-Vertrag fehlt: ${marker}`); } if (!ciText.includes('Geführte Shell-Installer simulieren') || !ciText.includes('run: npm run verify:installer-shell')) { failures.push('Linux-CI-Vertrag für den funktionalen Installer-Shell-Test fehlt.'); } const releaseWorkflow = readFileSync(resolve(root, '.github/workflows/release.yml'), 'utf8'); for (const marker of ['npm run verify:all', 'generate-release-manifest.mjs', 'git diff --exit-code -- release-manifest.json', 'Manifestbasiertes Release-Staging']) { if (!releaseWorkflow.includes(marker)) failures.push(`Release-Workflow-Vertrag fehlt: ${marker}`); } const buildRelease = readFileSync(resolve(root, 'tools/build-release.mjs'), 'utf8'); for (const marker of ['generateReleaseManifest', 'readAndValidateManifest', 'manifest.files', 'Manifestbasiertes Release-Staging']) { if (!buildRelease.includes(marker)) failures.push(`Manifestbasierter Release-Builder fehlt: ${marker}`); } if (buildRelease.includes('shouldSkipStagingEntry')) failures.push('Release-Builder verwendet weiterhin die alte Ausschlusslisten-Kopie.'); const installerWrapper = readFileSync(resolve(root, 'tools/verify-installer-shell.mjs'), 'utf8'); const installerShell = readFileSync(resolve(root, 'tools/verify-installer-shell.sh'), 'utf8'); for (const requiredMarker of ['VENDOO_INSTALLER_SHELL_HOST', 'VENDOO_TEST_WINDOWS_SHELL', "process.platform === 'win32'", "['-n', script]", 'verpflichtenden Linux-CI-Gate', 'Shell-Fehlerausgabe:', 'Shell-Standardausgabe:']) { if (!installerWrapper.includes(requiredMarker)) failures.push(`Installer-Wrapper-Vertrag fehlt: ${requiredMarker}`); } for (const requiredMarker of ['MINGW*|MSYS*|CYGWIN*', 'POSIX-Modus 0600', 'assert_env_permissions', 'show_file', '--exclude=.git']) { if (!installerShell.includes(requiredMarker)) failures.push(`Installer-Shell-Vertrag fehlt: ${requiredMarker}`); } if (!installerShell.includes("mode=$(stat -c '%a'")) failures.push('Linux/NAS/VPS-Rechteprüfung auf Modus 600 fehlt.'); const packageJson = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')); if (packageJson.scripts?.['verify:release-manifest'] !== 'node tools/verify-release-manifest-staging-3.0.mjs') failures.push('verify:release-manifest ist nicht korrekt verdrahtet.'); if (!packageJson.scripts?.['verify:all']?.includes('verify:release-manifest')) failures.push('verify:release-manifest fehlt in verify:all.'); for (const name of ['verify:one-click-update', 'verify:updater-ui', 'verify:updater-hotfix']) { if (packageJson.scripts?.[name] !== 'node tools/verify-production-updater-3.3.mjs') failures.push(`${name} verweist nicht auf Updater-3.3-Gate.`); } for (const [name, command] of Object.entries(packageJson.scripts || {})) { if (/\bpython(?:3)?\b|\bpy(?:\.exe)?\s+-/i.test(command)) failures.push(`Aktiver npm-Script ${name} enthält eine Python-Abhängigkeit.`); } if (failures.length) { console.error(`Production-Updater-3.3-Gate fehlgeschlagen (${failures.length})`); for (const failure of failures) console.error(`- ${failure}`); process.exit(1); } console.log('Production-Updater-3.3-Gate erfolgreich: manifestbasiertes Overlay, SHA-256-Prüfung, bytegeprüfte Steuerdatei, explizite Löschungen, Pflichtdateien und Sitzungsgrenze geprüft.');