Noticed while building the migration-9 refusal in #388, and left alone there deliberately because it is pre-existing behavior for any throwing migration rather than anything that migration introduced.
runMigrations logs the error and then rethrows, so the caller's own handler prints the same text again:
packages/cli/src/filesystem/migrate.ts:498
} catch (error) {
console.error(
`Migration ${String(v)} failed: ${error instanceof Error ? error.message : String(error)}`
);
A user upgrading a project whose rule id is held by two engines sees the whole refusal, which is a paragraph naming both directories, the shared sidecar and the rename to perform, printed once under a Migration 9 failed: prefix and then once more on its own. The longer and more useful the message, the worse it reads.
This matters more now than it did, because migration 9 is the first migration whose failure is a deliberate, user-facing instruction rather than an unexpected fault. Before it, a migration throwing meant something had gone wrong and a doubled stack-adjacent line was noise nobody was reading carefully.
What to do
- Decide which layer owns the user-facing print. The
console.error in runMigrations predates coded CLIErrors carrying their own presentation, and a CLIError that reaches the top level is already reported.
- Most likely fix: let a
CLIError through without the local console.error, and keep the local log only for an unrecognized error, where the Migration N failed: prefix is the only thing naming which migration threw.
- Whatever the shape, keep the migration number visible. A user reporting the problem should be able to say which migration refused without reading the code.
- Do not change the manifest-writing behavior in the same
catch (the "write at last successful version so completed migrations do not re-run" logic at migrate.ts:502-506). That is load-bearing and unrelated.
Refs #387
Noticed while building the migration-9 refusal in #388, and left alone there deliberately because it is pre-existing behavior for any throwing migration rather than anything that migration introduced.
runMigrationslogs the error and then rethrows, so the caller's own handler prints the same text again:A user upgrading a project whose rule id is held by two engines sees the whole refusal, which is a paragraph naming both directories, the shared sidecar and the rename to perform, printed once under a
Migration 9 failed:prefix and then once more on its own. The longer and more useful the message, the worse it reads.This matters more now than it did, because migration 9 is the first migration whose failure is a deliberate, user-facing instruction rather than an unexpected fault. Before it, a migration throwing meant something had gone wrong and a doubled stack-adjacent line was noise nobody was reading carefully.
What to do
console.errorinrunMigrationspredates codedCLIErrors carrying their own presentation, and aCLIErrorthat reaches the top level is already reported.CLIErrorthrough without the localconsole.error, and keep the local log only for an unrecognized error, where theMigration N failed:prefix is the only thing naming which migration threw.catch(the "write at last successful version so completed migrations do not re-run" logic atmigrate.ts:502-506). That is load-bearing and unrelated.Refs #387