fix: roll back installer migration state

This commit is contained in:
Tom Boucher
2026-05-11 14:40:57 -04:00
parent 4e40ee8b2f
commit 041e94c43b
3 changed files with 121 additions and 1 deletions

View File

@@ -232,6 +232,10 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
actions: [],
};
const rollback = [];
const installStatePath = path.join(configDir, INSTALL_STATE_NAME);
const previousInstallStateBytes = fs.existsSync(installStatePath)
? fs.readFileSync(installStatePath)
: null;
try {
for (const action of plan.actions) {
@@ -285,6 +289,13 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
return {
appliedMigrationIds: journal.appliedMigrationIds,
journalRelPath,
rollback: () => rollbackAppliedMigrationResult({
configDir,
journal,
journalPath,
rollbackRoot,
previousInstallStateBytes,
}),
};
} catch (error) {
const rollbackFailures = [];
@@ -311,6 +322,54 @@ function applyInstallerMigrationPlan({ configDir, plan, now = () => new Date().t
}
}
function rollbackAppliedMigrationResult({ configDir, journal, journalPath, rollbackRoot, previousInstallStateBytes }) {
const failures = [];
for (const action of [...journal.actions].reverse()) {
if (!action.rollbackRelPath) continue;
const rollbackPath = path.join(configDir, action.rollbackRelPath);
const dest = path.join(configDir, action.relPath);
try {
if (fs.existsSync(rollbackPath)) {
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(rollbackPath, dest);
}
} catch (error) {
failures.push({ relPath: action.relPath, error: error.message });
}
if (action.backupRelPath) {
try {
fs.rmSync(path.join(configDir, action.backupRelPath), { force: true });
} catch {
// backup cleanup is best-effort; preserve restore failures above
}
}
}
try {
if (previousInstallStateBytes === null) {
fs.rmSync(path.join(configDir, INSTALL_STATE_NAME), { force: true });
} else {
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(path.join(configDir, INSTALL_STATE_NAME), previousInstallStateBytes);
}
} catch (error) {
failures.push({ relPath: INSTALL_STATE_NAME, error: error.message });
}
try {
fs.rmSync(journalPath, { force: true });
fs.rmSync(rollbackRoot, { recursive: true, force: true });
} catch {
// journal cleanup is best-effort; the rollback above is the safety-critical part
}
if (failures.length > 0) {
const error = new Error('migration rollback incomplete');
error.rollbackFailures = failures;
throw error;
}
}
function runInstallerMigrations({
configDir,
migrationsDir = DEFAULT_MIGRATIONS_DIR,