feat(up): improve error handling and presentation

This commit is contained in:
Joakim Carlstein 2023-11-22 15:47:17 +01:00
parent b57c86eaab
commit 8347fc1fa4
6 changed files with 83 additions and 29 deletions

View file

@ -1,3 +1,4 @@
import path from 'node:path';
import process from 'node:process';
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
import {
@ -61,19 +62,30 @@ export default async function upCommand({
await reporter.onInit?.({ command: 'up', cwd, dry, directory });
const migrationFiles = await getMigrations(cwd, directory);
let migrationHistoryError: MigrationHistoryError | undefined;
const failedEntries: MigrationMetadataFinished[] = [];
for await (const migrationHistoryEntry of storage.getHistory()) {
if (migrationHistoryEntry.status === 'failed') {
migrationHistoryError = new MigrationHistoryError(
`Migration ${migrationHistoryEntry.name} is in a failed state, please fix it first`,
migrationHistoryEntry,
);
}
const index = migrationFiles.findIndex((migrationFile) => migrationFile.name === migrationHistoryEntry.name);
if (migrationHistoryEntry.status === 'failed') {
const filePath = path.resolve(cwd, directory, migrationHistoryEntry.name);
const finishedMigration: MigrationMetadataFinished = {
name: migrationHistoryEntry.name,
status: migrationHistoryEntry.status,
filePath,
relativeFilePath: path.relative(cwd, filePath),
extension: withLeadingPeriod(path.extname(migrationHistoryEntry.name)),
error: new MigrationHistoryError(
`Migration ${migrationHistoryEntry.name} is in a failed state, please fix it first`,
migrationHistoryEntry,
),
directory,
cwd,
duration: 0,
};
failedEntries.push(finishedMigration);
}
if (index !== -1) {
migrationFiles.splice(index, 1);
}
@ -100,22 +112,29 @@ export default async function upCommand({
}
}
await reporter.onCollectedMigrations?.(migrationFiles);
await reporter.onCollectedMigrations?.([...failedEntries, ...migrationFiles]);
if (migrationFiles.length === 0 || dry || migrationHistoryError) {
if (migrationFiles.length === 0 || dry || failedEntries.length > 0) {
const error = failedEntries.find((migration) => migration.status === 'failed')?.error;
await reporter.onLockedMigrations?.(migrationFiles);
const finishedMigrations: MigrationMetadataFinished[] = migrationFiles.map((migration) => ({
...migration,
duration: 0,
status: 'skipped',
status: 'pending',
}));
for await (const failedMigration of failedEntries) {
await reporter.onMigrationError?.(failedMigration, failedMigration.error!);
}
for await (const migration of finishedMigrations) {
await reporter.onMigrationSkip?.(migration);
}
await reporter.onFinished?.(finishedMigrations, migrationHistoryError);
await reporter.onFinished?.([...failedEntries, ...finishedMigrations], error);
process.exitCode = failedEntries.length > 0 ? 1 : 0;
return;
}
@ -197,14 +216,7 @@ export default async function upCommand({
finishedMigrations.push(finishedMigration);
} catch (error) {
let errorInstance = error instanceof Error ? error : new Error(String(error));
if (!(errorInstance instanceof EmigrateError)) {
errorInstance = new MigrationRunError(`Failed to run migration: ${migration.relativeFilePath}`, migration, {
cause: error,
});
}
const errorInstance = error instanceof Error ? error : new Error(String(error));
const duration = getDuration(start);
const finishedMigration: MigrationMetadataFinished = {
...migration,
@ -220,9 +232,19 @@ export default async function upCommand({
}
}
} finally {
const firstError = finishedMigrations.find((migration) => migration.status === 'failed')?.error;
const firstFailed = finishedMigrations.find((migration) => migration.status === 'failed');
const firstError =
firstFailed?.error instanceof EmigrateError
? firstFailed.error
: firstFailed
? new MigrationRunError(`Failed to run migration: ${firstFailed.relativeFilePath}`, firstFailed, {
cause: firstFailed?.error,
})
: undefined;
await cleanup();
await reporter.onFinished?.(finishedMigrations, firstError);
process.exitCode = firstError ? 1 : 0;
}
}

View file

@ -207,7 +207,10 @@ const getSummary = (
return ` ${statusLine}${showTotal ? gray(` (${total} total)`) : ''}`;
};
const getHeaderMessage = (migrations?: MigrationMetadata[], lockedMigrations?: MigrationMetadata[]) => {
const getHeaderMessage = (
migrations?: Array<MigrationMetadata | MigrationMetadataFinished>,
lockedMigrations?: Array<MigrationMetadata | MigrationMetadataFinished>,
) => {
if (!migrations || !lockedMigrations) {
return '';
}
@ -220,13 +223,22 @@ const getHeaderMessage = (migrations?: MigrationMetadata[], lockedMigrations?: M
return ` ${bold(migrations.length.toString())} ${dim('pending migrations to run')}`;
}
if (lockedMigrations.length === 0) {
return ` ${bold(`0 of ${migrations.length}`)} ${dim('pending migrations to run')} ${redBright('(all locked)')}`;
}
const nonLockedMigrations = migrations.filter(
(migration) => !lockedMigrations.some((lockedMigration) => lockedMigration.name === migration.name),
);
const failedMigrations = nonLockedMigrations.filter(
(migration) => 'status' in migration && migration.status === 'failed',
);
const unlockableCount = nonLockedMigrations.length - failedMigrations.length;
return ` ${bold(`${lockedMigrations.length} of ${migrations.length}`)} ${dim('pending migrations to run')} ${yellow(
`(${migrations.length - lockedMigrations.length} locked)`,
)}`;
const parts = [
bold(`${lockedMigrations.length} of ${migrations.length}`),
dim`pending migrations to run`,
unlockableCount > 0 ? yellow(`(${unlockableCount} locked)`) : '',
failedMigrations.length > 0 ? redBright(`(${failedMigrations.length} failed)`) : '',
].filter(Boolean);
return ` ${parts.join(' ')}`;
};
class DefaultFancyReporter implements Required<EmigrateReporter> {