-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(cli): server-selected deploy build path #4803
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
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
0be0511
feat(webapp): deploy settings endpoint resolving the build path from …
myftija 0344c58
feat(cli): pick the deploy build path from the server unless a flag s…
myftija 89d7665
fix(cli): single-attempt deploy settings fetch, quiet 404, keep dry r…
myftija e2f8309
feat(cli): add --native-build, decouple --detach from the build path
myftija a157b32
feat(cli): --local-bundle modifies the native path instead of selecti…
myftija b190c54
fix(cli): apply build path modifiers in one place, keep every native …
myftija f092a9b
fix(cli): clearer flag conflicts and more headroom on the deploy sett…
myftija a5c2866
fix(cli): keep native-only flags on native when the deploy settings f…
myftija 3fd0275
fix(webapp): read only the opt-out from build settings in deploy-sett…
myftija 039f939
feat(cli): --local-bundle and --detach require --native-build
myftija 6a02bfc
refactor(deploy-settings): return only build_path, resolve it in Depl…
myftija 30f6535
refactor(deploy-settings): typed service errors and a testable CLI re…
myftija 98f8903
fix(webapp): drop unused exports from the deploy settings types
myftija cd7cd5f
fix(cli): do not announce a server-selected build path on dry runs
myftija 5d4573e
fix(cli): log the server-selected build path at debug level only
myftija File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "trigger.dev": patch | ||
| "@trigger.dev/core": patch | ||
| --- | ||
|
|
||
| `trigger.dev deploy` now asks the server whether to build with Depot or the native build server unless `--native-build`, `--depot-build`, or `--local-build` is passed, so the native build server can be rolled out per organization without a CLI change. `--local-bundle` and `--detach` now require `--native-build`. |
70 changes: 70 additions & 0 deletions
70
apps/webapp/app/routes/api.v1.projects.$projectRef.$env.deploy-settings.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime"; | ||
| import { type GetDeploySettingsResponseBody } from "@trigger.dev/core/v3"; | ||
| import { z } from "zod"; | ||
| import { authenticateApiKeyWithScope } from "~/services/apiAuth.server"; | ||
| import { logger } from "~/services/logger.server"; | ||
| import { DeploymentService } from "~/v3/services/deployment.server"; | ||
|
|
||
| const ParamsSchema = z.object({ | ||
| projectRef: z.string(), | ||
| env: z.enum(["dev", "staging", "prod", "preview"]), | ||
| }); | ||
|
|
||
| export async function loader({ request, params }: LoaderFunctionArgs) { | ||
| const parsedParams = ParamsSchema.safeParse(params); | ||
|
|
||
| if (!parsedParams.success) { | ||
| return json({ error: "Invalid params" }, { status: 400 }); | ||
| } | ||
|
|
||
| try { | ||
| const authResult = await authenticateApiKeyWithScope(request, { | ||
| action: "read", | ||
| resource: { type: "deployments" }, | ||
| }); | ||
|
|
||
| if (!authResult.ok) { | ||
| logger.info("Invalid or missing api key", { url: request.url }); | ||
| return json({ error: authResult.error }, { status: authResult.status }); | ||
| } | ||
|
|
||
| const { environment: authenticatedEnv } = authResult.authentication; | ||
| const { projectRef, env } = parsedParams.data; | ||
|
|
||
| const deploymentService = new DeploymentService(); | ||
|
|
||
| return await deploymentService | ||
| .getDeploySettings(authenticatedEnv, { projectRef, envSlug: env }) | ||
| .match( | ||
| ({ buildPath, buildPathSource }) => { | ||
| logger.info("Resolved deploy build path", { | ||
| environmentId: authenticatedEnv.id, | ||
| projectRef, | ||
| env, | ||
| buildPath, | ||
| buildPathSource, | ||
| }); | ||
|
|
||
| return json({ build_path: buildPath } satisfies GetDeploySettingsResponseBody); | ||
| }, | ||
| (error) => { | ||
| switch (error.type) { | ||
| case "environment_mismatch": | ||
| return json( | ||
| { error: "API key does not belong to this project environment" }, | ||
| { status: 403 } | ||
| ); | ||
| case "failed_to_load_global_flags": | ||
| default: | ||
| error.type satisfies "failed_to_load_global_flags"; | ||
| logger.error("Failed to load the global feature flags", { error: error.cause }); | ||
| return json({ error: "Internal Server Error" }, { status: 500 }); | ||
| } | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| if (error instanceof Response) throw error; | ||
| logger.error("Failed to resolve deploy settings", { error }); | ||
| return json({ error: "Internal Server Error" }, { status: 500 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import { errAsync, okAsync } from "neverthrow"; | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| const mocks = vi.hoisted(() => ({ | ||
| authenticateApiKeyWithScope: vi.fn<(...args: any[]) => Promise<any>>(), | ||
| getDeploySettings: vi.fn<(...args: any[]) => any>(), | ||
| })); | ||
|
|
||
| vi.mock("~/services/apiAuth.server", () => ({ | ||
| authenticateApiKeyWithScope: mocks.authenticateApiKeyWithScope, | ||
| })); | ||
| vi.mock("~/v3/services/deployment.server", () => ({ | ||
| DeploymentService: class { | ||
| getDeploySettings = mocks.getDeploySettings; | ||
| }, | ||
| })); | ||
| vi.mock("~/services/logger.server", () => ({ | ||
| logger: { info: vi.fn(), debug: vi.fn(), error: vi.fn() }, | ||
| })); | ||
|
|
||
| import { loader } from "~/routes/api.v1.projects.$projectRef.$env.deploy-settings"; | ||
|
|
||
| function environment(overrides: Record<string, unknown> = {}) { | ||
| return { | ||
| id: "env_1", | ||
| type: "PRODUCTION", | ||
| project: { id: "proj_1", externalRef: "proj_ref" }, | ||
| organization: { featureFlags: {} }, | ||
| ...overrides, | ||
| }; | ||
| } | ||
|
|
||
| function load(env = "prod", projectRef = "proj_ref") { | ||
| return loader({ | ||
| request: new Request( | ||
| `https://app.example.com/api/v1/projects/${projectRef}/${env}/deploy-settings` | ||
| ), | ||
| params: { projectRef, env }, | ||
| context: {}, | ||
| }); | ||
| } | ||
|
|
||
| describe("deploy settings route", () => { | ||
| beforeEach(() => { | ||
| mocks.authenticateApiKeyWithScope.mockReset(); | ||
| mocks.getDeploySettings.mockReset(); | ||
| mocks.authenticateApiKeyWithScope.mockResolvedValue({ | ||
| ok: true, | ||
| authentication: { environment: environment() }, | ||
| }); | ||
| mocks.getDeploySettings.mockReturnValue( | ||
| okAsync({ buildPath: "depot", buildPathSource: "default" }) | ||
| ); | ||
| }); | ||
|
|
||
| it("rejects an unknown env slug before authenticating", async () => { | ||
| const response = await load("nope"); | ||
| expect(response.status).toBe(400); | ||
| expect(mocks.authenticateApiKeyWithScope).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("passes the auth failure through", async () => { | ||
| mocks.authenticateApiKeyWithScope.mockResolvedValue({ | ||
| ok: false, | ||
| status: 401, | ||
| error: "Invalid API key", | ||
| }); | ||
| const response = await load(); | ||
| expect(response.status).toBe(401); | ||
| expect(await response.json()).toEqual({ error: "Invalid API key" }); | ||
| expect(mocks.getDeploySettings).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("maps an environment mismatch to 403", async () => { | ||
| mocks.getDeploySettings.mockReturnValue(errAsync({ type: "environment_mismatch" })); | ||
| const response = await load("prod", "proj_other"); | ||
| expect(response.status).toBe(403); | ||
| expect(mocks.getDeploySettings).toHaveBeenCalledWith(environment(), { | ||
| projectRef: "proj_other", | ||
| envSlug: "prod", | ||
| }); | ||
| }); | ||
|
|
||
| it("returns only the build path, resolved for the authenticated environment", async () => { | ||
| const env = environment({ type: "PREVIEW" }); | ||
| mocks.authenticateApiKeyWithScope.mockResolvedValue({ | ||
| ok: true, | ||
| authentication: { environment: env }, | ||
| }); | ||
| mocks.getDeploySettings.mockReturnValue( | ||
| okAsync({ buildPath: "native", buildPathSource: "organization_environment" }) | ||
| ); | ||
|
|
||
| const response = await load("preview"); | ||
| expect(response.status).toBe(200); | ||
| expect(await response.json()).toEqual({ build_path: "native" }); | ||
| expect(mocks.getDeploySettings).toHaveBeenCalledWith(env, { | ||
| projectRef: "proj_ref", | ||
| envSlug: "preview", | ||
| }); | ||
| expect(mocks.authenticateApiKeyWithScope).toHaveBeenCalledWith(expect.any(Request), { | ||
| action: "read", | ||
| resource: { type: "deployments" }, | ||
| }); | ||
| }); | ||
|
|
||
| it("returns 500 when the global flags cannot be loaded", async () => { | ||
| mocks.getDeploySettings.mockReturnValue( | ||
| errAsync({ type: "failed_to_load_global_flags", cause: new Error("db down") }) | ||
| ); | ||
| const response = await load(); | ||
| expect(response.status).toBe(500); | ||
| expect(await response.json()).toEqual({ error: "Internal Server Error" }); | ||
| }); | ||
|
|
||
| it("rethrows a Response thrown by authentication", async () => { | ||
| const thrown = new Response(null, { status: 429 }); | ||
| mocks.authenticateApiKeyWithScope.mockRejectedValue(thrown); | ||
| await expect(load()).rejects.toBe(thrown); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.