GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat(webapp): redeploy recent deployments from the dashboard · triggerdotdev/trigger.dev@bdb2f35

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • .server-changes
    • redeploy-recent-deployments.md
  • apps/webapp
    • app
      • env.server.ts
      • presenters/v3
        • DeploymentListPresenter.server.ts
        • DeploymentPresenter.server.ts
      • routes
        • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam
          • route.tsx
        • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments
          • route.tsx
        • resources.$projectId.deployments.$deploymentShortCode.redeploy.ts
      • services
        • enqueueGithubBuild.server.test.ts
        • enqueueGithubBuild.server.ts
        • platform.v3.server.ts
      • v3
        • redeploy.ts
        • services
          • initializeDeployment.server.ts
          • redeployDeployment.server.test.ts
          • redeployDeployment.server.ts
    • vitest.config.ts
  • packages/core/src/v3/schemas
    • api.ts
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎.server-changes/redeploy-recent-deployments.md‎

Copy file name to clipboard
+6Lines changed: 6 additions & 0 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
Recent deployments started from the GitHub integration or a native CLI deploy can be redeployed from the deployments list, rebuilding the same commit or upload as a new deployment.
Collapse file

‎apps/webapp/app/env.server.ts‎

Copy file name to clipboardExpand all lines: apps/webapp/app/env.server.ts
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,6 +2026,7 @@ const EnvironmentSchema = z
20262026
TASK_EVENT_PARTITIONED_WINDOW_IN_SECONDS: z.coerce.number().int().default(60), // 1 minute
20272027

20282028
DEPLOYMENTS_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(5_000),
2029+
DEPLOYMENTS_REDEPLOY_WINDOW_DAYS: z.coerce.number().int().positive().default(7),
20292030
BULK_ACTION_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(1_000),
20302031
QUEUES_AUTORELOAD_POLL_INTERVAL_MS: z.coerce.number().int().default(5_000),
20312032

Collapse file

‎apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts‎

Copy file name to clipboardExpand all lines: apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts
+11-1Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
VercelProjectIntegrationDataSchema,
1515
buildVercelDeploymentUrl,
1616
} from "~/v3/vercel/vercelProjectIntegrationSchema";
17+
import { resolveRedeploySource } from "~/v3/redeploy";
1718

1819
const pageSize = 20;
1920

