Skip to content

Commit 0af9fb0

Browse files
fix(workflows): enforce undeploy rollback failures
1 parent 3a6f90d commit 0af9fb0

File tree

2 files changed

+91
-4
lines changed

2 files changed

+91
-4
lines changed

apps/sim/app/api/workflows/[id]/deploy/route.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,71 @@ describe('Workflow deploy route', () => {
417417
})
418418
})
419419

420+
it('fails first deploy rollback when undeploy reports failure before deletion', async () => {
421+
mockValidateWorkflowAccess.mockResolvedValue({
422+
workflow: { id: 'wf-1', name: 'Test Workflow', workspaceId: 'ws-1' },
423+
auth: {
424+
success: true,
425+
userId: 'api-user',
426+
userName: 'API Key Actor',
427+
userEmail: 'api@example.com',
428+
authType: 'api_key',
429+
},
430+
})
431+
mockDeployWorkflow.mockResolvedValue({
432+
success: true,
433+
deployedAt: '2024-02-01T00:00:00Z',
434+
deploymentVersionId: 'dep-failed',
435+
})
436+
mockSaveTriggerWebhooksForDeploy.mockResolvedValue({
437+
success: false,
438+
error: { message: 'Failed to save trigger configuration', status: 500 },
439+
})
440+
mockDbLimit.mockResolvedValue([])
441+
mockUndeployWorkflow.mockResolvedValue({ success: false, error: 'undeploy failed' })
442+
443+
const req = new NextRequest('http://localhost:3000/api/workflows/wf-1/deploy', {
444+
method: 'POST',
445+
headers: { 'x-api-key': 'test-key' },
446+
})
447+
const response = await POST(req, { params: Promise.resolve({ id: 'wf-1' }) })
448+
449+
expect(response.status).toBe(500)
450+
expect(await response.json()).toEqual({ error: 'undeploy failed', code: 'UNDEPLOY_FAILED' })
451+
expect(mockUndeployWorkflow).toHaveBeenCalledWith({ workflowId: 'wf-1' })
452+
expect(mockDeleteDeploymentVersionById).not.toHaveBeenCalled()
453+
})
454+
455+
it('fails when deployment version id is missing and undeploy reports failure', async () => {
456+
mockValidateWorkflowAccess.mockResolvedValue({
457+
workflow: { id: 'wf-1', name: 'Test Workflow', workspaceId: 'ws-1' },
458+
auth: {
459+
success: true,
460+
userId: 'api-user',
461+
userName: 'API Key Actor',
462+
userEmail: 'api@example.com',
463+
authType: 'api_key',
464+
},
465+
})
466+
mockDeployWorkflow.mockResolvedValue({
467+
success: true,
468+
deployedAt: '2024-02-01T00:00:00Z',
469+
deploymentVersionId: undefined,
470+
})
471+
mockUndeployWorkflow.mockResolvedValue({ success: false, error: 'undeploy failed' })
472+
473+
const req = new NextRequest('http://localhost:3000/api/workflows/wf-1/deploy', {
474+
method: 'POST',
475+
headers: { 'x-api-key': 'test-key' },
476+
})
477+
const response = await POST(req, { params: Promise.resolve({ id: 'wf-1' }) })
478+
479+
expect(response.status).toBe(500)
480+
expect(await response.json()).toEqual({ error: 'undeploy failed', code: 'UNDEPLOY_FAILED' })
481+
expect(mockUndeployWorkflow).toHaveBeenCalledWith({ workflowId: 'wf-1' })
482+
expect(mockDeleteDeploymentVersionById).not.toHaveBeenCalled()
483+
})
484+
420485
it('allows API-key auth for undeploy using hybrid auth userId', async () => {
421486
mockValidateWorkflowAccess.mockResolvedValue({
422487
workflow: { id: 'wf-1', name: 'Test Workflow', workspaceId: 'ws-1' },

apps/sim/app/api/workflows/[id]/deploy/route.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,15 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
170170
}
171171
}
172172

173-
await undeployWorkflow({ workflowId: id })
173+
const undeployResult = await undeployWorkflow({ workflowId: id })
174+
if (!undeployResult.success) {
175+
return createErrorResponse(
176+
undeployResult.error || 'Failed to undeploy workflow',
177+
500,
178+
'UNDEPLOY_FAILED'
179+
)
180+
}
181+
174182
await ensureFailedDeploymentVersionDeleted()
175183
}
176184

@@ -188,7 +196,15 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
188196
const deploymentVersionId = deployResult.deploymentVersionId
189197

190198
if (!deploymentVersionId) {
191-
await undeployWorkflow({ workflowId: id })
199+
const undeployResult = await undeployWorkflow({ workflowId: id })
200+
if (!undeployResult.success) {
201+
return createErrorResponse(
202+
undeployResult.error || 'Failed to undeploy workflow',
203+
500,
204+
'UNDEPLOY_FAILED'
205+
)
206+
}
207+
192208
return createErrorResponse('Failed to resolve deployment version', 500)
193209
}
194210

@@ -210,7 +226,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
210226
requestId,
211227
deploymentVersionId,
212228
})
213-
await rollbackDeployment(deploymentVersionId)
229+
const rollbackResponse = await rollbackDeployment(deploymentVersionId)
230+
if (rollbackResponse) {
231+
return rollbackResponse
232+
}
214233
return createErrorResponse(
215234
triggerSaveResult.error?.message || 'Failed to save trigger configuration',
216235
triggerSaveResult.error?.status || 500
@@ -234,7 +253,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
234253
requestId,
235254
deploymentVersionId,
236255
})
237-
await rollbackDeployment(deploymentVersionId)
256+
const rollbackResponse = await rollbackDeployment(deploymentVersionId)
257+
if (rollbackResponse) {
258+
return rollbackResponse
259+
}
238260
return createErrorResponse(scheduleResult.error || 'Failed to create schedule', 500)
239261
}
240262
if (scheduleResult.scheduleId) {

0 commit comments

Comments
 (0)