Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/21434-migrate-json-exit-signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@objectstack/cli': patch
---

fix(cli): `os migrate recorded-by`, `resume` and `account-issuer` print exactly one `--json` document, and a completed run exits 0 (#21434)

Clause-②: no

`os migrate recorded-by --apply --yes --json` converted the rows, printed its result, then printed a second document, `{"error":"EEXIT: 0","duration":…}`, and exited 1. A script that read the exit status took the completed run for a failure, and a parser that read stdout failed on the second document. The cause was the command's own `catch`: the `this.exit(…)` inside its `try` throws oclif's exit signal, and the `catch` reported the signal as an error.

The same `catch` sat in three more commands:

- **`os migrate resume --run <id> --json`.** A run that was already concluded printed a second `{"error":"EEXIT: 0"}` and exited 1 instead of 0. A resumed run did the same. Every refusal inside the command (unknown run id, plan not loaded, confirmation required) printed a second `{"error":"EEXIT: 1"}` under its own document.
- **`os migrate account-issuer --json`.** A refused pre-flight printed a second `{"error":"EEXIT: 1"}` under its report. Without `--json`, it printed an extra `EEXIT: 1` error line.
- **`os migrate apply`** (text output). A `sys_account.issuer` pre-flight refusal printed an extra `EEXIT: 1` error line.

Each command now prints one document and exits with the status it computes. A completed `recorded-by --apply` and an already-concluded or resumed `resume --run` exit 0. Refusals and failed runs still exit 1. A script that worked around the second document or the exit status 1 can drop that workaround.
5 changes: 5 additions & 0 deletions packages/cli/src/commands/migrate/account-issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
createTimer,
emitJson,
errorCodeFields,
isExitSignal,
} from '../../utils/format.js';
import { bootSchemaStack } from '../../utils/schema-migrate.js';

Expand Down Expand Up @@ -161,6 +162,10 @@ export default class MigrateAccountIssuer extends Command {
);
this.exit(1);
} catch (error: any) {
// [#21434] The `this.exit(1)` calls above throw oclif's exit signal from
// inside this `try`; re-reporting it printed a second `--json` document
// (`{"error":"EEXIT: 1"}`) after the refusal report.
if (isExitSignal(error)) throw error;
// A refusal from the probe itself (unreadable table, truncated scan)
// lands here and stays a refusal — it is never softened into a clean run.
if (flags.json) {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/commands/migrate/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
createTimer,
emitJson,
errorCodeFields,
isExitSignal,
} from '../../utils/format.js';
import {
bootSchemaStack,
Expand Down Expand Up @@ -447,6 +448,10 @@ export default class MigrateApply extends Command {
console.log(chalk.dim(` ${timer.display()}`));
console.log('');
} catch (error: any) {
// [#21434] The account-issuer pre-flight refusals above `this.exit(1)`
// from inside this `try`; re-reporting the signal printed a second
// error line, `EEXIT: 1`, under the refusal.
if (isExitSignal(error)) throw error;
if (flags.json) { await emitJson({ error: error.message, ...errorCodeFields(error) }, 0, { compact: true }); this.exit(1); }
printError(error.message || String(error));
this.exit(1);
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/commands/migrate/recorded-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
createTimer,
emitJson,
errorCodeFields,
isExitSignal,
} from '../../utils/format.js';
import { bootSchemaStack } from '../../utils/schema-migrate.js';
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
Expand Down Expand Up @@ -193,6 +194,10 @@ export default class MigrateRecordedBy extends Command {
this.exit(1);
}
} catch (error: any) {
// [#21434] The `this.exit(…)` calls above throw oclif's exit signal from
// inside this `try`. Re-reporting it printed a second `--json` document
// (`{"error":"EEXIT: 0"}`) and turned a completed apply's exit 0 into 1.
if (isExitSignal(error)) throw error;
const msg = error instanceof MigrationJournalRefusal
? `Refused (${error.code}): ${error.message}`
: (error?.message || String(error));
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/commands/migrate/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
createTimer,
emitJson,
errorCodeFields,
isExitSignal,
} from '../../utils/format.js';
import { bootSchemaStack } from '../../utils/schema-migrate.js';
import { buildDataMigrationPlugins } from '../../utils/data-migration-plugins.js';
Expand Down Expand Up @@ -238,6 +239,11 @@ export default class MigrateResume extends Command {
this.exit(1);
}
} catch (error: any) {
// [#21434] The `this.exit(…)` calls above throw oclif's exit signal from
// inside this `try`. Re-reporting it printed a second `--json` document
// and turned every exit 0 above (a resumed run, an already-concluded
// run) into exit 1.
if (isExitSignal(error)) throw error;
const msg = error instanceof MigrationJournalRefusal
// A refusal is the runner working, not breaking — say what it refused.
? `Refused (${error.code}): ${error.message}`
Expand Down
Loading
Loading