diff --git a/.changeset/cold-coins-burn.md b/.changeset/cold-coins-burn.md
new file mode 100644
index 0000000000..6de3d72ec3
--- /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 d3e5451484..2a61062cae 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 ccba03b030..76ae56e155 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 227a3c6deb..c75cc2aba2 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 && (