diff --git a/.changeset/deploy-log-stream-disconnect.md b/.changeset/deploy-log-stream-disconnect.md new file mode 100644 index 00000000000..f9db45375e6 --- /dev/null +++ b/.changeset/deploy-log-stream-disconnect.md @@ -0,0 +1,5 @@ +--- +"trigger.dev": patch +--- + +When the build log stream cannot be opened or disconnects during a build server deploy, the CLI now explains that the deployment itself is unaffected and exits immediately with a non-zero code, since it can no longer confirm the outcome. Previously a disconnect printed the raw stream error and left the process hanging. diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 0868c05dfcf..6894b85afa2 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -26,6 +26,7 @@ import { resolveAlwaysExternal } from "../build/externals.js"; import { createContextArchive, getArchiveSize } from "../deploy/archiveContext.js"; import { createBundleArchive } from "../deploy/bundleArchive.js"; import { + type BuildLogRenderer, BuildLogsMode, createBuildLogRenderer, resolveBuildLogsMode, @@ -2284,6 +2285,36 @@ function showFullBuildLogs(options: DeployCommandOptions) { return resolveBuildLogsMode(options.buildLogs, buildLogsEnv(options)) === "full"; } +function buildLogStreamError( + error: unknown, + renderer: BuildLogRenderer, + deployment: Pick, + rawDeploymentLink: string +): OutroCommandError { + renderer.finish("Log stream stopped", "failure"); + + logger.debug("Build log stream failed", { error }); + + const reason = (error instanceof Error ? error.message : String(error)) + .replace(/\s+/g, " ") + .slice(0, 200); + + log.error(`Build log stream failed: ${reason}`); + log.info( + "The deployment itself is unaffected and continues on the build server. Check the dashboard for the final status." + ); + + if (!isLinksSupported) { + log.info(`View deployment: ${rawDeploymentLink}`); + } + + return new OutroCommandError( + `Version ${deployment.version} ${ + isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : "" + }` + ); +} + async function followBuildServerDeployment({ deployment, eventStream, @@ -2319,22 +2350,17 @@ async function followBuildServerDeployment({ ); if (readSessionError) { - renderer.finish("Failed to query build progress", "abandoned"); - log.warn(`Failed streaming build logs, open the deployment in the dashboard to view the logs`); - - outro( - `Version ${deployment.version} is being deployed ${ - isLinksSupported ? `| ${cliLink("View deployment", rawDeploymentLink)}` : "" - }` - ); - - return process.exit(0); + throw buildLogStreamError(readSessionError, renderer, deployment, rawDeploymentLink); } - const finalDeploymentEvent = await streamDeploymentEvents(readSession, renderer, () => - abortController.abort() + const [streamError, finalDeploymentEvent] = await tryCatch( + streamDeploymentEvents(readSession, renderer, () => abortController.abort()) ); + if (streamError) { + throw buildLogStreamError(streamError, renderer, deployment, rawDeploymentLink); + } + if (!renderer.started && !finalDeploymentEvent) { // unlikely that it happens in practice, only in rare corner cases // the timeout would kick in earlier if the build server fails to dequeue the build diff --git a/packages/cli-v3/src/deploy/buildLogs.test.ts b/packages/cli-v3/src/deploy/buildLogs.test.ts index 62efac5dd51..565080cb0f2 100644 --- a/packages/cli-v3/src/deploy/buildLogs.test.ts +++ b/packages/cli-v3/src/deploy/buildLogs.test.ts @@ -258,6 +258,38 @@ describe("streamDeploymentEvents", () => { expect(onFinalized).toHaveBeenCalledTimes(1); }); + it("stops consuming the stream once the finalized event arrives", async () => { + let released = false; + async function* recordsThenThrow() { + try { + yield { + seqNum: 0, + timestamp: 1_700_000_000_000, + body: JSON.stringify({ type: "log", data: { message: "a" } }), + }; + yield { + seqNum: 1, + timestamp: 1_700_000_000_000, + body: JSON.stringify({ type: "finalized", data: { result: "succeeded" } }), + }; + throw new Error("stream closed with unparsed data remaining"); + } finally { + released = true; + } + } + let finalizedCalls = 0; + const final = await streamDeploymentEvents( + recordsThenThrow(), + { started: false, log: () => {}, finish: () => {} }, + () => { + finalizedCalls++; + } + ); + expect(final).toEqual({ result: "succeeded" }); + expect(finalizedCalls).toBe(1); + expect(released).toBe(true); + }); + it("returns undefined when the stream ends without a finalized event", async () => { const final = await streamDeploymentEvents( records([JSON.stringify({ type: "log", data: { message: "a" } })]), diff --git a/packages/cli-v3/src/deploy/buildLogs.ts b/packages/cli-v3/src/deploy/buildLogs.ts index 8241ca7a5d9..21b04fa42bd 100644 --- a/packages/cli-v3/src/deploy/buildLogs.ts +++ b/packages/cli-v3/src/deploy/buildLogs.ts @@ -158,8 +158,6 @@ export async function streamDeploymentEvents( renderer: BuildLogRenderer, onFinalized: () => void ): Promise { - let finalEvent: DeploymentFinalizedEvent["data"] | undefined; - for await (const record of records) { const result = DeploymentEventFromString.safeParse(record.body); if (!result.success) { @@ -182,9 +180,8 @@ export async function streamDeploymentEvents( break; } case "finalized": { - finalEvent = event.data; onFinalized(); - break; + return event.data; } default: { event satisfies never; @@ -193,5 +190,5 @@ export async function streamDeploymentEvents( } } - return finalEvent; + return undefined; }