feat(up): improve error handling and presentation
This commit is contained in:
parent
b57c86eaab
commit
8347fc1fa4
6 changed files with 83 additions and 29 deletions
5
.changeset/angry-chicken-thank.md
Normal file
5
.changeset/angry-chicken-thank.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Return a non zero exit code in case a migration fails (or for a dry-run if there's a failed migration in the history)
|
||||||
5
.changeset/cold-points-rule.md
Normal file
5
.changeset/cold-points-rule.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Show any failed migration from the history in the "up" dry-run output
|
||||||
5
.changeset/giant-files-serve.md
Normal file
5
.changeset/giant-files-serve.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Don't pass the EmigrateError instance to the storage for each failed migration but only the real cause. This is so that errors from failed migrations are not wrapped twice in EmigrateError instances when presenting failed migrations during an "up" dry-run or the "list" command.
|
||||||
5
.changeset/happy-yaks-impress.md
Normal file
5
.changeset/happy-yaks-impress.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@emigrate/cli': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve the looks of the "up" dry-run default output by showing pending migrations in a different color
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
|
||||||
import {
|
import {
|
||||||
|
|
@ -61,19 +62,30 @@ export default async function upCommand({
|
||||||
await reporter.onInit?.({ command: 'up', cwd, dry, directory });
|
await reporter.onInit?.({ command: 'up', cwd, dry, directory });
|
||||||
|
|
||||||
const migrationFiles = await getMigrations(cwd, directory);
|
const migrationFiles = await getMigrations(cwd, directory);
|
||||||
|
const failedEntries: MigrationMetadataFinished[] = [];
|
||||||
let migrationHistoryError: MigrationHistoryError | undefined;
|
|
||||||
|
|
||||||
for await (const migrationHistoryEntry of storage.getHistory()) {
|
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);
|
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) {
|
if (index !== -1) {
|
||||||
migrationFiles.splice(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);
|
await reporter.onLockedMigrations?.(migrationFiles);
|
||||||
|
|
||||||
const finishedMigrations: MigrationMetadataFinished[] = migrationFiles.map((migration) => ({
|
const finishedMigrations: MigrationMetadataFinished[] = migrationFiles.map((migration) => ({
|
||||||
...migration,
|
...migration,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
status: 'skipped',
|
status: 'pending',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
for await (const failedMigration of failedEntries) {
|
||||||
|
await reporter.onMigrationError?.(failedMigration, failedMigration.error!);
|
||||||
|
}
|
||||||
|
|
||||||
for await (const migration of finishedMigrations) {
|
for await (const migration of finishedMigrations) {
|
||||||
await reporter.onMigrationSkip?.(migration);
|
await reporter.onMigrationSkip?.(migration);
|
||||||
}
|
}
|
||||||
|
|
||||||
await reporter.onFinished?.(finishedMigrations, migrationHistoryError);
|
await reporter.onFinished?.([...failedEntries, ...finishedMigrations], error);
|
||||||
|
|
||||||
|
process.exitCode = failedEntries.length > 0 ? 1 : 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,14 +216,7 @@ export default async function upCommand({
|
||||||
|
|
||||||
finishedMigrations.push(finishedMigration);
|
finishedMigrations.push(finishedMigration);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let errorInstance = error instanceof Error ? error : new Error(String(error));
|
const 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 duration = getDuration(start);
|
const duration = getDuration(start);
|
||||||
const finishedMigration: MigrationMetadataFinished = {
|
const finishedMigration: MigrationMetadataFinished = {
|
||||||
...migration,
|
...migration,
|
||||||
|
|
@ -220,9 +232,19 @@ export default async function upCommand({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} 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 cleanup();
|
||||||
await reporter.onFinished?.(finishedMigrations, firstError);
|
await reporter.onFinished?.(finishedMigrations, firstError);
|
||||||
|
|
||||||
|
process.exitCode = firstError ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,10 @@ const getSummary = (
|
||||||
return ` ${statusLine}${showTotal ? gray(` (${total} total)`) : ''}`;
|
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) {
|
if (!migrations || !lockedMigrations) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -220,13 +223,22 @@ const getHeaderMessage = (migrations?: MigrationMetadata[], lockedMigrations?: M
|
||||||
return ` ${bold(migrations.length.toString())} ${dim('pending migrations to run')}`;
|
return ` ${bold(migrations.length.toString())} ${dim('pending migrations to run')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lockedMigrations.length === 0) {
|
const nonLockedMigrations = migrations.filter(
|
||||||
return ` ${bold(`0 of ${migrations.length}`)} ${dim('pending migrations to run')} ${redBright('(all locked)')}`;
|
(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(
|
const parts = [
|
||||||
`(${migrations.length - lockedMigrations.length} locked)`,
|
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> {
|
class DefaultFancyReporter implements Required<EmigrateReporter> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue