Skip to content

Commit 477d56e

Browse files
committed
feat(cli): dev branch support
Let the CLI target a development branch, against the updated server API. - `trigger dev --branch <branch>` resolves a dev branch (flag or TRIGGER_DEV_BRANCH, defaulting to "default") via getDevBranch, upserts it on boot, and sends the x-trigger-branch header on requests. - Per-branch dev lock files so concurrent dev sessions on different branches don't evict each other; "default" keeps the dev.lock name. - New `trigger dev archive` command to archive a dev branch; archive API calls now pass the env ("preview" | "development"). - Dev output shows the active branch. TRI-8726
1 parent 69d442b commit 477d56e

15 files changed

Lines changed: 222 additions & 47 deletions

File tree

docs/management/authentication.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ await envvars.update("proj_1234", "preview", "DATABASE_URL", {
119119

120120
</Tab>
121121
<Tab title="cURL">
122-
To target a specific preview branch, include the `x-trigger-branch` header in your API requests with the branch name as the value:
122+
To target a specific preview or development branch, include the `x-trigger-branch` header in your API requests with the branch name as the value:
123123

124124
```bash
125125
curl --request PUT \
@@ -137,8 +137,8 @@ curl --request PUT \
137137
This will set the `DATABASE_URL` environment variable specifically for the `feature-xyz` preview branch.
138138

139139
<Note>
140-
The `x-trigger-branch` header is only relevant when working with the `preview` environment (`{env}
141-
` parameter set to `preview`). It has no effect when working with `dev`, `staging`, or `prod`
140+
The `x-trigger-branch` header is only relevant when working with the `preview` or `dev` environments (`{env}
141+
` parameter set to `preview` or `development`). It has no effect when working with `staging`, or `prod`
142142
environments.
143143
</Note>
144144

packages/cli-v3/src/apiClient.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ export class CliApiClient {
320320
);
321321
}
322322

323-
async archiveBranch(projectRef: string, branch: string) {
323+
async archiveBranch(projectRef: string, env: string, branch: string) {
324324
if (!this.accessToken) {
325325
throw new Error("archiveBranch: No access token");
326326
}
@@ -331,7 +331,7 @@ export class CliApiClient {
331331
{
332332
method: "POST",
333333
headers: this.getHeaders(),
334-
body: JSON.stringify({ branch }),
334+
body: JSON.stringify({ env, branch }),
335335
}
336336
);
337337
}
@@ -694,6 +694,7 @@ export class CliApiClient {
694694
headers: {
695695
Authorization: `Bearer ${this.accessToken}`,
696696
Accept: "application/json",
697+
...this.getBranchHeader(),
697698
},
698699
});
699700
}
@@ -714,6 +715,7 @@ export class CliApiClient {
714715
headers: {
715716
...init?.headers,
716717
Authorization: `Bearer ${this.accessToken}`,
718+
...this.getBranchHeader(),
717719
},
718720
}),
719721
});
@@ -766,6 +768,7 @@ export class CliApiClient {
766768
headers: {
767769
Authorization: `Bearer ${this.accessToken}`,
768770
Accept: "application/json",
771+
...this.getBranchHeader(),
769772
},
770773
body: JSON.stringify(body),
771774
});
@@ -783,6 +786,7 @@ export class CliApiClient {
783786
headers: {
784787
Authorization: `Bearer ${this.accessToken}`,
785788
Accept: "application/json",
789+
...this.getBranchHeader(),
786790
},
787791
body: JSON.stringify(body),
788792
});
@@ -802,6 +806,7 @@ export class CliApiClient {
802806
Authorization: `Bearer ${this.accessToken}`,
803807
Accept: "application/json",
804808
"Content-Type": "application/json",
809+
...this.getBranchHeader(),
805810
},
806811
body: JSON.stringify(body),
807812
});
@@ -818,6 +823,7 @@ export class CliApiClient {
818823
headers: {
819824
Authorization: `Bearer ${this.accessToken}`,
820825
Accept: "application/json",
826+
...this.getBranchHeader(),
821827
},
822828
}
823829
);
@@ -837,6 +843,7 @@ export class CliApiClient {
837843
Authorization: `Bearer ${this.accessToken}`,
838844
Accept: "application/json",
839845
"Content-Type": "application/json",
846+
...this.getBranchHeader(),
840847
},
841848
body: JSON.stringify(body),
842849
}
@@ -855,6 +862,7 @@ export class CliApiClient {
855862
headers: {
856863
Authorization: `Bearer ${this.accessToken}`,
857864
Accept: "application/json",
865+
...this.getBranchHeader(),
858866
},
859867
//no body at the moment, but we'll probably add things soon
860868
body: JSON.stringify({}),
@@ -875,6 +883,7 @@ export class CliApiClient {
875883
headers: {
876884
Authorization: `Bearer ${this.accessToken}`,
877885
Accept: "application/json",
886+
...this.getBranchHeader(),
878887
},
879888
body: JSON.stringify(body),
880889
}
@@ -898,4 +907,8 @@ export class CliApiClient {
898907

899908
return headers;
900909
}
910+
911+
private getBranchHeader(): Record<string, string> {
912+
return this.branch ? { "x-trigger-branch": this.branch } : {};
913+
}
901914
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ import { getProjectClient, upsertBranch } from "../utilities/session.js";
5757
import { getTmpDir } from "../utilities/tempDirectories.js";
5858
import { spinner } from "../utilities/windows.js";
5959
import { login } from "./login.js";
60-
import { archivePreviewBranch } from "./preview.js";
6160
import { updateTriggerPackages } from "./update.js";
61+
import { archivePreviewBranch } from "./preview.js";
6262

6363
const DeployCommandOptions = CommonCommandOptions.extend({
6464
dryRun: z.boolean().default(false),

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

Lines changed: 151 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import { intro } from "@clack/prompts";
2+
import { resolve } from "node:path";
3+
import { spinner } from "../utilities/windows.js";
4+
import { loadConfig } from "../config.js";
5+
import { verifyDirectory } from "./deploy.js";
16
import { ResolvedConfig } from "@trigger.dev/core/v3/build";
27
import { Command, Option as CommandOption } from "commander";
38
import { z } from "zod";
49
import { CliApiClient } from "../apiClient.js";
5-
import { CommonCommandOptions, commonOptions, wrapCommandAction } from "../cli/common.js";
10+
import { CommonCommandOptions, commonOptions, handleTelemetry, wrapCommandAction } from "../cli/common.js";
611
import { watchConfig } from "../config.js";
712
import { DevSessionInstance, startDevSession } from "../dev/devSession.js";
813
import { createLockFile } from "../dev/lock.js";
@@ -27,11 +32,22 @@ import { installMcpServer } from "./install-mcp.js";
2732
import { tryCatch } from "@trigger.dev/core/utils";
2833
import { VERSION } from "@trigger.dev/core";
2934
import { initiateSkillsInstallWizard } from "./skills.js";
35+
import { getDevBranch } from "@trigger.dev/core/v3";
36+
37+
const DevArchiveCommandOptions = CommonCommandOptions.extend({
38+
branch: z.string().optional(),
39+
config: z.string().optional(),
40+
projectRef: z.string().optional(),
41+
skipUpdateCheck: z.boolean().default(false),
42+
});
43+
44+
type DevArchiveCommandOptions = z.infer<typeof DevArchiveCommandOptions>;
3045

3146
const DevCommandOptions = CommonCommandOptions.extend({
3247
debugOtel: z.boolean().default(false),
3348
config: z.string().optional(),
3449
projectRef: z.string().optional(),
50+
branch: z.string().optional(),
3551
skipUpdateCheck: z.boolean().default(false),
3652
skipPlatformNotifications: z.boolean().default(false),
3753
envFile: z.string().optional(),
@@ -48,15 +64,45 @@ const DevCommandOptions = CommonCommandOptions.extend({
4864
export type DevCommandOptions = z.infer<typeof DevCommandOptions>;
4965

5066
export function configureDevCommand(program: Command) {
51-
return commonOptions(
52-
program
53-
.command("dev")
54-
.description("Run your Trigger.dev tasks locally")
67+
const devBase = program.command("dev").description("Run your Trigger.dev tasks locally");
68+
69+
commonOptions(
70+
devBase
71+
.command("archive")
72+
.description("Archive a dev branch")
73+
.argument("[path]", "The path to the project", ".")
74+
.option(
75+
"-b, --branch <branch>",
76+
"The dev branch to archive. If not provided, we'll detect your local git branch."
77+
)
78+
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
79+
.option("-c, --config <config file>", "The name of the config file, found at [path]")
80+
.option(
81+
"-p, --project-ref <project ref>",
82+
"The project ref. Required if there is no config file. This will override the project specified in the config file."
83+
)
84+
.option(
85+
"--env-file <env file>",
86+
"Path to the .env file to load into the CLI process. Defaults to .env in the project directory."
87+
)
88+
).action(async (path, options) => {
89+
await handleTelemetry(async () => {
90+
await printStandloneInitialBanner(true, options.profile);
91+
await devArchiveCommand(path, options);
92+
});
93+
});
94+
95+
commonOptions(
96+
devBase
5597
.option("-c, --config <config file>", "The name of the config file")
5698
.option(
5799
"-p, --project-ref <project ref>",
58100
"The project ref. Required if there is no config file."
59101
)
102+
.option(
103+
"-b, --branch <branch>",
104+
"The dev branch to use. If not provided, we'll use the default branch."
105+
)
60106
.option(
61107
"--env-file <env file>",
62108
"Path to the .env file to use for the dev session. Defaults to .env in the project directory."
@@ -164,8 +210,7 @@ export async function devCommand(options: DevCommandOptions) {
164210
);
165211
} else {
166212
logger.log(
167-
`${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${
168-
authorization.error
213+
`${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${authorization.error
169214
}`
170215
);
171216
}
@@ -198,12 +243,14 @@ async function startDev(options: StartDevOptions) {
198243
logger.loggerLevel = options.logLevel;
199244
}
200245

246+
const apiClient = new CliApiClient(options.login.auth.apiUrl, options.login.auth.accessToken);
247+
201248
const notificationPromise = options.skipPlatformNotifications
202249
? undefined
203250
: fetchPlatformNotification({
204-
apiClient: new CliApiClient(options.login.auth.apiUrl, options.login.auth.accessToken),
205-
projectRef: options.projectRef,
206-
});
251+
apiClient,
252+
projectRef: options.projectRef,
253+
});
207254

