Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/allow-rollbacks-promote-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: feature
---

Add allowRollbacks query param to the promote deployment API to enable version downgrades
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ export async function action({ request, params }: ActionFunctionArgs) {

const authenticatedEnv = authenticationResult.environment;

const url = new URL(request.url);
const allowRollbacks = url.searchParams.get("allowRollbacks") === "true";

const { deploymentVersion } = parsedParams.data;

const deployment = await prisma.workerDeployment.findFirst({
Expand All @@ -47,7 +50,7 @@ export async function action({ request, params }: ActionFunctionArgs) {

try {
const service = new ChangeCurrentDeploymentService();
await service.call(deployment, "promote");
await service.call(deployment, "promote", allowRollbacks);

return json(
{
Expand Down
50 changes: 31 additions & 19 deletions apps/webapp/app/v3/services/changeCurrentDeployment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic";
export type ChangeCurrentDeploymentDirection = "promote" | "rollback";

export class ChangeCurrentDeploymentService extends BaseService {
public async call(deployment: WorkerDeployment, direction: ChangeCurrentDeploymentDirection) {
public async call(
deployment: WorkerDeployment,
direction: ChangeCurrentDeploymentDirection,
disableVersionCheck?: boolean
) {
Comment on lines +10 to +14
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Limit version-check bypass to promotions only.

Line 49 currently disables both "promote" and "rollback" ordering checks. That makes rollback safety checks bypassable by any future caller that passes true, which is broader than the rollout goal (downgrades via promote API).

Suggested fix
-      if (!disableVersionCheck) {
-        switch (direction) {
-          case "promote": {
-            if (
-              compareDeploymentVersions(
-                currentPromotion.deployment.version,
-                deployment.version
-              ) >= 0
-            ) {
-              throw new ServiceValidationError(
-                "Cannot promote a deployment that is older than the current deployment."
-              );
-            }
-            break;
-          }
-          case "rollback": {
-            if (
-              compareDeploymentVersions(
-                currentPromotion.deployment.version,
-                deployment.version
-              ) <= 0
-            ) {
-              throw new ServiceValidationError(
-                "Cannot rollback to a deployment that is newer than the current deployment."
-              );
-            }
-            break;
-          }
-        }
-      }
+      switch (direction) {
+        case "promote": {
+          if (
+            !disableVersionCheck &&
+            compareDeploymentVersions(
+              currentPromotion.deployment.version,
+              deployment.version
+            ) >= 0
+          ) {
+            throw new ServiceValidationError(
+              "Cannot promote a deployment that is older than the current deployment."
+            );
+          }
+          break;
+        }
+        case "rollback": {
+          if (
+            compareDeploymentVersions(
+              currentPromotion.deployment.version,
+              deployment.version
+            ) <= 0
+          ) {
+            throw new ServiceValidationError(
+              "Cannot rollback to a deployment that is newer than the current deployment."
+            );
+          }
+          break;
+        }
+      }

Also applies to: 49-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/services/changeCurrentDeployment.server.ts` around lines
10 - 14, The current call method accepts disableVersionCheck and uses it to skip
ordering/version safety for both "promote" and "rollback"; change the logic so
disableVersionCheck is only honored when direction === "promote" (i.e., only
allow bypassing the version-order check for promotions), and ensure rollback
paths always perform the ordering/version checks regardless of the
disableVersionCheck flag; update any conditional(s) in the call method that
reference disableVersionCheck and the direction to explicitly require direction
=== "promote" before skipping checks (use the deployment, direction, and
disableVersionCheck parameters to locate the code).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is okay, given how isolated the change is. But happy to make this change too if reviewer feel strongly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description image shows a deployment table with two entries — h7mvlm0n (version 20260314.2, Deployed) and pzaalcga (version 20260314.1, marked Current) — confirming the downgrade-via-promote scenario the PR is testing.


@chengzp That's a fair point — since disableVersionCheck is only ever passed as true from the promote API endpoint today, there's no immediate safety gap. My suggestion is purely defensive: scoping the bypass explicitly to direction === "promote" prevents a future caller from inadvertently skipping the rollback ordering check by reusing this flag. The rollback direction already semantically implies "going backward," so unconditionally enforcing its version check feels like a reasonable invariant to lock in now.

That said, I agree it's not a blocker given the current isolation. Happy to leave it as-is if the reviewer is comfortable — just flagging it as a low-cost guard for future maintainability.


🧠 Learnings used
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Applies to packages/cli-v3/src/commands/promote.ts : Implement `promote.ts` command in `src/commands/` for deployment promotion

Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: packages/cli-v3/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:43:34.140Z
Learning: Applies to packages/cli-v3/src/commands/deploy.ts : Implement `deploy.ts` command in `src/commands/` for production deployment

Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: apps/webapp/CLAUDE.md:0-0
Timestamp: 2026-03-02T12:42:56.114Z
Learning: Applies to apps/webapp/app/v3/services/**/*.server.ts : When editing services that branch on `RunEngineVersion` to support both V1 and V2 (e.g., `cancelTaskRun.server.ts`, `batchTriggerV3.server.ts`), only modify V2 code paths

Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 3201
File: apps/webapp/app/v3/services/setSeatsAddOn.server.ts:25-29
Timestamp: 2026-03-10T17:56:20.938Z
Learning: Do not implement local userId-to-organizationId authorization checks inside org-scoped service classes (e.g., SetSeatsAddOnService, SetBranchesAddOnService) in the web app. Rely on route-layer authentication (requireUserId(request)) and org membership enforcement via the _app.orgs.$organizationSlug layout route. Any userId/organizationId that reaches these services from org-scoped routes has already been validated. Apply this pattern across all org-scoped services to avoid redundant auth checks and maintain consistency.

if (!deployment.workerId) {
throw new ServiceValidationError(
direction === "promote"
Expand Down Expand Up @@ -42,26 +46,34 @@ export class ChangeCurrentDeploymentService extends BaseService {
}

// if there is a current promotion, we have to validate we are moving in the right direction based on the deployment versions
switch (direction) {
case "promote": {
if (
compareDeploymentVersions(currentPromotion.deployment.version, deployment.version) >= 0
) {
throw new ServiceValidationError(
"Cannot promote a deployment that is older than the current deployment."
);
if (!disableVersionCheck) {
switch (direction) {
case "promote": {
if (
compareDeploymentVersions(
currentPromotion.deployment.version,
deployment.version
) >= 0
) {
throw new ServiceValidationError(
"Cannot promote a deployment that is older than the current deployment."
);
}
break;
}
break;
}
case "rollback": {
if (
compareDeploymentVersions(currentPromotion.deployment.version, deployment.version) <= 0
) {
throw new ServiceValidationError(
"Cannot rollback to a deployment that is newer than the current deployment."
);
case "rollback": {
if (
compareDeploymentVersions(
currentPromotion.deployment.version,
deployment.version
) <= 0
) {
throw new ServiceValidationError(
"Cannot rollback to a deployment that is newer than the current deployment."
);
}
break;
}
break;
}
}
}
Expand Down