Skip to content

Commit 2f3f82f

Browse files
authored
feat(deployments): show local build hint during depot outages (#2646)
* Add an API endpoint to query remote build provider status * Show local build hint for failed deployments when Depot is down * Show the local build flag in the help output * Add changeset * Fix import * Fix docs link
1 parent 4264bdf commit 2f3f82f

File tree

5 files changed

+145
-5
lines changed

5 files changed

+145
-5
lines changed

.changeset/nice-pillows-hide.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Added a hint about the `--force-local-build` flag on failed deployments due to upstream provider outages.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { json } from "@remix-run/node";
2+
import { err, fromPromise, fromSafePromise, ok } from "neverthrow";
3+
import z from "zod";
4+
import { logger } from "~/services/logger.server";
5+
import { type RemoteBuildProviderStatusResponseBody } from "@trigger.dev/core/v3/schemas";
6+
7+
const DEPOT_STATUS_URL = "https://status.depot.dev/proxy/status.depot.dev";
8+
const FETCH_TIMEOUT_MS = 2000;
9+
10+
export async function loader() {
11+
return await fetchDepotStatus().match(
12+
({ summary: { ongoing_incidents } }) => {
13+
if (ongoing_incidents.length > 0) {
14+
return json(
15+
{
16+
status: "degraded",
17+
message:
18+
"Our remote build provider is currently facing issues. You can use the `--force-local-build` flag to build and deploy the image locally. Read more about local builds here: https://trigger.dev/docs/deployment/overview#local-builds",
19+
} satisfies RemoteBuildProviderStatusResponseBody,
20+
{ status: 200 }
21+
);
22+
}
23+
24+
return json(
25+
{
26+
status: "operational",
27+
message: "Depot is operational",
28+
} satisfies RemoteBuildProviderStatusResponseBody,
29+
{ status: 200 }
30+
);
31+
},
32+
() => {
33+
return json(
34+
{
35+
status: "unknown",
36+
message: "Failed to fetch remote build provider status",
37+
} satisfies RemoteBuildProviderStatusResponseBody,
38+
{ status: 200 }
39+
);
40+
}
41+
);
42+
}
43+
44+
function fetchDepotStatus() {
45+
return fromPromise(
46+
fetch(DEPOT_STATUS_URL, {
47+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
48+
}),
49+
(error) => {
50+
if (
51+
error instanceof Error &&
52+
(error.name === "TimeoutError" || error.name === "AbortError")
53+
) {
54+
return {
55+
type: "timeout" as const,
56+
};
57+
}
58+
59+
return {
60+
type: "other" as const,
61+
cause: error,
62+
};
63+
}
64+
)
65+
.andThen((response) => {
66+
if (!response.ok) {
67+
return err({
68+
type: "other" as const,
69+
cause: new Error(`Failed to fetch Depot status: ${response.status}`),
70+
});
71+
}
72+
73+
return fromSafePromise(response.json());
74+
})
75+
.andThen((json) => {
76+
const parsed = DepotStatusResponseSchema.safeParse(json);
77+
78+
if (!parsed.success) {
79+
logger.warn("Invalid Depot status response", { error: parsed.error });
80+
return err({
81+
type: "validation_failed" as const,
82+
});
83+
}
84+
85+
return ok(parsed.data);
86+
});
87+
}
88+
89+
// partial schema
90+
const DepotStatusResponseSchema = z.object({
91+
summary: z.object({
92+
ongoing_incidents: z.array(z.any()),
93+
}),
94+
});

packages/cli-v3/src/apiClient.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
PromoteDeploymentResponseBody,
2323
StartDeploymentIndexingRequestBody,
2424
StartDeploymentIndexingResponseBody,
25-
TaskRunExecution,
2625
TriggerTaskRequestBody,
2726
TriggerTaskResponse,
2827
UpsertBranchRequestBody,
@@ -38,6 +37,7 @@ import {
3837
GetJWTResponse,
3938
ApiBranchListResponseBody,
4039
GenerateRegistryCredentialsResponseBody,
40+
RemoteBuildProviderStatusResponseBody,
4141
} from "@trigger.dev/core/v3";
4242
import {
4343
WorkloadDebugLogRequestBody,
@@ -52,6 +52,7 @@ import { ApiResult, wrapZodFetch, zodfetchSSE } from "@trigger.dev/core/v3/zodfe
5252
import { EventSource } from "eventsource";
5353
import { z } from "zod";
5454
import { logger } from "./utilities/logger.js";
55+
import { VERSION } from "./version.js";
5556

5657
export class CliApiClient {
5758
private engineURL: string;
@@ -328,6 +329,21 @@ export class CliApiClient {
328329
);
329330
}
330331

332+
async getRemoteBuildProviderStatus() {
333+
return wrapZodFetch(
334+
RemoteBuildProviderStatusResponseBody,
335+
`${this.apiURL}/api/v1/remote-build-provider-status`,
336+
{
337+
method: "GET",
338+
headers: {
339+
...this.getHeaders(),
340+
// probably a good idea to add this to the other requests too
341+
"x-trigger-cli-version": VERSION,
342+
},
343+
}
344+
);
345+
}
346+
331347
async generateRegistryCredentials(deploymentId: string) {
332348
if (!this.accessToken) {
333349
throw new Error("generateRegistryCredentials: No access token");

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

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ import {
2828
printWarnings,
2929
saveLogs,
3030
} from "../deploy/logs.js";
31-
import { chalkError, cliLink, isLinksSupported, prettyError } from "../utilities/cliOutput.js";
31+
import {
32+
chalkError,
33+
cliLink,
34+
isLinksSupported,
35+
prettyError,
36+
prettyWarning,
37+
} from "../utilities/cliOutput.js";
3238
import { loadDotEnvVars } from "../utilities/dotEnv.js";
3339
import { isDirectory } from "../utilities/fileSystem.js";
3440
import { setGithubActionsOutputAndEnvVars } from "../utilities/githubActions.js";
@@ -128,9 +134,7 @@ export function configureDeployCommand(program: Command) {
128134
).hideHelp()
129135
)
130136
// Local build options
131-
.addOption(
132-
new CommandOption("--force-local-build", "Force a local build of the image").hideHelp()
133-
)
137+
.addOption(new CommandOption("--force-local-build", "Force a local build of the image"))
134138
.addOption(new CommandOption("--push", "Push the image after local builds").hideHelp())
135139
.addOption(
136140
new CommandOption("--no-push", "Do not push the image after local builds").hideHelp()
@@ -458,6 +462,17 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
458462

459463
const warnings = checkLogsForWarnings(buildResult.logs);
460464

465+
const canShowLocalBuildHint = !isLocalBuild && !process.env.TRIGGER_LOCAL_BUILD_HINT_DISABLED;
466+
const buildFailed = !warnings.ok || !buildResult.ok;
467+
468+
if (buildFailed && canShowLocalBuildHint) {
469+
const providerStatus = await projectClient.client.getRemoteBuildProviderStatus();
470+
471+
if (providerStatus.success && providerStatus.data.status === "degraded") {
472+
prettyWarning(providerStatus.data.message + "\n");
473+
}
474+
}
475+
461476
if (!warnings.ok) {
462477
await failDeploy(
463478
projectClient.client,

packages/core/src/v3/schemas/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,15 @@ export const InitializeDeploymentRequestBody = z.object({
439439

440440
export type InitializeDeploymentRequestBody = z.infer<typeof InitializeDeploymentRequestBody>;
441441

442+
export const RemoteBuildProviderStatusResponseBody = z.object({
443+
status: z.enum(["operational", "degraded", "unknown"]),
444+
message: z.string(),
445+
});
446+
447+
export type RemoteBuildProviderStatusResponseBody = z.infer<
448+
typeof RemoteBuildProviderStatusResponseBody
449+
>;
450+
442451
export const GenerateRegistryCredentialsResponseBody = z.object({
443452
username: z.string(),
444453
password: z.string(),

0 commit comments

Comments
 (0)