Skip to content

Commit 8c4df32

Browse files
authored
v3: Better dev/deploy errors and handle image build errors (#988)
* Better error output when an ESM only package is required * Add better errors for npm package errors, and bring deploy errors to the dev CLI * Handle depot build errors and write out a temporary file to view the build logs * Add changeset
1 parent 11b997d commit 8c4df32

File tree

7 files changed

+322
-54
lines changed

7 files changed

+322
-54
lines changed

.changeset/mighty-camels-joke.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Improve error messages during dev/deploy and handle deploy image build issues

.vscode/launch.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"type": "node-terminal",
4242
"request": "launch",
4343
"name": "Debug V3 Deploy CLI",
44-
"command": "pnpm exec trigger.dev deploy",
44+
"command": "pnpm exec trigger.dev deploy --skip-deploy",
4545
"cwd": "${workspaceFolder}/references/v3-catalog",
4646
"sourceMaps": true
4747
},

packages/cli-v3/src/cli/common.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getTracer, provider } from "../telemetry/tracing";
55
import { fromZodError } from "zod-validation-error";
66
import { logger } from "../utilities/logger";
77
import { outro } from "@clack/prompts";
8+
import { chalkError } from "../utilities/cliOutput";
89

910
export const CommonCommandOptions = z.object({
1011
apiUrl: z.string().optional(),
@@ -84,7 +85,8 @@ export async function wrapCommandAction<T extends z.AnyZodObject, TResult>(
8485
// do nothing
8586
} else {
8687
recordSpanException(span, e);
87-
logger.error(e instanceof Error ? e.message : String(e));
88+
89+
logger.log(`${chalkError("X Error:")} ${e instanceof Error ? e.message : String(e)}`);
8890
}
8991

9092
span.end();

packages/cli-v3/src/commands/deploy.ts

Lines changed: 118 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,15 @@ import { logger } from "../utilities/logger.js";
4343
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
4444
import { login } from "./login";
4545

46+
import { Glob } from "glob";
4647
import type { SetOptional } from "type-fest";
4748
import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../utilities/build";
48-
import { Glob } from "glob";
49+
import { chalkError, chalkPurple, chalkWarning } from "../utilities/cliOutput";
50+
import {
51+
logESMRequireError,
52+
parseBuildErrorStack,
53+
parseNpmInstallError,
54+
} from "../utilities/deployErrors";
4955

5056
const DeployCommandOptions = CommonCommandOptions.extend({
5157
skipTypecheck: z.boolean().default(false),
@@ -272,28 +278,44 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
272278
);
273279
}
274280

275-
return buildAndPushImage({
276-
registryHost,
277-
auth: authorization.auth.accessToken,
278-
imageTag: deploymentResponse.data.imageTag,
279-
buildId: deploymentResponse.data.externalBuildData.buildId,
280-
buildToken: deploymentResponse.data.externalBuildData.buildToken,
281-
buildProjectId: deploymentResponse.data.externalBuildData.projectId,
282-
cwd: compilation.path,
283-
projectId: resolvedConfig.config.project,
284-
deploymentId: deploymentResponse.data.id,
285-
deploymentVersion: deploymentResponse.data.version,
286-
contentHash: deploymentResponse.data.contentHash,
287-
projectRef: resolvedConfig.config.project,
288-
loadImage: options.loadImage,
289-
buildPlatform: options.buildPlatform,
290-
});
281+
return buildAndPushImage(
282+
{
283+
registryHost,
284+
auth: authorization.auth.accessToken,
285+
imageTag: deploymentResponse.data.imageTag,
286+
buildId: deploymentResponse.data.externalBuildData.buildId,
287+
buildToken: deploymentResponse.data.externalBuildData.buildToken,
288+
buildProjectId: deploymentResponse.data.externalBuildData.projectId,
289+
cwd: compilation.path,
290+
projectId: resolvedConfig.config.project,
291+
deploymentId: deploymentResponse.data.id,
292+
deploymentVersion: deploymentResponse.data.version,
293+
contentHash: deploymentResponse.data.contentHash,
294+
projectRef: resolvedConfig.config.project,
295+
loadImage: options.loadImage,
296+
buildPlatform: options.buildPlatform,
297+
},
298+
deploymentSpinner
299+
);
291300
};
292301

293302
const image = await buildImage();
294303

295304
if (!image.ok) {
296-
deploymentSpinner.stop(`Failed to build project image: ${image.error}`);
305+
deploymentSpinner.stop(`Failed to build project.`);
306+
307+
// If there are logs, let's write it out to a temporary file and include the path in the error message
308+
if (image.logs.trim() !== "") {
309+
const logPath = join(await createTempDir(), `build-${deploymentResponse.data.shortCode}.log`);
310+
311+
await writeFile(logPath, image.logs);
312+
313+
logger.log(
314+
`${chalkError("X Error:")} ${image.error}. Full build logs have been saved to ${logPath})`
315+
);
316+
} else {
317+
logger.log(`${chalkError("X Error:")} ${image.error}.`);
318+
}
297319

298320
throw new SkipLoggingError(`Failed to build project image: ${image.error}`);
299321
}
@@ -379,10 +401,19 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
379401
}
380402
case "FAILED": {
381403
if (finishedDeployment.errorData) {
382-
deploymentSpinner.stop(
383-
`Deployment encountered an error: ${finishedDeployment.errorData.name}. ${deploymentLink}`
384-
);
385-
logger.error(finishedDeployment.errorData.stack);
404+
const parsedError = finishedDeployment.errorData.stack
405+
? parseBuildErrorStack(finishedDeployment.errorData)
406+
: finishedDeployment.errorData.message;
407+
408+
if (typeof parsedError === "string") {
409+
deploymentSpinner.stop(`Deployment encountered an error. ${deploymentLink}`);
410+
411+
logger.log(`${chalkError("X Error:")} ${parsedError}`);
412+
} else {
413+
deploymentSpinner.stop(`Deployment encountered an error. ${deploymentLink}`);
414+
415+
logESMRequireError(parsedError, resolvedConfig);
416+
}
386417

387418
throw new SkipLoggingError(
388419
`Deployment encountered an error: ${finishedDeployment.errorData.name}`
@@ -551,15 +582,18 @@ type BuildAndPushImageResults =
551582
| {
552583
ok: true;
553584
image: string;
585+
logs: string;
554586
digest?: string;
555587
}
556588
| {
557589
ok: false;
558590
error: string;
591+
logs: string;
559592
};
560593

561594
async function buildAndPushImage(
562-
options: BuildAndPushImageOptions
595+
options: BuildAndPushImageOptions,
596+
updater: ReturnType<typeof spinner>
563597
): Promise<BuildAndPushImageResults> {
564598
return tracer.startActiveSpan("buildAndPushImage", async (span) => {
565599
span.setAttributes({
@@ -626,7 +660,7 @@ async function buildAndPushImage(
626660
const errors: string[] = [];
627661

628662
try {
629-
await new Promise<void>((res, rej) => {
663+
const processCode = await new Promise<number | null>((res, rej) => {
630664
// For some reason everything is output on stderr, not stdout
631665
childProcess.stderr?.on("data", (data: Buffer) => {
632666
const text = data.toString();
@@ -636,9 +670,19 @@ async function buildAndPushImage(
636670
});
637671

638672
childProcess.on("error", (e) => rej(e));
639-
childProcess.on("close", () => res());
673+
childProcess.on("close", (code) => res(code));
640674
});
641675

676+
const logs = extractLogs(errors);
677+
678+
if (processCode !== 0) {
679+
return {
680+
ok: false as const,
681+
error: `Error building image`,
682+
logs,
683+
};
684+
}
685+
642686
const digest = extractImageDigest(errors);
643687

644688
span.setAttributes({
@@ -650,6 +694,7 @@ async function buildAndPushImage(
650694
return {
651695
ok: true as const,
652696
image: options.imageTag,
697+
logs,
653698
digest,
654699
};
655700
} catch (e) {
@@ -659,6 +704,7 @@ async function buildAndPushImage(
659704
return {
660705
ok: false as const,
661706
error: e instanceof Error ? e.message : JSON.stringify(e),
707+
logs: extractLogs(errors),
662708
};
663709
}
664710
});
@@ -751,6 +797,7 @@ async function buildAndPushSelfHostedImage(
751797
return {
752798
ok: false as const,
753799
error: e instanceof Error ? e.message : JSON.stringify(e),
800+
logs: extractLogs(errors),
754801
};
755802
}
756803

@@ -793,6 +840,7 @@ async function buildAndPushSelfHostedImage(
793840
return {
794841
ok: false as const,
795842
error: e instanceof Error ? e.message : JSON.stringify(e),
843+
logs: extractLogs(errors),
796844
};
797845
}
798846
}
@@ -803,6 +851,7 @@ async function buildAndPushSelfHostedImage(
803851
ok: true as const,
804852
image: options.imageTag,
805853
digest,
854+
logs: extractLogs(errors),
806855
};
807856
});
808857
}
@@ -820,6 +869,13 @@ function extractImageDigest(outputs: string[]) {
820869
}
821870
}
822871

872+
function extractLogs(outputs: string[]) {
873+
// Remove empty lines
874+
const cleanedOutputs = outputs.map((line) => line.trim()).filter((line) => line !== "");
875+
876+
return cleanedOutputs.map((line) => line.trim()).join("\n");
877+
}
878+
823879
async function compileProject(
824880
config: ResolvedConfig,
825881
options: DeployCommandOptions,
@@ -1057,7 +1113,7 @@ async function compileProject(
10571113
);
10581114

10591115
if (!resolvingDependenciesResult) {
1060-
throw new Error("Failed to resolve dependencies");
1116+
throw new SkipLoggingError("Failed to resolve dependencies");
10611117
}
10621118

10631119
// Write the Containerfile to /tmp/dir/Containerfile
@@ -1188,15 +1244,39 @@ async function resolveDependencies(
11881244

11891245
return true;
11901246
} catch (installError) {
1191-
logger.debug(`Failed to resolve dependencies: ${JSON.stringify(installError)}`);
1192-
11931247
recordSpanException(span, installError);
1194-
11951248
span.end();
11961249

1197-
resolvingDepsSpinner.stop(
1198-
"Failed to resolve dependencies. Rerun with --log-level=debug for more information"
1199-
);
1250+
const parsedError = parseNpmInstallError(installError);
1251+
1252+
if (typeof parsedError === "string") {
1253+
resolvingDepsSpinner.stop(`Failed to resolve dependencies: ${parsedError}`);
1254+
} else {
1255+
switch (parsedError.type) {
1256+
case "package-not-found-error": {
1257+
resolvingDepsSpinner.stop(`Failed to resolve dependencies`);
1258+
1259+
logger.log(
1260+
`\n${chalkError("X Error:")} The package ${chalkPurple(
1261+
parsedError.packageName
1262+
)} could not be found in the npm registry.`
1263+
);
1264+
1265+
break;
1266+
}
1267+
case "no-matching-version-error": {
1268+
resolvingDepsSpinner.stop(`Failed to resolve dependencies`);
1269+
1270+
logger.log(
1271+
`\n${chalkError("X Error:")} The package ${chalkPurple(
1272+
parsedError.packageName
1273+
)} could not resolve because the version doesn't exist`
1274+
);
1275+
1276+
break;
1277+
}
1278+
}
1279+
}
12001280

12011281
return false;
12021282
}
@@ -1312,8 +1392,12 @@ async function gatherRequiredDependencies(
13121392
dependencies[packageParts.name] = externalDependencyVersion;
13131393
continue;
13141394
} else {
1315-
logger.warn(
1316-
`Could not find version for package ${packageName}, add a version specifier to the package name (e.g. ${packageParts.name}@latest) or add it to your project's package.json`
1395+
logger.log(
1396+
`${chalkWarning("X Warning:")} Could not find version for package ${chalkPurple(
1397+
packageName
1398+
)}, add a version specifier to the package name (e.g. ${
1399+
packageParts.name
1400+
}@latest) or add it to your project's package.json`
13171401
);
13181402
}
13191403
}

packages/cli-v3/src/commands/dev.tsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
4141
import { UncaughtExceptionError } from "../workers/common/errors";
4242
import { BackgroundWorker, BackgroundWorkerCoordinator } from "../workers/dev/backgroundWorker.js";
4343
import { runtimeCheck } from "../utilities/runtimeCheck";
44+
import {
45+
logESMRequireError,
46+
parseBuildErrorStack,
47+
parseNpmInstallError,
48+
} from "../utilities/deployErrors";
4449

4550
let apiClient: CliApiClient | undefined;
4651

@@ -544,20 +549,55 @@ function useDev({
544549
);
545550
} catch (e) {
546551
if (e instanceof UncaughtExceptionError) {
552+
const parsedBuildError = parseBuildErrorStack(e.originalError);
553+
554+
if (typeof parsedBuildError !== "string") {
555+
logESMRequireError(
556+
parsedBuildError,
557+
configPath
558+
? { status: "file", path: configPath, config }
559+
: { status: "in-memory", config }
560+
);
561+
return;
562+
} else {
563+
}
564+
547565
if (e.originalError.stack) {
548-
logger.error("Background worker failed to start", e.originalError.stack);
566+
logger.log(
567+
`${chalkError("X Error:")} Worker failed to start`,
568+
e.originalError.stack
569+
);
549570
}
550571

551572
return;
552573
}
553574

554-
if (e instanceof Error) {
555-
logger.error(`Background worker failed to start`, e.stack);
556-
557-
return;
575+
const parsedError = parseNpmInstallError(e);
576+
577+
if (typeof parsedError === "string") {
578+
logger.log(`${chalkError("X Error:")} ${parsedError}`);
579+
} else {
580+
switch (parsedError.type) {
581+
case "package-not-found-error": {
582+
logger.log(
583+
`\n${chalkError("X Error:")} The package ${chalkPurple(
584+
parsedError.packageName
585+
)} could not be found in the npm registry.`
586+
);
587+
588+
break;
589+
}
590+
case "no-matching-version-error": {
591+
logger.log(
592+
`\n${chalkError("X Error:")} The package ${chalkPurple(
593+
parsedError.packageName
594+
)} could not resolve because the version doesn't exist`
595+
);
596+
597+
break;
598+
}
599+
}
558600
}
559-
560-
logger.error(`Background worker failed to start: ${e}`);
561601
}
562602
});
563603
},

0 commit comments

Comments
 (0)