diff --git a/.changeset/cold-coins-burn.md b/.changeset/cold-coins-burn.md
new file mode 100644
index 000000000..6de3d72ec
--- /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 d3e545148..2a61062ca 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 ccba03b03..76ae56e15 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 227a3c6de..c75cc2aba 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 && (