Skip to content

Commit d5f2712

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 1863152 commit d5f2712

13 files changed

Lines changed: 223 additions & 53 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: 16 additions & 8 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
}
@@ -886,16 +895,15 @@ export class CliApiClient {
886895
}
887896

888897
private getHeaders() {
889-
const headers: Record<string, string> = {
898+
return {
890899
Authorization: `Bearer ${this.accessToken}`,
891900
"Content-Type": "application/json",
892901
"x-trigger-source": this.source,
902+
...this.getBranchHeader(),
893903
};
904+
}
894905

895-
if (this.branch) {
896-
headers["x-trigger-branch"] = this.branch;
897-
}
898-
899-
return headers;
906+
private getBranchHeader(): Record<string, string> {
907+
return this.branch ? { "x-trigger-branch": this.branch } : {};
900908
}
901909
}

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: 152 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,23 @@ 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+
import { isDefaultDevBranch } from "@trigger.dev/core/v3/utils/gitBranch";
37+
38+
const DevArchiveCommandOptions = CommonCommandOptions.extend({
39+
branch: z.string().optional(),
40+
config: z.string().optional(),
41+
projectRef: z.string().optional(),
42+
skipUpdateCheck: z.boolean().default(false),
43+
});
44+
45+
type DevArchiveCommandOptions = z.infer<typeof DevArchiveCommandOptions>;
3046

3147
const DevCommandOptions = CommonCommandOptions.extend({
3248
debugOtel: z.boolean().default(false),
3349
config: z.string().optional(),
3450
projectRef: z.string().optional(),
51+
branch: z.string().optional(),
3552
skipUpdateCheck: z.boolean().default(false),
3653
skipPlatformNotifications: z.boolean().default(false),
3754
envFile: z.string().optional(),
@@ -48,15 +65,45 @@ const DevCommandOptions = CommonCommandOptions.extend({
4865
export type DevCommandOptions = z.infer<typeof DevCommandOptions>;
4966

5067
export function configureDevCommand(program: Command) {
51-
return commonOptions(
52-
program
53-
.command("dev")
54-
.description("Run your Trigger.dev tasks locally")
68+
const devBase = program.command("dev").description("Run your Trigger.dev tasks locally");
69+
70+
commonOptions(
71+
devBase
72+
.command("archive")
73+
.description("Archive a dev branch")
74+
.argument("[path]", "The path to the project", ".")
75+
.option(
76+
"-b, --branch <branch>",
77+
"The dev branch to archive. If not provided, we'll detect your local git branch."
78+
)
79+
.option("--skip-update-check", "Skip checking for @trigger.dev package updates")
80+
.option("-c, --config <config file>", "The name of the config file, found at [path]")
81+
.option(
82+
"-p, --project-ref <project ref>",
83+
"The project ref. Required if there is no config file. This will override the project specified in the config file."
84+
)
85+
.option(
86+
"--env-file <env file>",
87+
"Path to the .env file to load into the CLI process. Defaults to .env in the project directory."
88+
)
89+
).action(async (path, options) => {
90+
await handleTelemetry(async () => {
91+
await printStandloneInitialBanner(true, options.profile);
92+
await devArchiveCommand(path, options);
93+
});
94+
});
95+
96+
commonOptions(
97+
devBase
5598
.option("-c, --config <config file>", "The name of the config file")
5699
.option(
57100
"-p, --project-ref <project ref>",
58101
"The project ref. Required if there is no config file."
59102
)
103+
.option(
104+
"-b, --branch <branch>",
105+
"The dev branch to use. If not provided, we'll use the default branch."
106+
)
60107
.option(
61108
"--env-file <env file>",
62109
"Path to the .env file to use for the dev session. Defaults to .env in the project directory."
@@ -164,8 +211,7 @@ export async function devCommand(options: DevCommandOptions) {
164211
);
165212
} else {
166213
logger.log(
167-
`${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${
168-
authorization.error
214+
`${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${authorization.error
169215
}`
170216
);
171217
}
@@ -198,12 +244,14 @@ async function startDev(options: StartDevOptions) {
198244
logger.loggerLevel = options.logLevel;
199245
}
200246

247+
const apiClient = new CliApiClient(options.login.auth.apiUrl, options.login.auth.accessToken);
248+
201249
const notificationPromise = options.skipPlatformNotifications
202250
? undefined
203251
: fetchPlatformNotification({
204-
apiClient: new CliApiClient(options.login.auth.apiUrl, options.login.auth.accessToken),
205-
projectRef: options.projectRef,
206-
});
252+
apiClient,
253+
projectRef: options.projectRef,
254+
});
207255

208256
await printStandloneInitialBanner(true, options.profile);
209257

@@ -215,7 +263,9 @@ async function startDev(options: StartDevOptions) {
215263
displayedUpdateMessage = await updateTriggerPackages(options.cwd, { ...options }, true, true);
216264
}
217265

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

220270
let devInstance: DevSessionInstance | undefined;
221271

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

247297
logger.debug("Initial config", watcher.config);
248298

299+
if (!isDefaultDevBranch(branch)) {
300+
await apiClient.upsertBranch(watcher.config.project, { branch, env: "development" });
301+
}
302+
249303
// eslint-disable-next-line no-inner-declarations
250304
async function bootDevSession(configParam: ResolvedConfig) {
251305
const projectClient = await getProjectClient({
252306
accessToken: options.login.auth.accessToken,
253307
apiUrl: options.login.auth.apiUrl,
254308
projectRef: configParam.project,
255309
env: "dev",
310+
branch,
256311
profile: options.profile,
257312
});
258313

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

263318
return startDevSession({
264319
name: projectClient.name,
320+
branch,
265321
rawArgs: options,
266322
rawConfig: configParam,
267323
client: projectClient.client,
@@ -274,7 +330,7 @@ async function startDev(options: StartDevOptions) {
274330

275331
devInstance = await bootDevSession(watcher.config);
276332

277-
const waitUntilExit = async () => {};
333+
const waitUntilExit = async () => { };
278334

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

0 commit comments

Comments
 (0)