Files
toolbox-portal/scripts/publication.mjs

85 lines
2.4 KiB
JavaScript

import { randomUUID } from 'node:crypto';
import { access, rename, rm } from 'node:fs/promises';
import path from 'node:path';
async function exists(candidate) {
return access(candidate).then(
() => true,
() => false
);
}
function backupPath(target) {
return path.join(
path.dirname(target),
`.${path.basename(target)}.toolbox-backup-${randomUUID()}`
);
}
export async function publishStagedTargets(
targets,
{ force = false, renamePath = rename, removePath = rm } = {}
) {
if (!Array.isArray(targets) || targets.length === 0)
throw new Error('At least one staged publication target is required.');
const normalized = targets.map(({ staged, target }) => ({
staged: path.resolve(staged),
target: path.resolve(target),
}));
if (
new Set(normalized.map(({ target }) => target)).size !== normalized.length
)
throw new Error('Publication targets must be unique.');
for (const item of normalized) {
if (!(await exists(item.staged)))
throw new Error(`Staged publication input is missing: ${item.staged}`);
if (!force && (await exists(item.target)))
throw new Error(`Publication target already exists: ${item.target}`);
}
const backups = [];
const published = [];
try {
for (const item of normalized) {
if (!(await exists(item.target))) continue;
const backup = backupPath(item.target);
await renamePath(item.target, backup);
backups.push({ target: item.target, backup });
}
for (const item of normalized) {
await renamePath(item.staged, item.target);
published.push(item.target);
}
} catch (publicationError) {
const rollbackErrors = [];
for (const target of [...published].reverse()) {
try {
await removePath(target, { recursive: true, force: true });
} catch (error) {
rollbackErrors.push(error);
}
}
for (const item of [...backups].reverse()) {
try {
await renamePath(item.backup, item.target);
} catch (error) {
rollbackErrors.push(error);
}
}
if (rollbackErrors.length > 0) {
throw new AggregateError(
[publicationError, ...rollbackErrors],
'Release publication failed and rollback was incomplete.'
);
}
throw publicationError;
}
for (const { backup } of backups) {
await removePath(backup, { recursive: true, force: true });
}
}