Skip to content

v3: Better dev/deploy errors and handle image build errors #988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 29, 2024
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
5 changes: 5 additions & 0 deletions .changeset/mighty-camels-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---

Improve error messages during dev/deploy and handle deploy image build issues
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"type": "node-terminal",
"request": "launch",
"name": "Debug V3 Deploy CLI",
"command": "pnpm exec trigger.dev deploy",
"command": "pnpm exec trigger.dev deploy --skip-deploy",
"cwd": "${workspaceFolder}/references/v3-catalog",
"sourceMaps": true
},
Expand Down
4 changes: 3 additions & 1 deletion packages/cli-v3/src/cli/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getTracer, provider } from "../telemetry/tracing";
import { fromZodError } from "zod-validation-error";
import { logger } from "../utilities/logger";
import { outro } from "@clack/prompts";
import { chalkError } from "../utilities/cliOutput";

export const CommonCommandOptions = z.object({
apiUrl: z.string().optional(),
Expand Down Expand Up @@ -84,7 +85,8 @@ export async function wrapCommandAction<T extends z.AnyZodObject, TResult>(
// do nothing
} else {
recordSpanException(span, e);
logger.error(e instanceof Error ? e.message : String(e));

logger.log(`${chalkError("X Error:")} ${e instanceof Error ? e.message : String(e)}`);
}

span.end();
Expand Down
152 changes: 118 additions & 34 deletions packages/cli-v3/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ import { logger } from "../utilities/logger.js";
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
import { login } from "./login";

import { Glob } from "glob";
import type { SetOptional } from "type-fest";
import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../utilities/build";
import { Glob } from "glob";
import { chalkError, chalkPurple, chalkWarning } from "../utilities/cliOutput";
import {
logESMRequireError,
parseBuildErrorStack,
parseNpmInstallError,
} from "../utilities/deployErrors";

const DeployCommandOptions = CommonCommandOptions.extend({
skipTypecheck: z.boolean().default(false),
Expand Down Expand Up @@ -272,28 +278,44 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
);
}

return buildAndPushImage({
registryHost,
auth: authorization.auth.accessToken,
imageTag: deploymentResponse.data.imageTag,
buildId: deploymentResponse.data.externalBuildData.buildId,
buildToken: deploymentResponse.data.externalBuildData.buildToken,
buildProjectId: deploymentResponse.data.externalBuildData.projectId,
cwd: compilation.path,
projectId: resolvedConfig.config.project,
deploymentId: deploymentResponse.data.id,
deploymentVersion: deploymentResponse.data.version,
contentHash: deploymentResponse.data.contentHash,
projectRef: resolvedConfig.config.project,
loadImage: options.loadImage,
buildPlatform: options.buildPlatform,
});
return buildAndPushImage(
{
registryHost,
auth: authorization.auth.accessToken,
imageTag: deploymentResponse.data.imageTag,
buildId: deploymentResponse.data.externalBuildData.buildId,
buildToken: deploymentResponse.data.externalBuildData.buildToken,
buildProjectId: deploymentResponse.data.externalBuildData.projectId,
cwd: compilation.path,
projectId: resolvedConfig.config.project,
deploymentId: deploymentResponse.data.id,
deploymentVersion: deploymentResponse.data.version,
contentHash: deploymentResponse.data.contentHash,
projectRef: resolvedConfig.config.project,
loadImage: options.loadImage,
buildPlatform: options.buildPlatform,
},
deploymentSpinner
);
};

const image = await buildImage();

if (!image.ok) {
deploymentSpinner.stop(`Failed to build project image: ${image.error}`);
deploymentSpinner.stop(`Failed to build project.`);

// If there are logs, let's write it out to a temporary file and include the path in the error message
if (image.logs.trim() !== "") {
const logPath = join(await createTempDir(), `build-${deploymentResponse.data.shortCode}.log`);

await writeFile(logPath, image.logs);

logger.log(
`${chalkError("X Error:")} ${image.error}. Full build logs have been saved to ${logPath})`
);
} else {
logger.log(`${chalkError("X Error:")} ${image.error}.`);
}

throw new SkipLoggingError(`Failed to build project image: ${image.error}`);
}
Expand Down Expand Up @@ -379,10 +401,19 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
}
case "FAILED": {
if (finishedDeployment.errorData) {
deploymentSpinner.stop(
`Deployment encountered an error: ${finishedDeployment.errorData.name}. ${deploymentLink}`
);
logger.error(finishedDeployment.errorData.stack);
const parsedError = finishedDeployment.errorData.stack
? parseBuildErrorStack(finishedDeployment.errorData)
: finishedDeployment.errorData.message;

if (typeof parsedError === "string") {
deploymentSpinner.stop(`Deployment encountered an error. ${deploymentLink}`);

logger.log(`${chalkError("X Error:")} ${parsedError}`);
} else {
deploymentSpinner.stop(`Deployment encountered an error. ${deploymentLink}`);

logESMRequireError(parsedError, resolvedConfig);
}

throw new SkipLoggingError(
`Deployment encountered an error: ${finishedDeployment.errorData.name}`
Expand Down Expand Up @@ -551,15 +582,18 @@ type BuildAndPushImageResults =
| {
ok: true;
image: string;
logs: string;
digest?: string;
}
| {
ok: false;
error: string;
logs: string;
};

async function buildAndPushImage(
options: BuildAndPushImageOptions
options: BuildAndPushImageOptions,
updater: ReturnType<typeof spinner>
): Promise<BuildAndPushImageResults> {
return tracer.startActiveSpan("buildAndPushImage", async (span) => {
span.setAttributes({
Expand Down Expand Up @@ -626,7 +660,7 @@ async function buildAndPushImage(
const errors: string[] = [];

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

childProcess.on("error", (e) => rej(e));
childProcess.on("close", () => res());
childProcess.on("close", (code) => res(code));
});

const logs = extractLogs(errors);

if (processCode !== 0) {
return {
ok: false as const,
error: `Error building image`,
logs,
};
}

const digest = extractImageDigest(errors);

span.setAttributes({
Expand All @@ -650,6 +694,7 @@ async function buildAndPushImage(
return {
ok: true as const,
image: options.imageTag,
logs,
digest,
};
} catch (e) {
Expand All @@ -659,6 +704,7 @@ async function buildAndPushImage(
return {
ok: false as const,
error: e instanceof Error ? e.message : JSON.stringify(e),
logs: extractLogs(errors),
};
}
});
Expand Down Expand Up @@ -751,6 +797,7 @@ async function buildAndPushSelfHostedImage(
return {
ok: false as const,
error: e instanceof Error ? e.message : JSON.stringify(e),
logs: extractLogs(errors),
};
}

Expand Down Expand Up @@ -793,6 +840,7 @@ async function buildAndPushSelfHostedImage(
return {
ok: false as const,
error: e instanceof Error ? e.message : JSON.stringify(e),
logs: extractLogs(errors),
};
}
}
Expand All @@ -803,6 +851,7 @@ async function buildAndPushSelfHostedImage(
ok: true as const,
image: options.imageTag,
digest,
logs: extractLogs(errors),
};
});
}
Expand All @@ -820,6 +869,13 @@ function extractImageDigest(outputs: string[]) {
}
}

function extractLogs(outputs: string[]) {
// Remove empty lines
const cleanedOutputs = outputs.map((line) => line.trim()).filter((line) => line !== "");

return cleanedOutputs.map((line) => line.trim()).join("\n");
}

async function compileProject(
config: ResolvedConfig,
options: DeployCommandOptions,
Expand Down Expand Up @@ -1057,7 +1113,7 @@ async function compileProject(
);

if (!resolvingDependenciesResult) {
throw new Error("Failed to resolve dependencies");
throw new SkipLoggingError("Failed to resolve dependencies");
}

// Write the Containerfile to /tmp/dir/Containerfile
Expand Down Expand Up @@ -1188,15 +1244,39 @@ async function resolveDependencies(

return true;
} catch (installError) {
logger.debug(`Failed to resolve dependencies: ${JSON.stringify(installError)}`);

recordSpanException(span, installError);

span.end();

resolvingDepsSpinner.stop(
"Failed to resolve dependencies. Rerun with --log-level=debug for more information"
);
const parsedError = parseNpmInstallError(installError);

if (typeof parsedError === "string") {
resolvingDepsSpinner.stop(`Failed to resolve dependencies: ${parsedError}`);
} else {
switch (parsedError.type) {
case "package-not-found-error": {
resolvingDepsSpinner.stop(`Failed to resolve dependencies`);

logger.log(
`\n${chalkError("X Error:")} The package ${chalkPurple(
parsedError.packageName
)} could not be found in the npm registry.`
);

break;
}
case "no-matching-version-error": {
resolvingDepsSpinner.stop(`Failed to resolve dependencies`);

logger.log(
`\n${chalkError("X Error:")} The package ${chalkPurple(
parsedError.packageName
)} could not resolve because the version doesn't exist`
);

break;
}
}
}

return false;
}
Expand Down Expand Up @@ -1312,8 +1392,12 @@ async function gatherRequiredDependencies(
dependencies[packageParts.name] = externalDependencyVersion;
continue;
} else {
logger.warn(
`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`
logger.log(
`${chalkWarning("X Warning:")} Could not find version for package ${chalkPurple(
packageName
)}, add a version specifier to the package name (e.g. ${
packageParts.name
}@latest) or add it to your project's package.json`
);
}
}
Expand Down
54 changes: 47 additions & 7 deletions packages/cli-v3/src/commands/dev.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
import { UncaughtExceptionError } from "../workers/common/errors";
import { BackgroundWorker, BackgroundWorkerCoordinator } from "../workers/dev/backgroundWorker.js";
import { runtimeCheck } from "../utilities/runtimeCheck";
import {
logESMRequireError,
parseBuildErrorStack,
parseNpmInstallError,
} from "../utilities/deployErrors";

let apiClient: CliApiClient | undefined;

Expand Down Expand Up @@ -544,20 +549,55 @@ function useDev({
);
} catch (e) {
if (e instanceof UncaughtExceptionError) {
const parsedBuildError = parseBuildErrorStack(e.originalError);

if (typeof parsedBuildError !== "string") {
logESMRequireError(
parsedBuildError,
configPath
? { status: "file", path: configPath, config }
: { status: "in-memory", config }
);
return;
} else {
}

if (e.originalError.stack) {
logger.error("Background worker failed to start", e.originalError.stack);
logger.log(
`${chalkError("X Error:")} Worker failed to start`,
e.originalError.stack
);
}

return;
}

if (e instanceof Error) {
logger.error(`Background worker failed to start`, e.stack);

return;
const parsedError = parseNpmInstallError(e);

if (typeof parsedError === "string") {
logger.log(`${chalkError("X Error:")} ${parsedError}`);
} else {
switch (parsedError.type) {
case "package-not-found-error": {
logger.log(
`\n${chalkError("X Error:")} The package ${chalkPurple(
parsedError.packageName
)} could not be found in the npm registry.`
);

break;
}
case "no-matching-version-error": {
logger.log(
`\n${chalkError("X Error:")} The package ${chalkPurple(
parsedError.packageName
)} could not resolve because the version doesn't exist`
);

break;
}
}
}

logger.error(`Background worker failed to start: ${e}`);
}
});
},
Expand Down
Loading