Manifestbasierter Production-Updater 3.3, Modulnavigation, optionales Produktbild Studio, Listing-Studio-Aktionsleiste sowie plattformübergreifende Windows-/Linux-Release-Gates.
91 lines
5.2 KiB
JavaScript
91 lines
5.2 KiB
JavaScript
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.');
|