Files
vendoo/tools/verify-production-updater-3.4.mjs
T

171 lines
11 KiB
JavaScript

import { existsSync, readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import {
classifyChecks,
createPackageManifest,
decideReleasePath,
evaluatePayloadGuard,
formatStatusMatrix,
PACKAGE_MANIFEST_SCHEMA,
REQUIRED_CHECKS,
REQUIRED_WORKFLOWS,
SESSION_SCHEMA,
UPDATER_VERSION,
} from './updater-lifecycle-3.4.mjs';
import { inspectPayload } from './check-release-payload-3.4.mjs';
const root = resolve(import.meta.dirname, '..');
const failures = [];
const assert = (condition, message) => { if (!condition) failures.push(message); };
const read = rel => readFileSync(join(root, rel), 'utf8');
for (const file of [
'scripts/updater/Vendoo.Updater.Worker34.ps1',
'tools/updater-lifecycle-3.4.mjs',
'tools/check-release-payload-3.4.mjs',
'tools/verify-production-updater-3.4.mjs',
'docs/PRODUCTION_UPDATER_3_4.md',
]) assert(existsSync(join(root, file)), `Pflichtdatei fehlt: ${file}`);
const lifecycle = read('tools/updater-lifecycle-3.4.mjs');
for (const marker of ['main-and-production-at-target', 'main-at-target-production-behind', 'matching-open-pr', 'matching-pr-already-merged']) {
assert(lifecycle.includes(marker), `Lifecycle-Entscheidung fehlt: ${marker}`);
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Worker34.ps1'))) {
const worker = read('scripts/updater/Vendoo.Updater.Worker34.ps1');
for (const marker of [
"'3.4.0'", 'production-update-v4', 'payloadHash', 'baseCommit', 'sourceCommit',
'Get-ReleaseObservation', 'Wait-RequiredGitHubChecks', 'headRefOid', 'workflow_runs',
'force-with-lease', 'completedAt', 'finalPrHeadSha',
'check-release-payload-3.4.mjs', '--match-head-commit',
]) assert(worker.includes(marker), `Worker-3.4-Vertrag fehlt: ${marker}`);
assert(!worker.includes("@('pr','checks'"), 'Updater 3.4 darf gh pr checks --watch nicht mehr verwenden.');
assert(!worker.includes("@('push','--force'"), 'Updater 3.4 darf keinen unbedingten Force-Push verwenden.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.UI.ps1'))) {
const ui = read('scripts/updater/Vendoo.Updater.UI.ps1');
assert(ui.includes("$UpdaterVersion = '3.4.0'"), 'Updater-UI meldet nicht Version 3.4.0.');
assert(ui.includes("Vendoo.Updater.Worker34.ps1"), 'Updater-UI startet nicht den Worker 3.4.');
}
if (existsSync(join(root, 'scripts/updater/Vendoo.Updater.Host.ps1'))) {
assert(read('scripts/updater/Vendoo.Updater.Host.ps1').includes("$UpdaterVersion = '3.4.0'"), 'Updater-Host meldet nicht Version 3.4.0.');
}
const manifestSync = read('.github/workflows/sync-release-manifest.yml');
assert(manifestSync.includes("'release/*-production-update-v*'"), 'Manifest-Sync ist nicht auf versionsneutrale Releasebranches generalisiert.');
assert(!manifestSync.includes('release/1.43.0-production-update-v3'), 'Manifest-Sync enthält weiterhin den alten festen 3.3-Branch.');
const packageJson = JSON.parse(read('package.json'));
for (const name of ['verify:one-click-update', 'verify:updater-ui', 'verify:updater-hotfix']) {
assert(packageJson.scripts?.[name] === 'node tools/verify-production-updater-3.4.mjs', `${name} verweist nicht auf Updater 3.4.`);
}
assert(packageJson.scripts?.['verify:updater-lifecycle'] === 'node tools/verify-production-updater-3.4.mjs', 'Eigenständiger Lifecycle-Test fehlt.');
const updateManifest = JSON.parse(read('update-manifest.json'));
assert(updateManifest.updaterVersion === UPDATER_VERSION && updateManifest.updater_version === UPDATER_VERSION, 'Update-Manifest meldet nicht 3.4.0.');
const sourceManifest = {
schema: 'vendoo-release-manifest-v1', version: '1.44.0', updaterVersion: UPDATER_VERSION,
mode: 'overlay-with-explicit-removals', requiredFiles: ['package.json'], removedFiles: [],
files: [{ path: 'package.json', sha256: 'a'.repeat(64), size: 1 }],
};
const packageManifest = createPackageManifest(sourceManifest, { sourceCommit: '1'.repeat(40), baseCommit: '2'.repeat(40), createdAt: '2026-07-10T12:00:00Z' });
assert(packageManifest.schema === PACKAGE_MANIFEST_SCHEMA, 'Auslieferungsmanifest verwendet nicht Schema v2.');
assert(/^[a-f0-9]{64}$/.test(packageManifest.payloadHash), 'Auslieferungsmanifest besitzt keinen stabilen Payload-Hash.');
const successChecks = REQUIRED_CHECKS.map(name => ({ name, status: 'COMPLETED', conclusion: 'SUCCESS', completedAt: '2026-07-10T12:00:00Z' }));
const successRuns = REQUIRED_WORKFLOWS.map(name => ({ name, status: 'completed', conclusion: 'success', updated_at: '2026-07-10T12:00:00Z' }));
let result = classifyChecks({ checks: [], workflowRuns: [], elapsedSeconds: 10, gracePeriodSeconds: 90 });
assert(result.state === 'waiting-registration', 'Leere Checks innerhalb der Grace Period müssen warten.');
result = classifyChecks({ checks: successChecks, workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'success', 'Vollständig erfolgreiche Checks wurden nicht erkannt.');
for (const status of ['QUEUED', 'PENDING', 'IN_PROGRESS']) {
result = classifyChecks({ checks: [{ name: REQUIRED_CHECKS[0], status }], workflowRuns: [], elapsedSeconds: 120 });
assert(['pending', 'waiting-missing'].includes(result.state), `${status} wurde nicht als laufend behandelt.`);
}
result = classifyChecks({ checks: [...successChecks, { ...successChecks[0], completedAt: '2026-07-10T11:00:00Z', conclusion: 'CANCELLED' }], workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'success', 'Paralleler älterer abgebrochener Check darf neueren Erfolg nicht überstimmen.');
result = classifyChecks({ checks: successChecks.map((x, i) => i ? x : ({ ...x, conclusion: 'FAILURE' })), workflowRuns: successRuns, elapsedSeconds: 120 });
assert(result.state === 'failed', 'Fehlgeschlagener Pflichtcheck wurde nicht terminal erkannt.');
assert(formatStatusMatrix(result.rows).includes(REQUIRED_CHECKS[0]), 'Timeout-/Fehlermatrix enthält Checknamen nicht.');
const runBase = successRuns.filter(run => run.name !== REQUIRED_WORKFLOWS[0]);
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'success', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'failure', updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'failed', 'Ein älterer erfolgreicher Workflow-Run darf einen neueren Fehler nicht verdecken.');
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'failure', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'in_progress', conclusion: null, updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'pending', 'Ein neuer laufender Workflow-Run muss einen älteren Fehler ablösen.');
result = classifyChecks({
checks: successChecks,
workflowRuns: [
...runBase,
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'cancelled', updated_at: '2026-07-10T11:00:00Z' },
{ name: REQUIRED_WORKFLOWS[0], status: 'completed', conclusion: 'success', updated_at: '2026-07-10T12:00:00Z' },
],
elapsedSeconds: 120,
});
assert(result.state === 'success', 'Ein neuer erfolgreicher Workflow-Run muss einen älteren Abbruch ablösen.');
let decision = decideReleasePath({ targetVersion: '1.43.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0' });
assert(decision.action === 'complete', 'Bereits deployte Zielversion muss sofort erfolgreich enden.');
decision = decideReleasePath({ targetVersion: '1.43.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.42.0' });
assert(decision.action === 'deploy', 'main auf Zielversion darf keinen Quell-PR erzeugen.');
decision = decideReleasePath({ targetVersion: '1.44.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0', pullRequests: [{ targetVersion: '1.44.0', state: 'OPEN', number: 30, updatedAt: '2026-07-10T12:00:00Z' }] });
assert(decision.action === 'resume-pr', 'Vorhandener offener PR muss übernommen werden.');
decision = decideReleasePath({ targetVersion: '1.44.0', mainVersion: '1.43.0', productionReady: true, productionVersion: '1.43.0', pullRequests: [{ targetVersion: '1.44.0', state: 'CLOSED', mergedAt: '2026-07-10T12:00:00Z' }] });
assert(decision.action === 'deploy', 'Bereits gemergter PR muss die Sourcephase überspringen.');
assert(evaluatePayloadGuard({ baseCommit: 'a', currentMainCommit: 'b', baseIsAncestor: true, conflictingPaths: ['x'] }).allowed === false, 'Veralteter Payload mit Konflikt wurde nicht blockiert.');
assert(evaluatePayloadGuard({ baseCommit: 'a', currentMainCommit: 'b', baseIsAncestor: true, conflictingPaths: [] }).allowed === true, 'Nicht kollidierender Main-Fortschritt muss zulässig sein.');
const temp = mkdtempSync(join(tmpdir(), 'vendoo-updater34-'));
try {
const repo = join(temp, 'repo');
const pkg = join(temp, 'pkg');
mkdirSync(repo); mkdirSync(pkg);
const run = (args, cwd = repo) => {
const r = spawnSync('git', args, { cwd, encoding: 'utf8' });
if (r.status !== 0) throw new Error(r.stderr || r.stdout);
return r.stdout.trim();
};
run(['init']); run(['config', 'user.email', 'ci@example.invalid']); run(['config', 'user.name', 'CI']);
writeFileSync(join(repo, 'a.txt'), 'base\n'); writeFileSync(join(repo, 'b.txt'), 'base\n');
run(['add', '.']); run(['commit', '-m', 'base']); const base = run(['rev-parse', 'HEAD']);
writeFileSync(join(repo, 'a.txt'), 'main-new\n'); run(['add', '.']); run(['commit', '-m', 'main']); const main = run(['rev-parse', 'HEAD']);
const content = Buffer.from('payload-old\n');
writeFileSync(join(pkg, 'a.txt'), content);
writeFileSync(join(pkg, 'release-manifest.json'), JSON.stringify({ schema: PACKAGE_MANIFEST_SCHEMA, files: [{ path: 'a.txt', sha256: createHash('sha256').update(content).digest('hex'), size: content.length }], removedFiles: [] }));
const guard = inspectPayload({ packageRoot: pkg, repositoryRoot: repo, baseCommit: base, currentMainCommit: main });
assert(!guard.allowed && guard.conflicts.includes('a.txt'), 'Realer Git-Payload-Rückschritt wurde nicht blockiert.');
} catch (error) {
failures.push(`Payload-Regressionstest fehlgeschlagen: ${error.message}`);
} finally {
rmSync(temp, { recursive: true, force: true });
}
assert(SESSION_SCHEMA === 'vendoo-updater-session-v2', 'Session-Schema ist nicht v2.');
if (failures.length) {
console.error(`Production-Updater-3.4-Gate fehlgeschlagen (${failures.length})`);
failures.forEach(failure => console.error(`- ${failure}`));
process.exit(1);
}
console.log('Production-Updater-3.4-Gate erfolgreich: Zustandsautomat, Grace Period, finaler Head-SHA, Idempotenz, parallele Runs und Payload-Rückschrittschutz geprüft.');