@@ -154,7 +155,10 @@ export class DeploymentListPresenter {
154155
userAvatarUrl: string | null;
155156
type: WorkerInstanceGroupType;
156157
externalId: string | null;
158+
triggeredVia: string | null;
157159
git: Prisma.JsonValue | null;
160+
buildServerMetadata: Prisma.JsonValue | null;
161+
createdAt: Date;
158162
integrationDeploymentId: string | null;
159163
}[]
160164
>`
@@ -175,7 +179,10 @@ export class DeploymentListPresenter {
175179
wd."deployedAt",
176180
wd."type",
177181
wd."externalId",
178-
wd."git"
182+
wd."triggeredVia",
183+
wd."git",
184+
wd."buildServerMetadata",
185+
wd."createdAt"
179186
${vercelSelect}
180187
FROM
181188
${sqlDatabaseSchema}."WorkerDeployment" as wd
@@ -267,7 +274,10 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
267274
}
268275
: undefined,
269276
externalId: deployment.externalId,
277+
triggeredVia: deployment.triggeredVia,
270278
git: processGitMetadata(deployment.git),
279+
createdAt: deployment.createdAt,
280+
redeploySource: resolveRedeploySource(deployment)?.kind ?? null,
271281
vercelDeploymentUrl,
272282
};
273283
}),
Collapse file

‎apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts‎

Copy file name to clipboardExpand all lines: apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { env } from "~/env.server";
1818
import { createRedisClient } from "~/redis.server";
1919
import { tryCatch } from "@trigger.dev/core";
2020
import { logger } from "~/services/logger.server";
21+
import { redeployOfShortCode } from "~/v3/redeploy";
2122

2223
const S2_TOKEN_KEY_PREFIX = "s2-token:project:";
2324

@@ -278,6 +279,7 @@ export class DeploymentPresenter {
278279
externalId: deployment.externalId,
279280
git: gitMetadata,
280281
triggeredVia: deployment.triggeredVia,
282+
redeployOf: redeployOfShortCode(deployment.buildServerMetadata),
281283
vercelDeploymentUrl,
282284
},
283285
};
Collapse file

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

Copy file name to clipboardExpand all lines: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx
+24-1Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ import { useProject } from "~/hooks/useProject";
4949
import { DeploymentPresenter } from "~/presenters/v3/DeploymentPresenter.server";
5050
import { requireUserId } from "~/services/session.server";
5151
import { cn } from "~/utils/cn";
52-
import { v3DeploymentParams, v3DeploymentsPath, v3RunsPath } from "~/utils/pathBuilder";
52+
import {
53+
v3DeploymentParams,
54+
v3DeploymentPath,
55+
v3DeploymentsPath,
56+
v3RunsPath,
57+
} from "~/utils/pathBuilder";
5358
import { capitalizeWord } from "~/utils/string";
5459
import { UserTag } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route";
5560
import { useDeploymentLogs } from "~/hooks/useDeploymentLogs";
@@ -296,6 +301,24 @@ export default function Page() {
296301
<Property.Label>Version</Property.Label>
297302
<Property.Value>{deployment.version}</Property.Value>
298303
</Property.Item>
304+
{deployment.redeployOf && (
305+
<Property.Item>
306+
<Property.Label>Redeploy of</Property.Label>
307+
<Property.Value>
308+
<TextLink
309+
to={v3DeploymentPath(
310+
organization,
311+
project,
312+
environment,
313+
{ shortCode: deployment.redeployOf },
314+
0
315+
)}
316+
>
317+
{deployment.redeployOf}
318+
</TextLink>
319+
</Property.Value>
320+
</Property.Item>
321+
)}
299322
<Property.Item>
300323
<Property.Label>Status</Property.Label>
301324
<Property.Value>
Collapse file

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

Copy file name to clipboardExpand all lines: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx
+133-5Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import { Form, Outlet, useLocation, useNavigate, useNavigation, useParams } from
1212

1313
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
1414
import { CogIcon, GitBranchIcon } from "lucide-react";
15-
import { useEffect } from "react";
15+
import { useEffect, useRef, useState } from "react";
1616
import { typedjson, useTypedLoaderData } from "remix-typedjson";
1717
import { z } from "zod";
1818
import { PromoteIcon } from "~/assets/icons/PromoteIcon";
19+
import { InlineCode } from "~/components/code/InlineCode";
1920
import { VercelLink } from "~/components/integrations/VercelLink";
2021
import { DeploymentsNone, DeploymentsNoneDev } from "~/components/BlankStatePanels";
2122
import { OctoKitty } from "~/components/GitHubLoginButton";
@@ -25,6 +26,7 @@ import { UserAvatar } from "~/components/UserProfilePhoto";
2526
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
2627
import { Badge } from "~/components/primitives/Badge";
2728
import { Button, LinkButton } from "~/components/primitives/Buttons";
29+
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
2830
import { DateTime } from "~/components/primitives/DateTime";
2931
import { SpinnerWhite } from "~/components/primitives/Spinner";
3032
import {
@@ -154,6 +156,9 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
154156
canWriteDeployments: { action: "write", resource: { type: "deployments" } },
155157
}).canWriteDeployments
156158
: true;
159+
const redeployCutoff = new Date(
160+
Date.now() - env.DEPLOYMENTS_REDEPLOY_WINDOW_DAYS * 24 * 60 * 60 * 1000
161+
);
157162

158163
const onboarding = await resolveDeploymentOnboardingUi({
159164
request,
@@ -173,6 +178,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
173178
...result,
174179
...onboarding,
175180
canWriteDeployments,
181+
redeployCutoff,
176182
selectedDeployment,
177183
autoReloadPollIntervalMs,
178184
});
@@ -199,6 +205,7 @@ export default function Page() {
199205
autoReloadPollIntervalMs,
200206
hasVercelIntegration,
201207
canWriteDeployments,
208+
redeployCutoff,
202209
canDeployNow,
203210
deployNowEnabled,
204211
isPlatformConfigured,
@@ -409,6 +416,7 @@ export default function Page() {
409416
isSelected={isSelected}
410417
currentDeployment={currentDeployment}
411418
canWriteDeployments={canWriteDeployments}
419+
redeployCutoff={redeployCutoff}
412420
/>
413421
</TableRow>
414422
);
@@ -531,12 +539,14 @@ function DeploymentActionsCell({
531539
isSelected,
532540
currentDeployment,
533541
canWriteDeployments,
542+
redeployCutoff,
534543
}: {
535544
deployment: DeploymentListItem;
536545
path: string;
537546
isSelected: boolean;
538547
currentDeployment?: DeploymentListItem;
539548
canWriteDeployments: boolean;
549+
redeployCutoff: Date;
540550
}) {
541551
const location = useLocation();
542552
const project = useProject();
@@ -550,8 +560,15 @@ function DeploymentActionsCell({
550560

551561
const finalStatuses = ["CANCELED", "DEPLOYED", "FAILED", "TIMED_OUT"];
552562
const canBeCanceled = !finalStatuses.includes(deployment.status);
553-
554-
if (!canBeRolledBack && !canBePromoted && !canBeCanceled) {
563+
const showRedeploy =
564+
deployment.redeploySource !== null &&
565+
finalStatuses.includes(deployment.status) &&
566+
deployment.createdAt >= redeployCutoff;
567+
const redeployDisabledReason = canWriteDeployments
568+
? undefined
569+
: "You don't have permission to redeploy";
570+
571+
if (!canBeRolledBack && !canBePromoted && !canBeCanceled && !showRedeploy) {
555572
return (
556573
<TableCell to={path} isSelected={isSelected}>
557574
{""}
@@ -563,7 +580,8 @@ function DeploymentActionsCell({
563580
<TableCellMenu
564581
isSticky
565582
isSelected={isSelected}
566-
popoverContent={
583+
// oxlint-disable-next-line react/no-unstable-nested-components -- Render prop: the menu primitive passes its close handler so the redeploy dialog can dismiss the menu.
584+
popoverContent={(closeMenu) => (
567585
<>
568586
{canBeRolledBack &&
569587
(canWriteDeployments ? (
@@ -664,12 +682,122 @@ function DeploymentActionsCell({
664682
Cancel
665683
</Button>
666684
))}
685+
{showRedeploy &&
686+
(redeployDisabledReason === undefined ? (
687+
<RedeployDeploymentDialog
688+
projectId={project.id}
689+
deployment={deployment}
690+
redirectPath={`${location.pathname}${location.search}`}
691+
onSettled={closeMenu}
692+
/>
693+
) : (
694+
<Button
695+
variant="small-menu-item"
696+
LeadingIcon={ArrowPathIcon}
697+
leadingIconClassName="text-blue-500"
698+
fullWidth
699+
textAlignLeft
700+
disabled
701+
tooltip={redeployDisabledReason}
702+
>
703+
Redeploy
704+
</Button>
705+
))}
667706
</>
668-
}
707+
)}
669708
/>
670709
);
671710
}
672711

712+
function RedeployDeploymentDialog({
713+
projectId,
714+
deployment,
715+
redirectPath,
716+
onSettled,
717+
}: {
718+
projectId: string;
719+
deployment: DeploymentListItem;
720+
redirectPath: string;
721+
onSettled: () => void;
722+
}) {
723+
const navigation = useNavigation();
724+
const [open, setOpen] = useState(false);
725+
726+
const formAction = `/resources/${projectId}/deployments/${deployment.shortCode}/redeploy`;
727+
const formId = `redeploy-${deployment.shortCode}`;
728+
const isLoading = navigation.formAction === formAction;
729+
730+
// The source row stays eligible after a redeploy, so the dialog never unmounts; close it
731+
// once the submission has settled (the toast carries the outcome).
732+
const wasLoading = useRef(false);
733+
useEffect(() => {
734+
if (wasLoading.current && !isLoading) {
735+
setOpen(false);
736+
onSettled();
737+
}
738+
wasLoading.current = isLoading;
739+
}, [isLoading, onSettled]);
740+
741+
return (
742+
<Dialog open={open} onOpenChange={setOpen}>
743+
<DialogTrigger asChild>
744+
<Button
745+
variant="small-menu-item"
746+
LeadingIcon={ArrowPathIcon}
747+
leadingIconClassName="text-blue-500"
748+
fullWidth
749+
textAlignLeft
750+
>
751+
Redeploy
752+
</Button>
753+
</DialogTrigger>
754+
<DialogContent key="redeploy">
755+
<DialogHeader>Redeploy</DialogHeader>
756+
<DialogDescription>
757+
{deployment.redeploySource === "github" && deployment.git ? (
758+
<>
759+
A new deployment will rebuild commit{" "}
760+
<InlineCode variant="extra-small">{deployment.git.shortSha}</InlineCode> from branch{" "}
761+
<InlineCode variant="extra-small">{deployment.git.branchName}</InlineCode>.
762+
</>
763+
) : (
764+
<>
765+
A new deployment will be created from the source files of deployment{" "}
766+
<InlineCode variant="extra-small">{deployment.shortCode}</InlineCode>.
767+
</>
768+
)}
769+
</DialogDescription>
770+
<CheckboxWithLabel
771+
name="promote"
772+
value="on"
773+
form={formId}
774+
variant="simple/small"
775+
defaultChecked
776+
label="Promote deployment automatically"
777+
/>
778+
<DialogFooter>
779+
<DialogClose asChild>
780+
<Button variant="tertiary/medium">Cancel</Button>
781+
</DialogClose>
782+
<Form id={formId} action={formAction} method="post">
783+
<Button
784+
type="submit"
785+
name="redirectUrl"
786+
value={redirectPath}
787+
variant="primary/medium"
788+
LeadingIcon={isLoading ? SpinnerWhite : ArrowPathIcon}
789+
disabled={isLoading}
790+
shortcut={{ modifiers: ["mod"], key: "enter" }}
791+
>
792+
{isLoading ? "Redeploying..." : "Redeploy"}
793+
</Button>
794+
</Form>
795+
</DialogFooter>
796+
</DialogContent>
797+
</Dialog>
798+
);
799+
}
800+
673801
function DeploymentExternalIdCell({
674802
externalId,
675803
path,

0 commit comments

Comments
0 (0)
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.