Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2b35c59
chore(db): backfill isBranchableEnvironment for existing dev environm…
carderne Jun 19, 2026
39cb208
feat(webapp): make development environments branchable (API + auth)
carderne Jun 19, 2026
1fbf92c
feat(webapp): dashboard for dev branches
carderne Jun 19, 2026
9b90c28
feat(cli): dev branch support
carderne Jun 19, 2026
f24f396
use parentEnvironmentId instead of isBranchable for dev
carderne Jun 22, 2026
0841d7e
Revert "chore(db): backfill isBranchableEnvironment for existing dev …
carderne Jun 22, 2026
d13cbef
consistent dev/preview branch differentiation
carderne Jun 22, 2026
16ee1ff
add docs
carderne Jun 22, 2026
7c143c9
multi presence redis query use mget
carderne Jun 22, 2026
4249289
remove dev branch upgrade button
carderne Jun 22, 2026
168b1f4
add tests
carderne Jun 22, 2026
5dde6c6
feature flag
carderne Jun 22, 2026
5ae3445
add changeset
carderne Jun 22, 2026
fb716f0
out of dev branches error
carderne Jun 22, 2026
1011cde
some fixes and tests
carderne Jun 22, 2026
89bbec9
fix dev build namespacing
carderne Jun 22, 2026
c86695f
fix cli dev subcommand
carderne Jun 22, 2026
17d5e26
scope dev branch correctly
carderne Jun 23, 2026
902c9a2
cli guards
carderne Jun 23, 2026
14f4c53
redis multi
carderne Jun 23, 2026
ce41b72
clean up branches/dev-branches routes
carderne Jun 23, 2026
9df5006
improve default branch handling and errors
carderne Jun 23, 2026
a900436
cleaning up nits
carderne Jun 23, 2026
5d0c7d1
fix rbac test
carderne Jun 23, 2026
6050490
improve backwards compat
carderne Jun 23, 2026
1a24302
fix dev command env file resolution
carderne Jun 23, 2026
7a425ee
fix bugs from coderabbit
carderne Jun 23, 2026
99d2498
simplify new env var logic
carderne Jun 23, 2026
fd20cf2
more env filtering correctness
carderne Jun 23, 2026
47df38a
catch eager prisma connect
carderne Jun 23, 2026
5c5508a
fix nits
carderne Jun 23, 2026
fe3e4c9
improve var name in rbac
carderne Jun 25, 2026
b008343
add auto revalidate to dev-branches page
carderne Jun 25, 2026
9ffbbc5
bump @trigger.dev/platform dep
carderne Jun 25, 2026
4c311d6
show root dev in concurrency mgmt
carderne Jun 25, 2026
32409b4
fixes from code review
carderne Jun 25, 2026
2475580
only catch db.connect when testing
carderne Jun 25, 2026
e982fc4
centralise dev branch path hashing
carderne Jun 25, 2026
27379cf
add .server-change
carderne Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/dev-branches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
Comment thread
carderne marked this conversation as resolved.
"@trigger.dev/core": patch
---

