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.
This commit is contained in:
Saadi Myftija
2026-07-22 18:25:03 +02:00
parent 4a0eb683ab
commit f3594dc47b
3 changed files with 39 additions and 6 deletions
@@ -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<string, string>;
@@ -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 });
+13
View File
@@ -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");
+12
View File
@@ -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(