import { readFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { DEPLOYMENT_STATES, ACTIVE_DEPLOYMENT_STATES, canTransitionDeployment, isActiveDeploymentState, } from '../app/modules/deployments/state-machine.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const read = path => readFileSync(join(root, path), 'utf8'); const failures = []; const assert = (condition, message) => { if (!condition) failures.push(message); }; const contains = (file, markers) => { const source = read(file); for (const marker of markers) assert(source.includes(marker), `${file}: Vertrag fehlt: ${marker}`); return source; }; const pkg = JSON.parse(read('package.json')); assert(pkg.version === '1.43.0', `package.json ist ${pkg.version}`); assert(pkg.scripts['verify:production-operations']?.includes('verify-production-operations-1.42.0.mjs'), 'NPM-Gate für Production Operations fehlt'); assert(DEPLOYMENT_STATES.length === 10, `State-Machine hat ${DEPLOYMENT_STATES.length} statt 10 Zustände`); for (const state of ['checking','backup_pending','backup_running','backup_verified','deploy_requested','deploy_running','health_wait','succeeded','failed','rollback_required']) { assert(DEPLOYMENT_STATES.includes(state), `Zustand fehlt: ${state}`); } for (const [from, to] of [ ['checking','backup_pending'], ['backup_running','backup_verified'], ['backup_verified','deploy_requested'], ['deploy_requested','deploy_running'], ['deploy_running','health_wait'], ['health_wait','succeeded'], ['health_wait','rollback_required'], ['rollback_required','failed'], ]) assert(canTransitionDeployment(from, to), `Legaler Übergang blockiert: ${from} -> ${to}`); for (const [from, to] of [['checking','succeeded'], ['succeeded','deploy_requested'], ['failed','checking'], ['rollback_required','succeeded']]) { assert(!canTransitionDeployment(from, to), `Illegaler Übergang erlaubt: ${from} -> ${to}`); } assert(ACTIVE_DEPLOYMENT_STATES.includes('rollback_required'), 'Rollbackbedarf blockiert keine Folgeaufträge'); assert(isActiveDeploymentState('health_wait') && !isActiveDeploymentState('succeeded'), 'Aktivstatus ist fehlerhaft'); contains('lib/db.mjs', [ 'CREATE TABLE IF NOT EXISTS deployment_runs', 'idempotency_key TEXT NOT NULL UNIQUE', 'CREATE TABLE IF NOT EXISTS deployment_events', 'CREATE TABLE IF NOT EXISTS backup_verifications', 'CREATE TABLE IF NOT EXISTS production_checks', "production-operations-1.42.0", ]); const repository = contains('app/modules/deployments/repository.mjs', [ 'DEPLOYMENT_ALREADY_ACTIVE', 'findByIdempotencyKey', 'recordBackupVerification', 'createProductionCheck', 'canTransitionDeployment', ]); assert(!repository.includes('DELETE FROM deployment_runs'), 'Deployment-Repository löscht Verlauf'); const service = contains('app/modules/deployments/service.mjs', [ "input.confirmation !== 'DEPLOY'", "input.confirmation !== 'CONFIRM'", 'verifyStoredBackup(backup.id)', 'DEPLOYMENT_BACKUP_INVALID', 'DEPLOYMENT_TARGET_NOT_APPROVED', 'UPDATE_CHECKSUM_MISSING', 'UPDATE_URL_INSECURE', 'deployment_status_endpoint', 'probePublicReadiness', 'targetMatches', 'rollback_required', 'runProductionCheck', 'VENDOO_COOLIFY_AUTO_DEPLOY', 'docker_socket_access: false', ]); for (const forbidden of ['docker.sock', 'git pull', 'npm install', 'docker compose', 'child_process']) { assert(!service.toLowerCase().includes(forbidden), `Deployment-Service enthält verbotenen Selbstupdate-/Hostzugriff: ${forbidden}`); } contains('app/modules/deployments/routes.mjs', [ "policy: 'updates.manage'", "csrf: true", '/api/admin/deployment/runs', '/api/admin/deployment/runs/:id/refresh', '/api/admin/deployment/runs/:id/confirm', '/api/admin/deployment/production-check', "req.get('Idempotency-Key')", ]); contains('server.mjs', ['verifyStoredBackup,', 'getPlatformSnapshot: () => platformKernel.snapshot()', 'registerDeploymentRoutes']); contains('public/index.html', [ 'deployment-production-overview', 'deployment-production-check', 'deployment-status-endpoint', 'deployment-target-version', 'deployment-run-list', 'content="1.43.0"', ]); contains('public/app.js', [ "const VENDOO_UI_BUILD = '1.43.0';", 'deploymentStateLabel', 'data-deployment-refresh', 'data-deployment-confirm', 'data-deployment-resolve', 'idempotency_key', ]); contains('.env.example', [ 'COOLIFY_DEPLOYMENT_STATUS_ENDPOINT=', 'VENDOO_DEPLOY_TIMEOUT_SECONDS=900', 'VENDOO_DEPLOY_HEALTH_POLL_SECONDS=15', 'VENDOO_COOLIFY_AUTO_DEPLOY=false', ]); const moduleManifest = JSON.parse(read('app/modules/deployments/module.json')); assert(moduleManifest.version === '1.1.0', `Deployment-Modulversion ist ${moduleManifest.version}`); assert(moduleManifest.provides.includes('deployments.state-machine'), 'State-Machine-Capability fehlt'); assert(moduleManifest.provides.includes('deployments.production-checks'), 'Production-Check-Capability fehlt'); if (failures.length) { console.error(`Production-Operations-Gate 1.42.0 fehlgeschlagen (${failures.length}):`); for (const failure of failures) console.error(`- ${failure}`); process.exit(1); } console.log('Production-Operations-Gate 1.42.0 erfolgreich: State-Machine, Idempotenz, Backup-Gate, Coolify-Status, Readiness, Rollback-Sperre, Produktionscheck, API und UI geprüft.');