Skip to content
Closed
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
6 changes: 1 addition & 5 deletions packages/cli/src/deploy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import { randomUUID } from "node:crypto";
import { compile } from "./compile";
import { defaultLogger, type Logger } from "./logger";
import { redirectStdoutToStderr } from "./stdio";
import { runWithCliErrorHandling } from "./run-with-error-handling";

export type DeployCommandOptions = {
json?: boolean;
Expand All@@ -29,8 +28,5 @@ export async function deploy(
const executionId = opts.executionId ?? randomUUID();
logger.info(`Yieldstar execution ${executionId}`);

await runWithCliErrorHandling(
() => deployApp({ entryPoint, emit, executionId }),
{ logger, command: "deploy" },
);
await deployApp({ entryPoint, emit, executionId });
}
6 changes: 1 addition & 5 deletions packages/cli/src/destroy.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,6 @@ import { randomUUID } from "node:crypto";
import { compile } from "./compile";
import { defaultLogger, type Logger } from "./logger";
import { redirectStdoutToStderr } from "./stdio";
import { runWithCliErrorHandling } from "./run-with-error-handling";

export type DestroyCommandOptions = {
json?: boolean;
Expand All@@ -29,8 +28,5 @@ export async function destroy(
const executionId = opts.executionId ?? randomUUID();
logger.info(`Yieldstar execution ${executionId}`);

await runWithCliErrorHandling(
() => destroyApp({ entryPoint, emit, executionId }),
{ logger, command: "destroy" },
);
await destroyApp({ entryPoint, emit, executionId });
}
7 changes: 6 additions & 1 deletion packages/cli/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,8 @@ import { compile } from "./compile";
import { deploy } from "./deploy";
import { destroy } from "./destroy";
import { plan } from "./plan";
import { defaultLogger } from "./logger";
import { runWithCliErrorHandling } from "./run-with-error-handling";
import { visualise } from "./visualise";
import { watch } from "./watch";
import { startDashboardServer } from "@notation/dashboard";
Expand DownExpand Up@@ -77,4 +79,7 @@ program
await watch(entryPoint);
});

program.parse(process.argv);
process.exitCode = await runWithCliErrorHandling(
() => program.parseAsync(process.argv),
{ logger: defaultLogger, command: process.argv[2] ?? program.name() },
);
50 changes: 19 additions & 31 deletions packages/cli/src/plan.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,39 +19,27 @@ const decisionSymbols: Record<PlanNode["decision"], string> = {

export async function plan(entryPoint: string, opts: PlanCommandOptions = {}) {
const logger = opts.logger ?? defaultLogger;
try {
if (opts.json) {
let result: Plan;
const { restore } = redirectStdoutToStderr();
try {
await compile(entryPoint, { logger });
result = await planApp({
entryPoint,
});
} finally {
restore();
}
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
if (opts.json) {
let result: Plan;
const { restore } = redirectStdoutToStderr();
try {
await compile(entryPoint, { logger });
result = await planApp({
entryPoint,
});
} finally {
restore();
}

await compile(entryPoint, { logger });
logger.info(`Planning ${entryPoint}\n`);
const result = await planApp({
entryPoint,
});
printPlanSummary(result, logger);
} catch (err: any) {
if (err.name === "CredentialsProviderError") {
logger.error(
"\nAWS credentials not found.",
"\n\nEnsure you have a default profile set up in ~/.aws/credentials.",
"\n\nIf using another profile run AWS_PROFILE=otherProfile notation plan.\n",
);
process.exit(1);
}
throw err;
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}

await compile(entryPoint, { logger });
logger.info(`Planning ${entryPoint}\n`);
const result = await planApp({
entryPoint,
});
printPlanSummary(result, logger);
}

function printPlanSummary(result: Plan, logger: Logger) {
Expand Down
15 changes: 8 additions & 7 deletions packages/cli/src/run-with-error-handling.ts
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
import type { Logger } from "./logger";

export async function runWithCliErrorHandling(
fn: () => Promise<void>,
fn: () => Promise<unknown>,
opts: { logger: Logger; command: string },
): Promise<void> {
): Promise<0 | 1> {
try {
await fn();
} catch (err: any) {
if (err.name === "CredentialsProviderError") {
return 0;
} catch (error: unknown) {
if (error instanceof Error && error.name === "CredentialsProviderError") {
opts.logger.error(
"\nAWS credentials not found.",
"\n\nEnsure you have a default profile set up in ~/.aws/credentials.",
`\n\nIf using another profile run AWS_PROFILE=otherProfile notation ${opts.command}.\n`,
);
process.exit(1);
return 1;
}
opts.logger.error(err);
process.exit(1);
opts.logger.error(error);
return 1;
}
}
41 changes: 41 additions & 0 deletions packages/cli/test/run-with-error-handling.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
import { describe, expect, it, vi } from "vitest";
import { runWithCliErrorHandling } from "../src/run-with-error-handling";

describe("CLI error handling", () => {
it("reports credential failures with command-specific guidance", async () => {
const error = new Error("Could not load credentials");
error.name = "CredentialsProviderError";
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };

const exitCode = await runWithCliErrorHandling(
async () => {
throw error;
},
{ logger, command: "deploy" },
);

expect(exitCode).toBe(1);
expect(logger.error).toHaveBeenCalledOnce();
expect(logger.error).toHaveBeenCalledWith(
"\nAWS credentials not found.",
"\n\nEnsure you have a default profile set up in ~/.aws/credentials.",
"\n\nIf using another profile run AWS_PROFILE=otherProfile notation deploy.\n",
);
});

it("reports non-credential failures unchanged", async () => {
const error = new Error("deploy failed");
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn() };

const exitCode = await runWithCliErrorHandling(
async () => {
throw error;
},
{ logger, command: "deploy" },
);

expect(exitCode).toBe(1);
expect(logger.error).toHaveBeenCalledOnce();
expect(logger.error).toHaveBeenCalledWith(error);
});
});
2 changes: 1 addition & 1 deletion packages/reconciler/src/logger-subscriber.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ export function createLoggerReconcilerSubscriber(
return;
}

if (event.event === "reconciler.orphan-deletion.skipped") {
if (event.level === "warn") {
logger.warn(event.event, event);
return;
}
Expand Down
14 changes: 13 additions & 1 deletion packages/reconciler/test/logger-subscriber.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,13 @@ describe("logger reconciler subscriber", () => {
resourceId: "resource-2",
resourceType: "test/service/subscriber",
});
await emit({
level: "warn",
event: "reconciler.coordination.waiting",
deploymentId: "deployment-1",
executionId: "execution-2",
holderExecutionId: "execution-1",
});
await emit({
level: "error",
event: "reconciler.operation.lifecycle",
Expand All@@ -39,7 +46,12 @@ describe("logger reconciler subscriber", () => {
});

expect(info).toHaveBeenCalledOnce();
expect(warn).toHaveBeenCalledOnce();
expect(warn).toHaveBeenCalledTimes(2);
expect(warn).toHaveBeenNthCalledWith(
2,
"reconciler.coordination.waiting",
expect.objectContaining({ level: "warn" }),
);
expect(error).toHaveBeenCalledOnce();
});
});
Loading