Skip to content

Commit 4764978

Browse files
d-csTrigger.dev RepoOps
authored andcommitted
feat(webapp): auto-archive inactive preview branches
Add opt-in preview branch auto-archiving after a configurable period without deployments. Projects can protect named branches and preview the effect of their settings before saving. Availability is controlled by an organization or global feature flag and is disabled by default. Mono-RevId: 96ef71472cc849ed8fffbef47fc4503987ba6288
1 parent 9c870f0 commit 4764978

20 files changed

Lines changed: 1829 additions & 31 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Automatically archive preview branches after a configurable period without deployments, with protected branch names and a preview of affected branches.

apps/webapp/app/presenters/v3/BranchesPresenter.server.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { classifyPreviewBranch } from "~/utils/previewAutoArchive";
2+
import {
3+
previewBranchActivity,
4+
isPreviewAutoArchiveEnabled,
5+
} from "~/services/previewAutoArchive.server";
16
import { GitMeta } from "@trigger.dev/core/v3";
27
import { DEFAULT_DEV_BRANCH } from "@trigger.dev/core/v3/utils/gitBranch";
38
import { type RuntimeEnvironmentType } from "@trigger.dev/database";
@@ -103,10 +108,12 @@ export class BranchesPresenter {
103108
projectSlug: Project["slug"];
104109
env: BranchableEnvironmentToken;
105110
} & Options) {
111+
const envType = toBranchableEnvironmentType(env);
106112
const project = await this.#prismaClient.project.findFirst({
107113
select: {
108114
id: true,
109115
organizationId: true,
116+
...(envType === "PREVIEW" ? { organization: { select: { featureFlags: true } } } : {}),
110117
},
111118
where: {
112119
slug: projectSlug,
@@ -124,11 +131,18 @@ export class BranchesPresenter {
124131
throw new Error("Project not found");
125132
}
126133

127-
const envType = toBranchableEnvironmentType(env);
134+
const autoArchiveAvailable =
135+
envType === "PREVIEW" &&
136+
(await isPreviewAutoArchiveEnabled(
137+
this.#prismaClient,
138+
project.organization?.featureFlags ?? null
139+
));
128140

129141
const branchableEnvironment = await this.#prismaClient.runtimeEnvironment.findFirst({
130142
select: {
131143
id: true,
144+
previewAutoArchiveAfterDays: true,
145+
previewAutoArchiveExcludedBranches: true,
132146
},
133147
where: {
134148
projectId: project.id,
@@ -149,6 +163,7 @@ export class BranchesPresenter {
149163
}
150164
return {
151165
branchableEnvironment: null,
166+
autoArchiveAvailable,
152167
currentPage: page,
153168
totalPages: 0,
154169
hasBranches: false,
@@ -235,10 +250,30 @@ export class BranchesPresenter {
235250
},
236251
});
237252

253+
const archiveActivity = autoArchiveAvailable
254+
? await previewBranchActivity(
255+
this.#prismaClient,
256+
branches.map(({ id }) => id)
257+
)
258+
: new Map<string, { lastDeploymentAt: Date | null; inProgress: boolean }>();
259+
260+
const excluded = new Set(branchableEnvironment.previewAutoArchiveExcludedBranches);
261+
const archiveNow = new Date();
238262
const branchesFiltered = branches
239263
.filter((branch) => envType === "DEVELOPMENT" || branch.branchName !== null)
240264
.map((branch) => ({
241265
...branch,
266+
lastDeploymentAt: archiveActivity.get(branch.id)?.lastDeploymentAt ?? null,
267+
autoArchive:
268+
autoArchiveAvailable && branchableEnvironment.previewAutoArchiveAfterDays !== null
269+
? classifyPreviewBranch(
270+
branch,
271+
archiveActivity.get(branch.id)!,
272+
branchableEnvironment.previewAutoArchiveAfterDays,
273+
excluded,
274+
archiveNow
275+
)
276+
: null,
242277
git: processGitMetadata(branch.git),
243278
branchName: branch.branchName ?? DEFAULT_DEV_BRANCH,
244279
}));
@@ -252,6 +287,7 @@ export class BranchesPresenter {
252287

253288
return {
254289
branchableEnvironment,
290+
autoArchiveAvailable,
255291
currentPage: page,
256292
totalPages: Math.ceil(visibleCount / BRANCHES_PER_PAGE),
257293
hasBranches: totalBranches > 0,

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.branches/route.tsx

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AutoArchiveSettings } from "~/routes/resources.branches.auto-archive";
12
import { getFormProps, getInputProps, useForm } from "@conform-to/react";
23
import { parseWithZod } from "@conform-to/zod/v4";
34
import { ArrowUpCircleIcon, CheckIcon, EnvelopeIcon, PlusIcon } from "@heroicons/react/20/solid";
@@ -6,7 +7,7 @@ import { DialogClose } from "@radix-ui/react-dialog";
67
import { useFetcher, useSearchParams } from "@remix-run/react";
78
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
89
import { tryCatch } from "@trigger.dev/core/v3";
9-
import { useCallback, useEffect, useState } from "react";
10+
import { type ReactNode, useCallback, useEffect, useState } from "react";
1011
import { SearchInput } from "~/components/primitives/SearchInput";
1112
import { typedjson, useTypedLoaderData } from "remix-typedjson";
1213
import { z } from "zod";
@@ -223,6 +224,7 @@ export default function Page() {
223224
hasBranches,
224225
canPurchaseBranches,
225226
canManageBranches,
227+
autoArchiveAvailable,
226228
extraBranches,
227229
branchPricing,
228230
maxBranchQuota,
@@ -321,6 +323,21 @@ export default function Page() {
321323
</NavBar>
322324
<PageBody scrollable={false}>
323325
<div className="grid max-h-full min-h-full grid-rows-[auto_1fr_auto]">
326+
<div className="flex items-center justify-between gap-x-1.5 p-2">
327+
<BranchFilters>
328+
{autoArchiveAvailable && (
329+
<AutoArchiveSettings
330+
environment={branchableEnvironment}
331+
canManage={canManageBranches}
332+
/>
333+
)}
334+
</BranchFilters>
335+
<PaginationControls
336+
currentPage={currentPage}
337+
totalPages={totalPages}
338+
showPageNumbers={false}
339+
/>
340+
</div>
324341
{!hasBranches ? (
325342
<MainCenteredContainer className="max-w-md">
326343
<BranchesNoBranches
@@ -333,21 +350,18 @@ export default function Page() {
333350
</MainCenteredContainer>
334351
) : (
335352
<>
336-
<div className="flex items-center justify-between gap-x-1.5 p-2">
337-
<BranchFilters />
338-
<PaginationControls
339-
currentPage={currentPage}
340-
totalPages={totalPages}
341-
showPageNumbers={false}
342-
/>
343-
</div>
344-
345353
<div className="grid max-h-full min-h-full grid-rows-[1fr] overflow-x-auto">
346354
<Table>
347355
<TableHeader>
348356
<TableRow>
349357
<TableHeaderCell>Branch</TableHeaderCell>
350358
<TableHeaderCell>Created</TableHeaderCell>
359+
{autoArchiveAvailable && (
360+
<>
361+
<TableHeaderCell>Last deployment</TableHeaderCell>
362+
<TableHeaderCell>Auto-archive</TableHeaderCell>
363+
</>
364+
)}
351365
<TableHeaderCell>Git</TableHeaderCell>
352366
<TableHeaderCell>Archived</TableHeaderCell>
353367
<TableHeaderCell>
@@ -357,7 +371,7 @@ export default function Page() {
357371
</TableHeader>
358372
<TableBody>
359373
{branches.length === 0 ? (
360-
<TableBlankRow colSpan={5}>
374+
<TableBlankRow colSpan={autoArchiveAvailable ? 7 : 5}>
361375
<Paragraph>There are no matches for your filters</Paragraph>
362376
</TableBlankRow>
363377
) : (
@@ -383,6 +397,33 @@ export default function Page() {
383397
<TableCell className={cellClass}>
384398
<DateTime date={branch.createdAt} />
385399
</TableCell>
400+
{autoArchiveAvailable && (
401+
<>
402+
<TableCell className={cellClass}>
403+
{branch.lastDeploymentAt ? (
404+
<DateTime date={branch.lastDeploymentAt} />
405+
) : (
406+
"Never"
407+
)}
408+
</TableCell>
409+
<TableCell className={cellClass}>
410+
{branch.archivedAt ? (
411+
"–"
412+
) : !branch.autoArchive ? (
413+
"Disabled"
414+
) : branch.autoArchive.status === "protected" ? (
415+
"Never"
416+
) : branch.autoArchive.status === "inProgress" ? (
417+
"Deployment in progress"
418+
) : (
419+
<DateTime
420+
includeTime={false}
421+
date={branch.autoArchive.archiveAt}
422+
/>
423+
)}
424+
</TableCell>
425+
</>
426+
)}
386427
<TableCell className={cellClass}>
387428
<div className="-ml-1 flex items-center">
388429
<GitMetadata git={branch.git} />
@@ -526,7 +567,7 @@ export default function Page() {
526567
);
527568
}
528569

529-
export function BranchFilters() {
570+
export function BranchFilters({ children }: { children?: ReactNode }) {
530571
const [searchParams, setSearchParams] = useSearchParams();
531572
const { showArchived } = BranchesOptions.parse(Object.fromEntries(searchParams.entries()));
532573

@@ -546,14 +587,17 @@ export function BranchFilters() {
546587
);
547588

548589
return (
549-
<div className="flex w-full items-center justify-between gap-2">
590+
<div className="flex w-full flex-wrap items-center justify-between gap-2">
550591
<SearchInput placeholder="Search branch name…" resetParams={["page"]} />
551-
<Switch
552-
checked={showArchived ?? false}
553-
onCheckedChange={handleArchivedChange}
554-
label="Show archived"
555-
variant="secondary/small"
556-
/>
592+
<div className="ml-auto flex flex-wrap items-center gap-2">
593+
<Switch
594+
checked={showArchived ?? false}
595+
onCheckedChange={handleArchivedChange}
596+
label="Show archived"
597+
variant="secondary/small"
598+
/>
599+
{children}
600+
</div>
557601
</div>
558602
);
559603
}

0 commit comments

Comments
 (0)