Skip to content

Commit 1fcff2b

Browse files
committed
fix(deploy): consolidate deployment detection into single source of truth
1 parent 6818c51 commit 1fcff2b

File tree

14 files changed

+244
-471
lines changed

14 files changed

+244
-471
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'
@@ -22,8 +22,11 @@ import {
2222
validateWorkflowSchedules,
2323
} from '@/lib/workflows/schedules'
2424
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
25-
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
26-
import type { WorkflowState } from '@/stores/workflows/workflow/types'
25+
import {
26+
checkNeedsRedeployment,
27+
createErrorResponse,
28+
createSuccessResponse,
29+
} from '@/app/api/workflows/utils'
2730

2831
const logger = createLogger('WorkflowDeployAPI')
2932

@@ -55,43 +58,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
5558
})
5659
}
5760

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

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

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,
@@ -61,7 +61,6 @@ interface DeployModalProps {
6161
needsRedeployment: boolean
6262
deployedState: WorkflowState
6363
isLoadingDeployedState: boolean
64-
refetchDeployedState: () => Promise<void>
6564
}
6665

6766
interface WorkflowDeploymentInfoUI {
@@ -84,7 +83,6 @@ export function DeployModal({
8483
needsRedeployment,
8584
deployedState,
8685
isLoadingDeployedState,
87-
refetchDeployedState,
8886
}: DeployModalProps) {
8987
const queryClient = useQueryClient()
9088
const { navigateToSettings } = useSettingsNavigation()
@@ -298,17 +296,17 @@ export function DeployModal({
298296
setDeployWarnings([])
299297

300298
try {
299+
// Deploy mutation handles query invalidation in its onSuccess callback
301300
const result = await deployMutation.mutateAsync({ workflowId, deployChatEnabled: false })
302301
if (result.warnings && result.warnings.length > 0) {
303302
setDeployWarnings(result.warnings)
304303
}
305-
await refetchDeployedState()
306304
} catch (error: unknown) {
307305
logger.error('Error deploying workflow:', { error })
308306
const errorMessage = error instanceof Error ? error.message : 'Failed to deploy workflow'
309307
setDeployError(errorMessage)
310308
}
311-
}, [workflowId, deployMutation, refetchDeployedState])
309+
}, [workflowId, deployMutation])
312310

313311
const handlePromoteToLive = useCallback(
314312
async (version: number) => {
@@ -321,13 +319,12 @@ export function DeployModal({
321319
if (result.warnings && result.warnings.length > 0) {
322320
setDeployWarnings(result.warnings)
323321
}
324-
await refetchDeployedState()
325322
} catch (error) {
326323
logger.error('Error promoting version:', { error })
327324
throw error
328325
}
329326
},
330-
[workflowId, activateVersionMutation, refetchDeployedState]
327+
[workflowId, activateVersionMutation]
331328
)
332329

333330
const handleUndeploy = useCallback(async () => {
@@ -367,13 +364,12 @@ export function DeployModal({
367364
if (result.warnings && result.warnings.length > 0) {
368365
setDeployWarnings(result.warnings)
369366
}
370-
await refetchDeployedState()
371367
} catch (error: unknown) {
372368
logger.error('Error redeploying workflow:', { error })
373369
const errorMessage = error instanceof Error ? error.message : 'Failed to redeploy workflow'
374370
setDeployError(errorMessage)
375371
}
376-
}, [workflowId, deployMutation, refetchDeployedState])
372+
}, [workflowId, deployMutation])
377373

378374
const handleCloseModal = useCallback(() => {
379375
setChatSubmitting(false)
@@ -385,17 +381,16 @@ export function DeployModal({
385381
const handleChatDeployed = useCallback(async () => {
386382
if (!workflowId) return
387383

388-
queryClient.invalidateQueries({ queryKey: deploymentKeys.versions(workflowId) })
384+
invalidateDeploymentQueries(queryClient, workflowId)
389385

390-
await refetchDeployedState()
391386
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
392387

393388
if (chatSuccessTimeoutRef.current) {
394389
clearTimeout(chatSuccessTimeoutRef.current)
395390
}
396391
setChatSuccess(true)
397392
chatSuccessTimeoutRef.current = setTimeout(() => setChatSuccess(false), 2000)
398-
}, [workflowId, queryClient, refetchDeployedState])
393+
}, [workflowId, queryClient])
399394

400395
const handleRefetchChat = useCallback(async () => {
401396
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)