mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(variables): update workflows to use deployed variables, not local ones to align with the rest of the canvas components (#2577)
* improvement(variables): update workflows to use deployed variables, not local ones to align with the rest of the canvas components * update change detection to ignore trigger id since it is runtime metadata and not actually required to be redeployed
This commit is contained in:
@@ -60,11 +60,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
const { loadWorkflowFromNormalizedTables } = await import('@/lib/workflows/persistence/utils')
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(id)
|
||||
if (normalizedData) {
|
||||
const [workflowRecord] = await db
|
||||
.select({ variables: workflow.variables })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
.limit(1)
|
||||
|
||||
const currentState = {
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
variables: workflowRecord?.variables || {},
|
||||
}
|
||||
const { hasWorkflowChanged } = await import('@/lib/workflows/comparison')
|
||||
needsRedeployment = hasWorkflowChanged(currentState as any, active.state as any)
|
||||
|
||||
@@ -318,6 +318,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
loops: Record<string, any>
|
||||
parallels: Record<string, any>
|
||||
deploymentVersionId?: string
|
||||
variables?: Record<string, any>
|
||||
} | null = null
|
||||
|
||||
let processedInput = input
|
||||
@@ -327,6 +328,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
: await loadDeployedWorkflowState(workflowId)
|
||||
|
||||
if (workflowData) {
|
||||
const deployedVariables =
|
||||
!shouldUseDraftState && 'variables' in workflowData
|
||||
? (workflowData as any).variables
|
||||
: undefined
|
||||
|
||||
cachedWorkflowData = {
|
||||
blocks: workflowData.blocks,
|
||||
edges: workflowData.edges,
|
||||
@@ -336,6 +342,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
!shouldUseDraftState && 'deploymentVersionId' in workflowData
|
||||
? (workflowData.deploymentVersionId as string)
|
||||
: undefined,
|
||||
variables: deployedVariables,
|
||||
}
|
||||
|
||||
const serializedWorkflow = new Serializer().serializeWorkflow(
|
||||
@@ -405,11 +412,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
workflowStateOverride: effectiveWorkflowStateOverride,
|
||||
}
|
||||
|
||||
const executionVariables = cachedWorkflowData?.variables ?? workflow.variables ?? {}
|
||||
|
||||
const snapshot = new ExecutionSnapshot(
|
||||
metadata,
|
||||
workflow,
|
||||
processedInput,
|
||||
workflow.variables || {},
|
||||
executionVariables,
|
||||
selectedOutputs
|
||||
)
|
||||
|
||||
@@ -471,6 +480,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
selectedOutputs,
|
||||
cachedWorkflowData?.blocks || {}
|
||||
)
|
||||
const streamVariables = cachedWorkflowData?.variables ?? (workflow as any).variables
|
||||
|
||||
const stream = await createStreamingResponse({
|
||||
requestId,
|
||||
workflow: {
|
||||
@@ -478,7 +489,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
userId: actorUserId,
|
||||
workspaceId,
|
||||
isDeployed: workflow.isDeployed,
|
||||
variables: (workflow as any).variables,
|
||||
variables: streamVariables,
|
||||
},
|
||||
input: processedInput,
|
||||
executingUserId: actorUserId,
|
||||
@@ -675,11 +686,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
workflowStateOverride: effectiveWorkflowStateOverride,
|
||||
}
|
||||
|
||||
const sseExecutionVariables = cachedWorkflowData?.variables ?? workflow.variables ?? {}
|
||||
|
||||
const snapshot = new ExecutionSnapshot(
|
||||
metadata,
|
||||
workflow,
|
||||
processedInput,
|
||||
workflow.variables || {},
|
||||
sseExecutionVariables,
|
||||
selectedOutputs
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, workflowDeploymentVersion } from '@sim/db'
|
||||
import { db, workflow, workflowDeploymentVersion } from '@sim/db'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
@@ -22,17 +22,12 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Check if the workflow has meaningful changes that would require redeployment
|
||||
let needsRedeployment = false
|
||||
|
||||
if (validation.workflow.isDeployed) {
|
||||
// Get current state from normalized tables (same logic as deployment API)
|
||||
// Load current state from normalized tables using centralized helper
|
||||
const normalizedData = await loadWorkflowFromNormalizedTables(id)
|
||||
|
||||
if (!normalizedData) {
|
||||
// Workflow exists but has no blocks in normalized tables (empty workflow or not migrated)
|
||||
// This is valid state - return success with no redeployment needed
|
||||
return createSuccessResponse({
|
||||
isDeployed: validation.workflow.isDeployed,
|
||||
deployedAt: validation.workflow.deployedAt,
|
||||
@@ -41,11 +36,18 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
})
|
||||
}
|
||||
|
||||
const [workflowRecord] = await db
|
||||
.select({ variables: workflow.variables })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
.limit(1)
|
||||
|
||||
const currentState = {
|
||||
blocks: normalizedData.blocks,
|
||||
edges: normalizedData.edges,
|
||||
loops: normalizedData.loops,
|
||||
parallels: normalizedData.parallels,
|
||||
variables: workflowRecord?.variables || {},
|
||||
lastSaved: Date.now(),
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ interface DeployModalProps {
|
||||
workflowId: string | null
|
||||
isDeployed: boolean
|
||||
needsRedeployment: boolean
|
||||
setNeedsRedeployment: (value: boolean) => void
|
||||
deployedState: WorkflowState
|
||||
isLoadingDeployedState: boolean
|
||||
refetchDeployedState: () => Promise<void>
|
||||
@@ -58,7 +57,6 @@ export function DeployModal({
|
||||
workflowId,
|
||||
isDeployed: isDeployedProp,
|
||||
needsRedeployment,
|
||||
setNeedsRedeployment,
|
||||
deployedState,
|
||||
isLoadingDeployedState,
|
||||
refetchDeployedState,
|
||||
@@ -229,7 +227,6 @@ export function DeployModal({
|
||||
|
||||
setDeploymentStatus(workflowId, isDeployedStatus, deployedAtTime, apiKeyLabel)
|
||||
|
||||
setNeedsRedeployment(false)
|
||||
if (workflowId) {
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
|
||||
}
|
||||
@@ -453,7 +450,6 @@ export function DeployModal({
|
||||
getApiKeyLabel(apiKey)
|
||||
)
|
||||
|
||||
setNeedsRedeployment(false)
|
||||
if (workflowId) {
|
||||
useWorkflowRegistry.getState().setWorkflowNeedsRedeployment(workflowId, false)
|
||||
}
|
||||
|
||||
@@ -45,8 +45,7 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
|
||||
isRegistryLoading,
|
||||
})
|
||||
|
||||
// Detect changes between current and deployed state
|
||||
const { changeDetected, setChangeDetected } = useChangeDetection({
|
||||
const { changeDetected } = useChangeDetection({
|
||||
workflowId: activeWorkflowId,
|
||||
deployedState,
|
||||
isLoadingDeployedState,
|
||||
@@ -136,7 +135,6 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
|
||||
workflowId={activeWorkflowId}
|
||||
isDeployed={isDeployed}
|
||||
needsRedeployment={changeDetected}
|
||||
setNeedsRedeployment={setChangeDetected}
|
||||
deployedState={deployedState!}
|
||||
isLoadingDeployedState={isLoadingDeployedState}
|
||||
refetchDeployedState={refetchWithErrorHandling}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { hasWorkflowChanged } from '@/lib/workflows/comparison'
|
||||
import { useDebounce } from '@/hooks/use-debounce'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
@@ -27,8 +28,18 @@ export function useChangeDetection({
|
||||
const subBlockValues = useSubBlockStore((state) =>
|
||||
workflowId ? state.workflowValues[workflowId] : null
|
||||
)
|
||||
const allVariables = useVariablesStore((state) => state.variables)
|
||||
const workflowVariables = useMemo(() => {
|
||||
if (!workflowId) return {}
|
||||
const vars: Record<string, any> = {}
|
||||
for (const [id, variable] of Object.entries(allVariables)) {
|
||||
if (variable.workflowId === workflowId) {
|
||||
vars[id] = variable
|
||||
}
|
||||
}
|
||||
return vars
|
||||
}, [workflowId, allVariables])
|
||||
|
||||
// Build current state with subblock values merged into blocks
|
||||
const currentState = useMemo((): WorkflowState | null => {
|
||||
if (!workflowId) return null
|
||||
|
||||
@@ -37,12 +48,10 @@ export function useChangeDetection({
|
||||
const blockSubValues = subBlockValues?.[blockId] || {}
|
||||
const subBlocks: Record<string, any> = {}
|
||||
|
||||
// Merge subblock values into the block's subBlocks structure
|
||||
for (const [subId, value] of Object.entries(blockSubValues)) {
|
||||
subBlocks[subId] = { value }
|
||||
}
|
||||
|
||||
// Also include existing subBlocks from the block itself
|
||||
if (block.subBlocks) {
|
||||
for (const [subId, subBlock] of Object.entries(block.subBlocks)) {
|
||||
if (!subBlocks[subId]) {
|
||||
@@ -64,10 +73,10 @@ export function useChangeDetection({
|
||||
edges,
|
||||
loops,
|
||||
parallels,
|
||||
}
|
||||
}, [workflowId, blocks, edges, loops, parallels, subBlockValues])
|
||||
variables: workflowVariables,
|
||||
} as WorkflowState & { variables: Record<string, any> }
|
||||
}, [workflowId, blocks, edges, loops, parallels, subBlockValues, workflowVariables])
|
||||
|
||||
// Compute change detection with debouncing for performance
|
||||
const rawChangeDetected = useMemo(() => {
|
||||
if (!currentState || !deployedState || isLoadingDeployedState) {
|
||||
return false
|
||||
@@ -75,13 +84,7 @@ export function useChangeDetection({
|
||||
return hasWorkflowChanged(currentState, deployedState)
|
||||
}, [currentState, deployedState, isLoadingDeployedState])
|
||||
|
||||
// Debounce to avoid UI flicker during rapid edits
|
||||
const changeDetected = useDebounce(rawChangeDetected, 300)
|
||||
|
||||
const setChangeDetected = () => {
|
||||
// No-op: change detection is now computed, not stateful
|
||||
// Kept for API compatibility
|
||||
}
|
||||
|
||||
return { changeDetected, setChangeDetected }
|
||||
return { changeDetected }
|
||||
}
|
||||
|
||||
@@ -2075,4 +2075,437 @@ describe('hasWorkflowChanged', () => {
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Variable Changes', () => {
|
||||
it.concurrent('should detect added variables', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should detect removed variables', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should detect variable value changes', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'world' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should detect variable type changes', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: '123' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'number', value: 123 },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should detect variable name changes', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'oldName', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'newName', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change for identical variables', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change for empty variables on both sides', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change for undefined vs empty object variables', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: undefined,
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should handle complex variable values (objects)', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'config', type: 'object', value: { key: 'value1' } },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'config', type: 'object', value: { key: 'value2' } },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should handle complex variable values (arrays)', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'items', type: 'array', value: [1, 2, 3] },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'items', type: 'array', value: [1, 2, 4] },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change when variable key order differs', () => {
|
||||
const deployedState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
},
|
||||
}
|
||||
|
||||
const currentState = {
|
||||
...createWorkflowState({}),
|
||||
variables: {
|
||||
var2: { id: 'var2', name: 'count', type: 'number', value: 42 },
|
||||
var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' },
|
||||
},
|
||||
}
|
||||
|
||||
expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Trigger Runtime Metadata (Should Not Trigger Change)', () => {
|
||||
it.concurrent('should not detect change when webhookId differs', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
webhookId: { value: null },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change when triggerPath differs', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
triggerPath: { value: '' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
triggerPath: { value: '/api/webhooks/abc123' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change when testUrl differs', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
testUrl: { value: null },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
testUrl: { value: 'https://test.example.com/webhook' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change when testUrlExpiresAt differs', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
testUrlExpiresAt: { value: null },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
testUrlExpiresAt: { value: '2025-12-31T23:59:59Z' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should not detect change when all runtime metadata differs', () => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
webhookId: { value: null },
|
||||
triggerPath: { value: '' },
|
||||
testUrl: { value: null },
|
||||
testUrlExpiresAt: { value: null },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
triggerPath: { value: '/api/webhooks/abc123' },
|
||||
testUrl: { value: 'https://test.example.com/webhook' },
|
||||
testUrlExpiresAt: { value: '2025-12-31T23:59:59Z' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should detect change when triggerConfig differs but runtime metadata also differs',
|
||||
() => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
webhookId: { value: null },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'pull_request' } },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent(
|
||||
'should not detect change when runtime metadata is added to current state',
|
||||
() => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
webhookId: { value: 'wh_123456' },
|
||||
triggerPath: { value: '/api/webhooks/abc123' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent(
|
||||
'should not detect change when runtime metadata is removed from current state',
|
||||
() => {
|
||||
const deployedState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
webhookId: { value: 'wh_old123' },
|
||||
triggerPath: { value: '/api/webhooks/old' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const currentState = createWorkflowState({
|
||||
blocks: {
|
||||
block1: createBlock('block1', {
|
||||
type: 'starter',
|
||||
subBlocks: {
|
||||
triggerConfig: { value: { event: 'push' } },
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||
import {
|
||||
normalizedStringify,
|
||||
normalizeEdge,
|
||||
@@ -100,10 +101,12 @@ export function hasWorkflowChanged(
|
||||
subBlocks: undefined,
|
||||
}
|
||||
|
||||
// Get all subBlock IDs from both states
|
||||
// Get all subBlock IDs from both states, excluding runtime metadata
|
||||
const allSubBlockIds = [
|
||||
...new Set([...Object.keys(currentSubBlocks), ...Object.keys(deployedSubBlocks)]),
|
||||
].sort()
|
||||
]
|
||||
.filter((id) => !TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(id))
|
||||
.sort()
|
||||
|
||||
// Normalize and compare each subBlock
|
||||
for (const subBlockId of allSubBlockIds) {
|
||||
@@ -224,5 +227,16 @@ export function hasWorkflowChanged(
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Compare variables
|
||||
const currentVariables = (currentState as any).variables || {}
|
||||
const deployedVariables = (deployedState as any).variables || {}
|
||||
|
||||
const normalizedCurrentVars = normalizeValue(currentVariables)
|
||||
const normalizedDeployedVars = normalizeValue(deployedVariables)
|
||||
|
||||
if (normalizedStringify(normalizedCurrentVars) !== normalizedStringify(normalizedDeployedVars)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface NormalizedWorkflowData {
|
||||
|
||||
export interface DeployedWorkflowData extends NormalizedWorkflowData {
|
||||
deploymentVersionId: string
|
||||
variables?: Record<string, any>
|
||||
}
|
||||
|
||||
export async function blockExistsInDeployment(
|
||||
@@ -94,13 +95,14 @@ export async function loadDeployedWorkflowState(workflowId: string): Promise<Dep
|
||||
throw new Error(`Workflow ${workflowId} has no active deployment`)
|
||||
}
|
||||
|
||||
const state = active.state as WorkflowState
|
||||
const state = active.state as WorkflowState & { variables?: Record<string, any> }
|
||||
|
||||
return {
|
||||
blocks: state.blocks || {},
|
||||
edges: state.edges || [],
|
||||
loops: state.loops || {},
|
||||
parallels: state.parallels || {},
|
||||
variables: state.variables || {},
|
||||
isFromNormalizedTables: false,
|
||||
deploymentVersionId: active.id,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user