Skip to content

Commit

Permalink
feat(up): improve error handling and presentation
Browse files Browse the repository at this point in the history
  • Loading branch information
joakimbeng committed Nov 22, 2023
1 parent b57c86e commit 8347fc1
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 28 deletions.
5 changes: 5 additions & 0 deletions .changeset/angry-chicken-thank.md
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions .changeset/cold-points-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@emigrate/cli': minor
---

Show any failed migration from the history in the "up" dry-run output
5 changes: 5 additions & 0 deletions .changeset/giant-files-serve.md
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions .changeset/happy-yaks-impress.md
Original file line number Diff line number Diff line change
@@ -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
64 changes: 43 additions & 21 deletions packages/cli/src/commands/up.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from 'node:path';
import process from 'node:process';
import { getOrLoadPlugins, getOrLoadReporter, getOrLoadStorage } from '@emigrate/plugin-tools';
import {
Expand Down Expand Up @@ -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()) {
const index = migrationFiles.findIndex((migrationFile) => migrationFile.name === migrationHistoryEntry.name);

if (migrationHistoryEntry.status === 'failed') {
migrationHistoryError = new MigrationHistoryError(
`Migration ${migrationHistoryEntry.name} is in a failed state, please fix it first`,
migrationHistoryEntry,
);
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);
}

const index = migrationFiles.findIndex((migrationFile) => migrationFile.name === migrationHistoryEntry.name);

if (index !== -1) {
migrationFiles.splice(index, 1);
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
}
26 changes: 19 additions & 7 deletions packages/cli/src/reporters/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
}
Expand All @@ -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;

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 ` ${bold(`${lockedMigrations.length} of ${migrations.length}`)} ${dim('pending migrations to run')} ${yellow(
`(${migrations.length - lockedMigrations.length} locked)`,
)}`;
return ` ${parts.join(' ')}`;
};

class DefaultFancyReporter implements Required<EmigrateReporter> {
Expand Down

0 comments on commit 8347fc1

Please sign in to comment.