bookworm-smart-assistant/hooks/rollback-on-fail.js
Bookworm Admin b7a8e29d21 release: v6.7.0 - OTA E2E test release
- VERSION file as authoritative version source
- export.mjs reads VERSION with package.json fallback
- bw-ota.ps1 DryRun mode for safe testing
- auto-setup.ps1 bumped to v3.2.0 (Phase 8 OTA)
2026-04-27 17:59:44 +08:00

100 lines
3.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* patch-c2-exit-code-normalize:v1 */
#!/usr/bin/env node
/**
* rollback-on-fail.js · Phase α 冲刺 3 · 2026-04-25
* Restore original file from file-history, move staging to quarantine.
* Called only in enforce mode by staging-validator on failure.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const ROOT = path.resolve(__dirname, '..');
const FILE_HISTORY = path.join(ROOT, 'file-history');
const PIPELINE_DIR = path.join(ROOT, 'ai-delivery-pipeline');
const QUARANTINE_DIR = path.join(PIPELINE_DIR, 'quarantine');
const MANIFEST = path.join(PIPELINE_DIR, 'manifest.jsonl');
function appendManifest(entry) {
try { fs.appendFileSync(MANIFEST, JSON.stringify(entry) + '\n', 'utf8'); } catch (_) {}
}
function ensureDir(d) { try { fs.mkdirSync(d, { recursive: true }); } catch (_) {} }
function sha256(buf) { return crypto.createHash('sha256').update(buf).digest('hex'); }
function findLatestHistorySnapshot(originalPath) {
if (!fs.existsSync(FILE_HISTORY)) return null;
const baseName = path.basename(originalPath);
const candidates = [];
try {
const walk = (dir) => {
for (const name of fs.readdirSync(dir)) {
const p = path.join(dir, name);
let s; try { s = fs.statSync(p); } catch { continue; }
if (s.isDirectory()) { walk(p); continue; }
if (name.startsWith(baseName)) candidates.push({ path: p, mtime: s.mtimeMs });
}
};
walk(FILE_HISTORY);
} catch (_) {}
if (candidates.length === 0) return null;
candidates.sort((a, b) => b.mtime - a.mtime);
return candidates[0].path;
}
function moveToQuarantine(stagingPath, hash) {
const today = new Date().toISOString().slice(0, 10);
const bucket = path.join(QUARANTINE_DIR, today);
ensureDir(bucket);
const target = path.join(bucket, hash + '_' + path.basename(stagingPath));
try { fs.renameSync(stagingPath, target); return target; }
catch (e) {
try { fs.copyFileSync(stagingPath, target); fs.unlinkSync(stagingPath); return target; }
catch (_) { return null; }
}
}
function atomicRestore(sourcePath, targetPath) {
const tmp = targetPath + '.rollback.tmp.' + process.pid;
try {
fs.copyFileSync(sourcePath, tmp);
const fd = fs.openSync(tmp, 'r+');
try { fs.fsyncSync(fd); } catch (_) {}
try { fs.closeSync(fd); } catch (_) {}
fs.renameSync(tmp, targetPath);
return true;
} catch (_) {
try { fs.unlinkSync(tmp); } catch (_) {}
return false;
}
}
function main() {
const [, , stagingPath, originalPath, hash, failuresJson] = process.argv;
if (!stagingPath || !originalPath || !hash) {
appendManifest({ ts: new Date().toISOString(), event: 'rollback-bad-args' });
process.exit(2);
}
let failures = [];
try { failures = JSON.parse(failuresJson || '[]'); } catch (_) {}
const historySnap = findLatestHistorySnapshot(originalPath);
if (!historySnap) {
appendManifest({
ts: new Date().toISOString(), event: 'rollback-skip-no-history',
originalPath, hash, note: 'no file-history snapshot, conservative no-op',
});
process.exit(0);
}
const restored = atomicRestore(historySnap, originalPath);
let restoredHash = null;
if (restored) { try { restoredHash = sha256(fs.readFileSync(originalPath)); } catch (_) {} }
const quarantined = moveToQuarantine(stagingPath, hash);
appendManifest({
ts: new Date().toISOString(),
event: restored ? 'rolled-back' : 'rollback-failed',
originalPath, historySnap, quarantinedTo: quarantined, restoredHash, hash, failures,
});
process.exit(restored ? 0 : 1);
}
try { main(); } catch (e) {
appendManifest({ ts: new Date().toISOString(), event: 'rollback-crash', error: String(e).slice(0, 200) });
process.exit(2);
}