208255
await printStandloneInitialBanner(true, options.profile);
209256

@@ -215,7 +262,9 @@ async function startDev(options: StartDevOptions) {
215262
displayedUpdateMessage = await updateTriggerPackages(options.cwd, { ...options }, true, true);
216263
}
217264

218-
const removeLockFile = await createLockFile(options.cwd);
265+
const branch = getDevBranch({ specified: options.branch });
266+
267+
const removeLockFile = await createLockFile(options.cwd, branch);
219268

220269
let devInstance: DevSessionInstance | undefined;
221270

@@ -246,13 +295,18 @@ async function startDev(options: StartDevOptions) {
246295

247296
logger.debug("Initial config", watcher.config);
248297

298+
if (branch !== "default") {
299+
await apiClient.upsertBranch(watcher.config.project, { branch, env: "development" });
300+
}
301+
249302
// eslint-disable-next-line no-inner-declarations
250303
async function bootDevSession(configParam: ResolvedConfig) {
251304
const projectClient = await getProjectClient({
252305
accessToken: options.login.auth.accessToken,
253306
apiUrl: options.login.auth.apiUrl,
254307
projectRef: configParam.project,
255308
env: "dev",
309+
branch,
256310
profile: options.profile,
257311
});
258312

@@ -262,6 +316,7 @@ async function startDev(options: StartDevOptions) {
262316

263317
return startDevSession({
264318
name: projectClient.name,
319+
branch,
265320
rawArgs: options,
266321
rawConfig: configParam,
267322
client: projectClient.client,
@@ -274,7 +329,7 @@ async function startDev(options: StartDevOptions) {
274329

275330
devInstance = await bootDevSession(watcher.config);
276331

277-
const waitUntilExit = async () => {};
332+
const waitUntilExit = async () => { };
278333

279334
return {
280335
watcher,
@@ -290,3 +345,87 @@ async function startDev(options: StartDevOptions) {
290345
throw error;
291346
}
292347
}
348+
349+
async function devArchiveCommand(dir: string, options: unknown) {
350+
return await wrapCommandAction(
351+
"devArchiveCommand",
352+
DevArchiveCommandOptions,
353+
options,
354+
async (opts) => {
355+
return await archiveDevBranchCommand(dir, opts);
356+
}
357+
);
358+
}
359+
360+
361+
async function archiveDevBranchCommand(dir: string, options: DevArchiveCommandOptions) {
362+
intro(`Archiving dev branch`);
363+
364+
if (!options.skipUpdateCheck) {
365+
await updateTriggerPackages(dir, { ...options }, true, true);
366+
}
367+
368+
const cwd = process.cwd();
369+
const projectPath = resolve(cwd, dir);
370+
371+
verifyDirectory(dir, projectPath);
372+
373+
const authorization = await login({
374+
embedded: true,
375+
defaultApiUrl: options.apiUrl,
376+
profile: options.profile,
377+
});
378+
379+
if (!authorization.ok) {
380+
if (authorization.error === "fetch failed") {
381+
throw new Error(
382+
`Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?`
383+
);
384+
} else {
385+
throw new Error(
386+
`You must login first. Use the \`login\` CLI command.\n\n${authorization.error}`
387+
);
388+
}
389+
}
390+
391+
const resolvedConfig = await loadConfig({
392+
cwd: projectPath,
393+
overrides: { project: options.projectRef },
394+
configFile: options.config,
395+
});
396+
397+
logger.debug("Resolved config", resolvedConfig);
398+
399+
const branch = getDevBranch({ specified: options.branch });
400+
401+
if (!branch) {
402+
throw new Error(
403+
"Didn't auto-detect branch, so you need to specify a dev branch. Use --branch <branch>."
404+
);
405+
}
406+
407+
const $buildSpinner = spinner();
408+
$buildSpinner.start(`Archiving "${branch}"`);
409+
const result = await archiveDevBranch(authorization, branch, resolvedConfig.project);
410+
$buildSpinner.stop(
411+
result ? `Successfully archived "${branch}"` : `Failed to archive "${branch}".`
412+
);
413+
return result;
414+
}
415+
416+
async function archiveDevBranch(
417+
authorization: LoginResultOk,
418+
branch: string,
419+
project: string
420+
) {
421+
const apiClient = new CliApiClient(authorization.auth.apiUrl, authorization.auth.accessToken);
422+
423+
const result = await apiClient.archiveBranch(project, "development", branch);
424+
425+
if (result.success) {
426+
return true;
427+
} else {
428+
logger.error(result.error);
429+
return false;
430+
}
431+
}

0 commit comments

Comments
 (0)