Skip to content
Draft
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
3 changes: 3 additions & 0 deletions docs/product/output-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ command:
- a non-zero child status is preserved as the process exit code and is
represented by `CLI.CHILD_PROCESS_FAILED`, with `exitCode` and `signal` in
`error.meta`
- a command may attach its own structured error (`exitWithChildStatus({ error })`);
for a child that exited non-zero the JSON result carries it in place of
`CLI.CHILD_PROCESS_FAILED`, keeping the child's exit code and those two `meta` keys

This lets automation consume a command family's structured result without
having to parse the delegated tool's human output.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/error-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ A `ctx.prompt.browserWait` flow (the command opened a URL and polled for the use

### CLI.CHILD_PROCESS_FAILED

Emitted only as a json-mode error envelope when a command that handed the terminal to a child process (`exitWithChildStatus`) saw that child exit non-zero or die on a signal; the run's exit code is the child's own status verbatim, not the CLI's usual 2. Meta: `exitCode`, `signal`.
Emitted only as a json-mode error envelope when a command that handed the terminal to a child process (`exitWithChildStatus`) saw that child exit non-zero or die on a signal; the run's exit code is the child's own status verbatim, not the CLI's usual 2. A command that attaches its own error to that settlement replaces this code for a child that exited non-zero. Meta: `exitCode`, `signal`.

### CLI.COMMAND_MOVED

Expand Down
2 changes: 1 addition & 1 deletion packages/cli-engine/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@prisma/cli-engine",
"version": "0.4.0",
"version": "0.4.1",
"description": "The execution engine of the unified Prisma CLI.",
"type": "module",
"exports": {
Expand Down
22 changes: 21 additions & 1 deletion packages/cli-engine/src/execution/settlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,26 @@ function settleStructuredChildStatus(
});
return;
}
const status = { exitCode: child.exitCode, signal: child.signal };
// A signal-killed child drops the command's error with its next actions.
const attached = child.signal === null ? settlement.error : undefined;
if (attached !== undefined) {
const error = diagnosticOf(attached);
const actions = [...error.nextActions, ...nextActions];
emitErrored(invocation, {
ok: false,
commandId: invocation.state.commandId,
// The engine's record of the child wins over the handler's meta.
error: {
...error,
nextActions: actions,
meta: { ...error.meta, ...status },
},
diagnostics: accompanyingFindings(attached.diagnostics),
nextActions: actions,
});
return;
}
const how =
child.signal === null
? `exited with code ${String(child.exitCode ?? "unknown")}`
Expand All @@ -305,7 +325,7 @@ function settleStructuredChildStatus(
severity: "error",
summary: `The delegated process ${how}.`,
nextActions,
meta: { exitCode: child.exitCode, signal: child.signal },
meta: status,
},
diagnostics: [],
nextActions,
Expand Down
8 changes: 7 additions & 1 deletion packages/cli-engine/src/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* the child-status settlement are built from. The engine never imports
* node:child_process — the bin injects an adapter satisfying SpawnChild.
*/
import type { NextAction } from "./protocol";
import type { CliStructuredError, NextAction } from "./protocol";

/** A fully composed child invocation. `env` is the child's COMPLETE
* environment: the engine has already merged the invocation
Expand Down Expand Up @@ -76,6 +76,7 @@ export const CHILD_STATUS: unique symbol = Symbol.for(
export interface ChildStatusSettlement {
readonly [CHILD_STATUS]: true;
readonly nextActions: readonly NextAction[];
readonly error: CliStructuredError | undefined;
}

export interface ExitWithChildStatusOptions {
Expand All @@ -84,6 +85,10 @@ export interface ExitWithChildStatusOptions {
* a signal-killed child drops these entirely: the user stopped the
* run, so there is nothing to reproduce. */
readonly nextActions?: readonly NextAction[];
/** The command's own structured error for a failed child. It replaces
* CLI.CHILD_PROCESS_FAILED in the json envelope only; the exit code
* stays the child's. Ignored when the child exited 0 or was signalled. */
readonly error?: CliStructuredError;
}

/** Signal numbers shared by Linux, macOS and the BSDs. Numbers that
Expand Down Expand Up @@ -133,6 +138,7 @@ export function exitWithChildStatus(
return Object.freeze({
[CHILD_STATUS]: true as const,
nextActions: Object.freeze([...(options?.nextActions ?? [])]),
error: options?.error,
});
}

Expand Down
71 changes: 70 additions & 1 deletion packages/cli-engine/tests/spawn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
exitWithChildStatus,
type Runtime,
} from "@prisma/cli-engine";
import { ok } from "@prisma/cli-engine/protocol";
import { CliStructuredError, ok } from "@prisma/cli-engine/protocol";
import {
createTestCli,
mintTestJwt,
Expand Down Expand Up @@ -1423,6 +1423,75 @@ describe("next actions on a child-status settlement", () => {
});
});

describe("a structured error on a child-status settlement", () => {
const deploy = defineCommand({
help: { summary: "A converge that knows why its child failed" },
maySpawn: true,
handler: async (_args, ctx) => {
await ctx.spawn({ command: "alchemy" });
return ok(
exitWithChildStatus({
error: new CliStructuredError("DEPLOY.ENGINE_FAILED", "Failed.", {
meta: { stackFilePath: "/app/stack.ts", exitCode: 99 },
}),
}),
);
},
});

async function settle(child: {
readonly exitCode: number | null;
readonly signal: string | null;
}) {
const cli = createTestCli({
commands: { deploy },
now: CLOCK,
spawnScript: () => child,
});
return cli.run(["deploy", "--json"]);
}

test("json carries the command's error and exits with the child's code", async () => {
const result = await settle({ exitCode: 3, signal: null });

expect(result.exitCode).toBe(3);
expect(result.json.at(-1)).toMatchObject({
envelope: {
ok: false,
error: {
code: "DEPLOY.ENGINE_FAILED",
summary: "Failed.",
meta: { stackFilePath: "/app/stack.ts", exitCode: 3, signal: null },
},
},
});
});

test("a signal-killed child is still CLI.CHILD_PROCESS_FAILED", async () => {
const result = await settle({ exitCode: null, signal: "SIGINT" });

expect(result.exitCode).toBe(130);
expect(result.json.at(-1)).toMatchObject({
envelope: {
ok: false,
error: {
code: "CLI.CHILD_PROCESS_FAILED",
meta: { exitCode: null, signal: "SIGINT" },
},
},
});
});

test("a child that exited 0 settles ok", async () => {
const result = await settle({ exitCode: 0, signal: null });

expect(result.exitCode).toBe(0);
expect(result.json.at(-1)).toMatchObject({
envelope: { ok: true, result: null, exitCode: 0 },
});
});
});

describe("unknown terminations are never success", () => {
test("an adapter that cannot say how the child ended settles 1", async () => {
const cli = createTestCli({
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
},
"dependencies": {
"@manypkg/tools": "^2.1.2",
"@prisma/cli-engine": "workspace:0.4.0",
"@prisma/cli-engine": "workspace:0.4.1",
"@prisma/composer-cli": "0.20.0",
"@prisma/compute-sdk": "0.42.0",
"@prisma/management-api-sdk": "1.69.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/prisma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
},
"dependencies": {
"@manypkg/tools": "^2.1.2",
"@prisma/cli-engine": "workspace:0.4.0",
"@prisma/cli-engine": "workspace:0.4.1",
"@prisma/composer-cli": "0.20.0",
"@prisma/compute-sdk": "0.42.0",
"@prisma/management-api-sdk": "1.69.0",
Expand Down
4 changes: 2 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading