171 lines
11 KiB
JavaScript
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.');
|