Add support for dev branches to the webapp and CLI. This allows humans (and agents) to run multiple local dev servers simultaneously, with a separate dashboard for each one.
6 changes: 6 additions & 0 deletions .server-changes/dev-branches.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Adds support for dev branches similar to the preview branches already supported.
18 changes: 11 additions & 7 deletions apps/webapp/app/components/BlankStatePanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { useFeatures } from "~/hooks/useFeatures";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { type MinimumEnvironment } from "~/presenters/SelectBestEnvironmentPresenter.server";
import { NewBranchPanel } from "~/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route";
import { type BranchableEnvironmentToken } from "~/utils/branchableEnvironment";
import { NewBranchPanel } from "~/routes/resources.branches.create";
import { GitHubSettingsPanel } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.github";
import {
docsPath,
Expand Down Expand Up @@ -488,24 +489,27 @@ export function BranchesNoBranchableEnvironment({ showSelfServe }: { showSelfSer
}

export function BranchesNoBranches({
parentEnvironment,
env,
limits,
canUpgrade,
showSelfServe,
}: {
parentEnvironment: { id: string };
env: BranchableEnvironmentToken;
limits: { used: number; limit: number };
canUpgrade: boolean;
showSelfServe: boolean;
}) {
const organization = useOrganization();

const envTextClassName = env === "preview" ? "text-preview" : "text-dev";
const branchesLabel = env === "preview" ? "preview branches" : "dev branches";

if (limits.used >= limits.limit) {
return (
<InfoPanel
title="Upgrade to get preview branches"
title={`Upgrade to get ${branchesLabel}`}
icon={BranchEnvironmentIconSmall}
iconClassName="text-preview"
iconClassName={envTextClassName}
panelClassName="max-w-full"
accessory={
showSelfServe && canUpgrade ? (
Expand Down Expand Up @@ -536,7 +540,7 @@ export function BranchesNoBranches({
<InfoPanel
title="Create your first branch"
icon={BranchEnvironmentIconSmall}
iconClassName="text-preview"
iconClassName={envTextClassName}
panelClassName="max-w-full"
accessory={
<NewBranchPanel
Expand All @@ -549,7 +553,7 @@ export function BranchesNoBranches({
New branch
</Button>
}
parentEnvironment={parentEnvironment}
env={env}
/>
}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/DevPresence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function DevPresenceProvider({ children, enabled = true }: DevPresencePro

// Only subscribe to event source if enabled is true
const streamedEvents = useEventSource(
`/resources/orgs/${organization.slug}/projects/${project.slug}/dev/presence`,
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/presence`,
Comment thread
carderne marked this conversation as resolved.
{
event: "presence",
disabled: !enabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function environmentFullTitle(environment: Environment) {
}
}

export function environmentTextClassName(environment: Environment) {
export function environmentTextClassName(environment: { type: Environment["type"] }) {
switch (environment.type) {
case "PRODUCTION":
return "text-prod";
Expand Down
105 changes: 65 additions & 40 deletions apps/webapp/app/components/navigation/EnvironmentSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ChevronRightIcon, Cog8ToothIcon } from "@heroicons/react/20/solid";
import { DEFAULT_DEV_BRANCH } from "@trigger.dev/core/v3/utils/gitBranch";
import { isBranchableEnvironment } from "~/utils/branchableEnvironment";
import { DropdownIcon } from "~/assets/icons/DropdownIcon";
import { useNavigation } from "@remix-run/react";
import { useEffect, useRef, useState } from "react";
Expand All @@ -9,8 +11,8 @@ import { useFeatures } from "~/hooks/useFeatures";
import { useOrganization, type MatchedOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { cn } from "~/utils/cn";
import { branchesPath, docsPath, v3BillingPath } from "~/utils/pathBuilder";
import { EnvironmentCombo, EnvironmentIcon, EnvironmentLabel, environmentFullTitle } from "../environments/EnvironmentLabel";
import { branchesPath, branchesDevPath, docsPath, v3BillingPath } from "~/utils/pathBuilder";
import { EnvironmentCombo, EnvironmentIcon, EnvironmentLabel, environmentFullTitle, environmentTextClassName } from "../environments/EnvironmentLabel";
import { ButtonContent } from "../primitives/Buttons";
import { Header2 } from "../primitives/Headers";
import { Paragraph } from "../primitives/Paragraph";
Expand Down Expand Up @@ -50,6 +52,7 @@ export function EnvironmentSelector({
}, [navigation.location?.pathname]);

const hasStaging = project.environments.some((env) => env.type === "STAGING");
const devBranchesEnabled = Boolean(organization.featureFlags?.devBranchesEnabled);
Comment thread
carderne marked this conversation as resolved.

return (
<Popover onOpenChange={(open) => setIsMenuOpen(open)} open={isMenuOpen}>
Expand Down Expand Up @@ -104,34 +107,40 @@ export function EnvironmentSelector({
>
<div className="flex flex-col gap-1 p-1">
{project.environments
.filter((env) => env.branchName === null)
.filter((env) => env.parentEnvironmentId === null)
.map((env) => {
switch (env.isBranchableEnvironment) {
case true: {
const branchEnvironments = project.environments.filter(
(e) => e.parentEnvironmentId === env.id
);
return (
<Branches
key={env.id}
parentEnvironment={env}
branchEnvironments={branchEnvironments}
currentEnvironment={environment}
/>
);
}
case false:
return (
<PopoverMenuItem
key={env.id}
to={urlForEnvironment(env)}
title={
<EnvironmentCombo environment={env} className="mx-auto grow text-2sm" />
}
isSelected={env.id === environment.id}
/>
);
// DEVELOPMENT is only branchable in the UI when the org has the
// multi-branch dev flag on. Without it, dev renders as a plain
// selector button (the original behavior). PREVIEW is unaffected.
const renderAsBranchable =
isBranchableEnvironment(env) &&
(env.type !== "DEVELOPMENT" || devBranchesEnabled);

if (renderAsBranchable) {
const branchEnvironments = project.environments.filter(
(e) => e.parentEnvironmentId === env.id
);
const allBranchEnvironments = env.type === "DEVELOPMENT" ? [env, ...branchEnvironments] : branchEnvironments;
Comment thread
carderne marked this conversation as resolved.
return (
<Branches
key={env.id}
parentEnvironment={env}
branchEnvironments={allBranchEnvironments}
currentEnvironment={environment}
/>
);
}

return (
<PopoverMenuItem
key={env.id}
to={urlForEnvironment(env)}
title={
<EnvironmentCombo environment={env} className="mx-auto grow text-2sm" />
}
isSelected={env.id === environment.id}
/>
);
})}
</div>
{!hasStaging && isManagedCloud && (
Expand Down Expand Up @@ -226,7 +235,14 @@ function Branches({
? "no-active-branches"
: "has-branches";

const currentBranchIsArchived = environment.archivedAt !== null;
// Only surface the active environment's archived-branch item in the submenu it
// actually belongs to. Both Development and Preview render this component, so
// without the parent check an archived dev branch would leak into the Preview
// submenu (and vice-versa).
const currentBranchIsArchived =
environment.archivedAt !== null && environment.parentEnvironmentId === parentEnvironment.id;

const envTextClassName = environmentTextClassName(parentEnvironment);

return (
<Popover onOpenChange={(open) => setMenuOpen(open)} open={isMenuOpen}>
Expand Down Expand Up @@ -260,11 +276,11 @@ function Branches({
to={urlForEnvironment(environment)}
title={
<>
<span className="block w-full text-preview">{environment.branchName}</span>
<span className={cn("block w-full", envTextClassName)}>{environment.branchName}</span>
<Badge variant="extra-small">Archived</Badge>
</>
}
icon={<BranchEnvironmentIconSmall className="size-4 shrink-0 text-preview" />}
icon={<BranchEnvironmentIconSmall className={cn("size-4 shrink-0", envTextClassName)} />}
isSelected={environment.id === currentEnvironment.id}
/>
)}
Expand All @@ -276,16 +292,16 @@ function Branches({
<PopoverMenuItem
key={env.id}
to={urlForEnvironment(env)}
title={<span className="block w-full text-preview">{env.branchName}</span>}
icon={<BranchEnvironmentIconSmall className="size-4 shrink-0 text-preview" />}
title={<span className={cn("block w-full", envTextClassName)}>{env.branchName ?? DEFAULT_DEV_BRANCH}</span>}
icon={<BranchEnvironmentIconSmall className={cn("size-4 shrink-0", envTextClassName)} />}
isSelected={env.id === currentEnvironment.id}
/>
))}
</>
) : state === "no-branches" ? (
<div className="flex max-w-sm flex-col gap-1 p-2">
<div className="flex items-center gap-1">
<BranchEnvironmentIconSmall className="size-4 text-preview" />
<BranchEnvironmentIconSmall className={cn("size-4", envTextClassName)} />
<Header2>Create your first branch</Header2>
</div>
<Paragraph spacing variant="small">
Expand All @@ -305,12 +321,21 @@ function Branches({
)}
</div>
<div className="border-t border-charcoal-700 p-1">
<PopoverMenuItem
to={branchesPath(organization, project, environment)}
title="Manage branches"
icon={<Cog8ToothIcon className="size-4 text-text-dimmed" />}
leadingIconClassName="text-text-dimmed"
/>
{parentEnvironment.type === "DEVELOPMENT" ? (
<PopoverMenuItem
to={branchesDevPath(organization, project, environment)}
title="Manage dev branches"
icon={<Cog8ToothIcon className="size-4 text-text-dimmed" />}
leadingIconClassName="text-text-dimmed"
/>
) : (
<PopoverMenuItem
to={branchesPath(organization, project, environment)}
title="Manage preview branches"
icon={<Cog8ToothIcon className="size-4 text-text-dimmed" />}
leadingIconClassName="text-text-dimmed"
/>
)}
</div>
</PopoverContent>
</div>
Expand Down
20 changes: 16 additions & 4 deletions apps/webapp/app/db.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,14 @@ function getClient() {
queryPerformanceMonitor.onQuery("writer", log);
});

// connect eagerly
client.$connect();
// Connect eagerly; Prisma will connect on use anyway.
// Swallow the error when testing (DB likely unavailable)
const connectPromise = client.$connect();
if (env.NODE_ENV === "test") {
connectPromise.catch((error) => {
logger.warn("Failed to eagerly connect prisma client (writer)", { error });
});
}

console.log(`🔌 prisma client connected`);

Expand Down Expand Up @@ -378,8 +384,14 @@ function getReplicaClient() {
queryPerformanceMonitor.onQuery("replica", log);
});

// connect eagerly
replicaClient.$connect();
// Connect eagerly; Prisma will connect on use anyway.
// Swallow the error when testing (DB likely unavailable)
const connectPromise = replicaClient.$connect();
if (env.NODE_ENV === "test") {
connectPromise.catch((error) => {
logger.warn("Failed to eagerly connect prisma client (replica)", { error });
});
}

console.log(`🔌 read replica connected`);

Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/models/member.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ export async function acceptInvite({
organization: invite.organization,
project,
type: "DEVELOPMENT",
isBranchableEnvironment: false,
// We set this true but no backfill (yet!?) so never used
Comment thread
carderne marked this conversation as resolved.
// for dev environments
isBranchableEnvironment: true,
member,
prismaClient: tx,
});
Expand Down
4 changes: 3 additions & 1 deletion apps/webapp/app/models/project.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ export async function createProject(
organization,
project,
type: "DEVELOPMENT",
isBranchableEnvironment: false,
// We set this true but no backfill (yet!?) so never used
// for dev environments
isBranchableEnvironment: true,
member,
});
}
Expand Down
Loading