From f3594dc47b210f061d4579e4962fe72686055887 Mon Sep 17 00:00:00 2001 From: Saadi Myftija Date: Wed, 22 Jul 2026 18:25:03 +0200 Subject: [PATCH] fix(deploy): fail loud when stored build env vars cannot be read Review feedback: a corrupt or undecryptable envelope previously returned an empty record, indistinguishable from no vars at all, letting the remote build run without its build-time secrets. The endpoint now returns an error so the build aborts with an actionable message. An empty record remains the response only for deployments that genuinely have none or are terminal. Also cancel the deployment best-effort when the version-skew guard aborts, instead of leaving it pending until the queue timeout reaps it. --- ...eployments.$deploymentId.build-env-vars.ts | 20 +++++++++++++------ packages/cli-v3/src/apiClient.ts | 13 ++++++++++++ packages/cli-v3/src/commands/deploy.ts | 12 +++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.build-env-vars.ts b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.build-env-vars.ts index de8a62102..561f8daf6 100644 --- a/apps/webapp/app/routes/api.v1.deployments.$deploymentId.build-env-vars.ts +++ b/apps/webapp/app/routes/api.v1.deployments.$deploymentId.build-env-vars.ts @@ -75,6 +75,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { }); } + // Vars exist but can't be read: fail LOUD. Returning an empty record here would + // be indistinguishable from "there were none" and let the build run without its + // build-time secrets (confusing failure at best, silently-wrong image at worst). + // Concrete trigger: ENCRYPTION_KEY rotation during the build window. const envelope = EncryptedSecretValueSchema.safeParse(deployment.buildEnvVars); if (!envelope.success) { @@ -82,9 +86,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) { deploymentId, environmentId: authenticatedEnv.id, }); - return json({ variables: {} } satisfies GetDeploymentBuildEnvVarsResponseBody, { - status: 200, - }); + return json( + { error: "The stored build environment variables could not be read. Retry the deploy." }, + { status: 500 } + ); } let variables: Record; @@ -98,9 +103,12 @@ export async function loader({ request, params }: LoaderFunctionArgs) { environmentId: authenticatedEnv.id, error, }); - return json({ variables: {} } satisfies GetDeploymentBuildEnvVarsResponseBody, { - status: 200, - }); + return json( + { + error: "The stored build environment variables could not be decrypted. Retry the deploy.", + }, + { status: 500 } + ); } return json({ variables } satisfies GetDeploymentBuildEnvVarsResponseBody, { status: 200 }); diff --git a/packages/cli-v3/src/apiClient.ts b/packages/cli-v3/src/apiClient.ts index 8b9fd56eb..9afadb0ab 100644 --- a/packages/cli-v3/src/apiClient.ts +++ b/packages/cli-v3/src/apiClient.ts @@ -690,6 +690,19 @@ export class CliApiClient { ); } + // Best-effort cancel (204 on success, no body) — callers may ignore failures. + async cancelDeployment(deploymentId: string, reason?: string) { + if (!this.accessToken) { + throw new Error("cancelDeployment: No access token"); + } + + return fetch(`${this.apiURL}/api/v1/deployments/${deploymentId}/cancel`, { + method: "POST", + headers: this.getHeaders(), + body: JSON.stringify({ reason }), + }); + } + async getDeploymentBuildEnvVars(deploymentId: string) { if (!this.accessToken) { throw new Error("getDeploymentBuildEnvVars: No access token"); diff --git a/packages/cli-v3/src/commands/deploy.ts b/packages/cli-v3/src/commands/deploy.ts index 10b7b1b6e..176309139 100644 --- a/packages/cli-v3/src/commands/deploy.ts +++ b/packages/cli-v3/src/commands/deploy.ts @@ -1556,6 +1556,18 @@ async function handleNativeBuildServerDeploy({ Object.keys(bundleBuildEnvVars).length > 0 && !deployment.buildEnvVarsStored ) { + // Courtesy cancel so the deployment doesn't linger as PENDING until the + // queue timeout reaps it. Best-effort: the hard error below is what matters. + const [cancelError] = await tryCatch( + apiClient.cancelDeployment(deployment.id, "Build environment variables were not stored") + ); + if (cancelError) { + logger.debug("Failed to cancel deployment after missing build env vars ack", { + deploymentId: deployment.id, + error: cancelError, + }); + } + $deploymentSpinner.stop("Failed to initialize deployment"); log.error( chalk.bold(