Skip to content

Commit 6f6e99b

Browse files
committed
fix(deploy): consolidate deployment detection into single source of truth
1 parent 4c12914 commit 6f6e99b

File tree

14 files changed

+243
-465
lines changed

14 files changed

+243
-465
lines changed

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

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
1+
import { db, workflowDeploymentVersion } from '@sim/db'
22
import { createLogger } from '@sim/logger'
3-
import { and, desc, eq } from 'drizzle-orm'
3+
import { and, eq } from 'drizzle-orm'
44
import type { NextRequest } from 'next/server'
55
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
66
import { generateRequestId } from '@/lib/core/utils/request'
@@ -21,8 +21,11 @@ import {
2121
validateWorkflowSchedules,
2222
} from '@/lib/workflows/schedules'
2323
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
24-
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
25-
import type { WorkflowState } from '@/stores/workflows/workflow/types'
24+
import {
25+
checkNeedsRedeployment,
26+
createErrorResponse,
27+
createSuccessResponse,
28+
} from '@/app/api/workflows/utils'
2629

2730
const logger = createLogger('WorkflowDeployAPI')
2831

@@ -54,43 +57,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
5457
})
5558
}
5659

57-
let needsRedeployment = false
58-
const [active] = await db
59-
.select({ state: workflowDeploymentVersion.state })
60-
.from(workflowDeploymentVersion)
61-
.where(
62-
and(
63-
eq(workflowDeploymentVersion.workflowId, id),
64-
eq(workflowDeploymentVersion.isActive, true)
65-
)
66-
)
67-
.orderBy(desc(workflowDeploymentVersion.createdAt))
68-
.limit(1)
69-
70-
if (active?.state) {
71-
const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/persistence/utils')
72-
const normalizedData = await loadWorkflowFromNormalizedTables(id)
73-
if (normalizedData) {
74-
const [workflowRecord] = await db
75-
.select({ variables: workflow.variables })
76-
.from(workflow)
77-
.where(eq(workflow.id, id))
78-
.limit(1)
79-
80-
const currentState = {
81-
blocks: normalizedData.blocks,
82-
edges: normalizedData.edges,
83-
loops: normalizedData.loops,
84-
parallels: normalizedData.parallels,
85-
variables: workflowRecord?.variables || {},
86-
}
87-
const { hasWorkflowChanged } = await import('@/lib/workflows/comparison')
88-
needsRedeployment = hasWorkflowChanged(
89-
currentState as WorkflowState,
90-
active.state as WorkflowState
91-
)
92-
}
93-
}
60+
const needsRedeployment = await checkNeedsRedeployment(id)
9461

9562
logger.info(`[${requestId}] Successfully retrieved deployment info: ${id}`)
9663

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

Lines changed: 8 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
21
import { createLogger } from '@sim/logger'
3-
import { and, desc, eq } from 'drizzle-orm'
42
import type { NextRequest } from 'next/server'
53
import { generateRequestId } from '@/lib/core/utils/request'
6-
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
7-
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
84
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
9-
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
10-
import type { WorkflowState } from '@/stores/workflows/workflow/types'
5+
import {
6+
checkNeedsRedeployment,
7+
createErrorResponse,
8+
createSuccessResponse,
9+
} from '@/app/api/workflows/utils'
1110

1211
const logger = createLogger('WorkflowStatusAPI')
1312

@@ -23,54 +22,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
2322
return createErrorResponse(validation.error.message, validation.error.status)
2423
}
2524

26-
let needsRedeployment = false
27-
28-
if (validation.workflow.isDeployed) {
29-
const normalizedData = await loadWorkflowFromNormalizedTables(id)
30-
31-
if (!normalizedData) {
32-
return createSuccessResponse({
33-
isDeployed: validation.workflow.isDeployed,
34-
deployedAt: validation.workflow.deployedAt,
35-
isPublished: validation.workflow.isPublished,
36-
needsRedeployment: false,
37-
})
38-
}
39-
40-
const [workflowRecord] = await db
41-
.select({ variables: workflow.variables })
42-
.from(workflow)
43-
.where(eq(workflow.id, id))
44-
.limit(1)
45-
46-
const currentState = {
47-
blocks: normalizedData.blocks,
48-
edges: normalizedData.edges,
49-
loops: normalizedData.loops,
50-
parallels: normalizedData.parallels,
51-
variables: workflowRecord?.variables || {},
52-
lastSaved: Date.now(),
53-
}
54-
55-
const [active] = await db
56-
.select({ state: workflowDeploymentVersion.state })
57-
.from(workflowDeploymentVersion)
58-
.where(
59-
and(
60-
eq(workflowDeploymentVersion.workflowId, id),
61-
eq(workflowDeploymentVersion.isActive, true)
62-
)
63-
)
64-
.orderBy(desc(workflowDeploymentVersion.createdAt))
65-
.limit(1)
66-
67-
if (active?.state) {
68-
needsRedeployment = hasWorkflowChanged(
69-
currentState as WorkflowState,
70-
active.state as WorkflowState
71-
)
72-
}
73-
}
25+
const needsRedeployment = validation.workflow.isDeployed
26+
? await checkNeedsRedeployment(id)
27+
: false
7428

