diff --git a/.changeset/cold-coins-burn.md b/.changeset/cold-coins-burn.md
new file mode 100644
index 00000000000..6de3d72ec39
--- /dev/null
+++ b/.changeset/cold-coins-burn.md
@@ -0,0 +1,7 @@
+---
+"@trigger.dev/react-hooks": patch
+"@trigger.dev/sdk": patch
+"trigger.dev": patch
+---
+
+Add support for two-phase deployments and task version pinning
diff --git a/apps/webapp/app/components/primitives/Tabs.tsx b/apps/webapp/app/components/primitives/Tabs.tsx
index d3e54514841..2a61062caeb 100644
--- a/apps/webapp/app/components/primitives/Tabs.tsx
+++ b/apps/webapp/app/components/primitives/Tabs.tsx
@@ -1,10 +1,8 @@
-import { Link, NavLink, useLocation } from "@remix-run/react";
+import { NavLink } from "@remix-run/react";
import { motion } from "framer-motion";
import { ReactNode, useRef } from "react";
-import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
-import { projectPubSub } from "~/v3/services/projectPubSub.server";
import { ShortcutKey } from "./ShortcutKey";
export type TabsProps = {
diff --git a/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx b/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx
index ccba03b0303..76ae56e1556 100644
--- a/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx
+++ b/apps/webapp/app/components/runs/v3/RollbackDeploymentDialog.tsx
@@ -27,7 +27,7 @@ export function RollbackDeploymentDialog({
return (
- Roll back to this deployment?
+ Rollback to this deployment?
This deployment will become the default for all future runs. Tasks triggered but not
included in this deploy will remain queued until you roll back to or create a new deployment
@@ -50,7 +50,49 @@ export function RollbackDeploymentDialog({
disabled={isLoading}
shortcut={{ modifiers: ["mod"], key: "enter" }}
>
- {isLoading ? "Rolling back..." : "Roll back deployment"}
+ {isLoading ? "Rolling back..." : "Rollback deployment"}
+
+
+
+
+ );
+}
+
+export function PromoteDeploymentDialog({
+ projectId,
+ deploymentShortCode,
+ redirectPath,
+}: RollbackDeploymentDialogProps) {
+ const navigation = useNavigation();
+
+ const formAction = `/resources/${projectId}/deployments/${deploymentShortCode}/promote`;
+ const isLoading = navigation.formAction === formAction;
+
+ return (
+
+ Promote this deployment?
+
+ This deployment will become the default for all future runs not explicitly tied to a
+ specific deployment.
+
+
+
+
+
+
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments/route.tsx
index 227a3c6deb4..c75cc2aba2a 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.v3.$projectParam.deployments/route.tsx
@@ -1,6 +1,7 @@
import {
ArrowPathIcon,
ArrowUturnLeftIcon,
+ ArrowUturnRightIcon,
BookOpenIcon,
ServerStackIcon,
} from "@heroicons/react/20/solid";
@@ -41,7 +42,10 @@ import {
deploymentStatuses,
} from "~/components/runs/v3/DeploymentStatus";
import { RetryDeploymentIndexingDialog } from "~/components/runs/v3/RetryDeploymentIndexingDialog";
-import { RollbackDeploymentDialog } from "~/components/runs/v3/RollbackDeploymentDialog";
+import {
+ PromoteDeploymentDialog,
+ RollbackDeploymentDialog,
+} from "~/components/runs/v3/RollbackDeploymentDialog";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useUser } from "~/hooks/useUser";
@@ -58,6 +62,7 @@ import {
} from "~/utils/pathBuilder";
import { createSearchParams } from "~/utils/searchParams";
import { deploymentIndexingIsRetryable } from "~/v3/deploymentStatus";
+import { compareDeploymentVersions } from "~/v3/utils/deploymentVersions";
export const meta: MetaFunction = () => {
return [
@@ -106,6 +111,8 @@ export default function Page() {
const { deploymentParam } = useParams();
+ const currentDeployment = deployments.find((d) => d.isCurrent);
+
return (
@@ -234,6 +241,7 @@ export default function Page() {
deployment={deployment}
path={path}
isSelected={isSelected}
+ currentDeployment={currentDeployment}
/>
);
@@ -320,18 +328,25 @@ function DeploymentActionsCell({
deployment,
path,
isSelected,
+ currentDeployment,
}: {
deployment: DeploymentListItem;
path: string;
isSelected: boolean;
+ currentDeployment?: DeploymentListItem;
}) {
const location = useLocation();
const project = useProject();
- const canRollback = !deployment.isCurrent && deployment.isDeployed;
+ const canBeMadeCurrent = !deployment.isCurrent && deployment.isDeployed;
const canRetryIndexing = deployment.isLatest && deploymentIndexingIsRetryable(deployment);
+ const canBeRolledBack =
+ canBeMadeCurrent &&
+ currentDeployment?.version &&
+ compareDeploymentVersions(deployment.version, currentDeployment.version) === -1;
+ const canBePromoted = canBeMadeCurrent && !canBeRolledBack;
- if (!canRollback && !canRetryIndexing) {
+ if (!canBeMadeCurrent && !canRetryIndexing) {
return (
{""}
@@ -345,7 +360,7 @@ function DeploymentActionsCell({
isSelected={isSelected}
popoverContent={
<>
- {canRollback && (
+ {canBeRolledBack && (
)}
+ {canBePromoted && (
+
+ )}
{canRetryIndexing && (