feat: release toolbox portal 0.1.0
This commit is contained in:
84
scripts/publication.mjs
Normal file
84
scripts/publication.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user