7529
return createSuccessResponse({
7630
isDeployed: validation.workflow.isDeployed,

apps/sim/app/api/workflows/utils.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
12
import { createLogger } from '@sim/logger'
3+
import { and, desc, eq } from 'drizzle-orm'
24
import { NextResponse } from 'next/server'
5+
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
6+
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
37
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
8+
import type { WorkflowState } from '@/stores/workflows/workflow/types'
49

510
const logger = createLogger('WorkflowUtils')
611

@@ -18,6 +23,46 @@ export function createSuccessResponse(data: any) {
1823
return NextResponse.json(data)
1924
}
2025

26+
/**
27+
* Checks whether a deployed workflow has changes that require redeployment.
28+
* Compares the current persisted state (from normalized tables) against the
29+
* active deployment version state.
30+
*
31+
* This is the single source of truth for redeployment detection — used by
32+
* both the /deploy and /status endpoints to ensure consistent results.
33+
*/
34+
export async function checkNeedsRedeployment(workflowId: string): Promise<boolean> {
35+
const [active] = await db
36+
.select({ state: workflowDeploymentVersion.state })
37+
.from(workflowDeploymentVersion)
38+
.where(
39+
and(
40+
eq(workflowDeploymentVersion.workflowId, workflowId),
41+
eq(workflowDeploymentVersion.isActive, true)
42+
)
43+
)
44+
.orderBy(desc(workflowDeploymentVersion.createdAt))
45+
.limit(1)
46+
47+
if (!active?.state) return false
48+
49+
const [normalizedData, [workflowRecord]] = await Promise.all([
50+
loadWorkflowFromNormalizedTables(workflowId),
51+
db.select({ variables: workflow.variables }).from(workflow).where(eq(workflow.id, workflowId)).limit(1),
52+
])
53+
if (!normalizedData) return false
54+
55+
const currentState = {
56+
blocks: normalizedData.blocks,
57+
edges: normalizedData.edges,
58+
loops: normalizedData.loops,
59+
parallels: normalizedData.parallels,
60+
variables: workflowRecord?.variables || {},
61+
}
62+
63+
return hasWorkflowChanged(currentState as WorkflowState, active.state as WorkflowState)
64+
}
65+
2166
/**
2267
* Verifies user's workspace permissions using the permissions table
2368
* @param userId User ID to check

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { startsWithUuid } from '@/executor/constants'
2525
import { useA2AAgentByWorkflow } from '@/hooks/queries/a2a/agents'
2626
import { useApiKeys } from '@/hooks/queries/api-keys'
2727
import {
28-
deploymentKeys,
28+
invalidateDeploymentQueries,
2929
useActivateDeploymentVersion,
3030
useChatDeploymentInfo,
3131
useDeploymentInfo,
@@ -60,7 +60,6 @@ interface DeployModalProps {
6060
needsRedeployment: boolean
6161
deployedState: WorkflowState
6262
isLoadingDeployedState: boolean
63-
refetchDeployedState: () => Promise<void>
6463
}
6564

6665
interface WorkflowDeploymentInfoUI {
@@ -83,7 +82,6 @@ export function DeployModal({
8382
needsRedeployment,
8483
deployedState,
8584
isLoadingDeployedState,
86-
refetchDeployedState,
8785
}: DeployModalProps) {
8886
const queryClient = useQueryClient()
8987
const openSettingsModal = useSettingsModalStore((state) => state.openModal)
@@ -297,17 +295,17 @@ export function DeployModal({
297295
setDeployWarnings([])
298296

299297
try {
298+
// Deploy mutation handles query invalidation in its onSuccess callback
300299
const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false })
301300
if (result.warnings && result.warnings.length > 0) {
302301
setDeployWarnings(result.warnings)
303302
}
304-
await refetchDeployedState()
305303
} catch (error: unknown) {
306304
logger.error('Error deploying workflow:', { error })
307305
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workflow'
308306
setDeployError(errorMessage)
309307
}
310-
}, [workflowId, deployMutation, refetchDeployedState])
308+
}, [workflowId, deployMutation])
311309

312310
const handlePromoteToLive = useCallback(
313311
async (version: number) => {
@@ -320,13 +318,12 @@ export function DeployModal({
320318
if (result.warnings && result.warnings.length > 0) {
321319
setDeployWarnings(result.warnings)
322320
}
323-
await refetchDeployedState()
324321
} catch (error) {
325322
logger.error('Error promoting version:', { error })
326323
throw error
327324
}
328325
},
329-
[workflowId, activateVersionMutation, refetchDeployedState]
326+
[workflowId, activateVersionMutation]
330327
)
331328

332329
const handleUndeploy = useCallback(async () => {
@@ -366,13 +363,12 @@ export function DeployModal({
366363
if (result.warnings && result.warnings.length > 0) {
367364
setDeployWarnings(result.warnings)
368365
}
369-
await refetchDeployedState()
370366
} catch (error: unknown) {
371367
logger.error('Error redeploying workflow:', { error })
372368
const errorMessage = error instanceof Error ? error.message : 'Failed to redeploy workflow'
373369
setDeployError(errorMessage)
374370
}
375-
}, [workflowId, deployMutation, refetchDeployedState])
371+
}, [workflowId, deployMutation])
376372

377373
const handleCloseModal = useCallback(() => {
378374
setChatSubmitting(false)
@@ -384,17 +380,16 @@ export function DeployModal({
384380
const handleChatDeployed = useCallback(async () => {
385381
if (!workflowId) return
386382

387-
queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) })
383+
invalidateDeploymentQueries(queryClient, workflowId)
388384

389-
await refetchDeployedState()
390385
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
391386

392387
if (chatSuccessTimeoutRef.current) {
393388
clearTimeout(chatSuccessTimeoutRef.current)
394389
}
395390
setChatSuccess(true)
396391
chatSuccessTimeoutRef.current = setTimeout(() => setChatSuccess(false), 2000)
397-
}, [workflowId, queryClient, refetchDeployedState])
392+
}, [workflowId, queryClient])
398393

399394
const handleRefetchChat = useCallback(async () => {
400395
await refetchChatInfo()

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import { Button, Tooltip } from '@/components/emcn'
66
import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
77
import {
88
useChangeDetection,
9-
useDeployedState,
109
useDeployment,
1110
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks'
1211
import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow'
12+
import { useDeployedWorkflowState, useDeploymentInfo } from '@/hooks/queries/deployments'
1313
import type { WorkspaceUserPermissions } from '@/hooks/use-user-permissions'
1414
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
1515

@@ -38,24 +38,32 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
3838
)
3939
const isDeployed = deploymentStatus?.isDeployed || false
4040

41-
// Fetch and manage deployed state
42-
const { deployedState, isLoadingDeployedState, refetchDeployedState } = useDeployedState({
43-
workflowId: activeWorkflowId,
44-
isDeployed,
45-
isRegistryLoading,
46-
})
41+
// Server-side deployment info (authoritative source for needsRedeployment)
42+
const { data: deploymentInfoData, isLoading: isLoadingDeploymentInfo } = useDeploymentInfo(
43+
activeWorkflowId,
44+
{ enabled: isDeployed && !isRegistryLoading }
45+
)
46+
47+
// Fetch deployed state snapshot for change detection and modal
48+
const isDeployedStateEnabled = Boolean(activeWorkflowId) && isDeployed && !isRegistryLoading
49+
const { data: deployedStateData, isLoading: isLoadingDeployedState } = useDeployedWorkflowState(
50+
activeWorkflowId,
51+
{ enabled: isDeployedStateEnabled }
52+
)
53+
const deployedState = isDeployedStateEnabled ? (deployedStateData ?? null) : null
4754

4855
const { changeDetected } = useChangeDetection({
4956
workflowId: activeWorkflowId,
5057
deployedState,
5158
isLoadingDeployedState,
59+
serverNeedsRedeployment: deploymentInfoData?.needsRedeployment,
60+
isServerLoading: isLoadingDeploymentInfo,
5261
})
5362

5463
// Handle deployment operations
5564
const { isDeploying, handleDeployClick } = useDeployment({
5665
workflowId: activeWorkflowId,
5766
isDeployed,
58-
refetchDeployedState,
5967
})
6068

6169
const isEmpty = !hasBlocks()
@@ -122,7 +130,6 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
122130
needsRedeployment={changeDetected}
123131
deployedState={deployedState!}
124132
isLoadingDeployedState={isLoadingDeployedState}
125-
refetchDeployedState={refetchDeployedState}
126133
/>
127134
</>
128135
)
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
export { useChangeDetection } from './use-change-detection'
2-
export { useDeployedState } from './use-deployed-state'
32
export { useDeployment } from './use-deployment'

0 commit comments

Comments
 (0)