fix(deploy): fix workflow change detection to handle old variable reference format (#2623)

This commit is contained in:
Waleed
2025-12-29 02:09:38 -08:00
committed by GitHub
parent 1c626dfcae
commit 88065088bf
3 changed files with 213 additions and 4 deletions

View File

@@ -2523,4 +2523,181 @@ describe('hasWorkflowChanged', () => {
}
)
})
describe('Variables (UI-only fields should not trigger change)', () => {
it.concurrent('should not detect change when validationError differs', () => {
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(deployedState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'plain',
value: 'test',
},
}
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(currentState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'plain',
value: 'test',
validationError: undefined,
},
}
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
})
it.concurrent('should not detect change when validationError has value vs missing', () => {
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(deployedState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'number',
value: 'invalid',
},
}
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(currentState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'number',
value: 'invalid',
validationError: 'Not a valid number',
},
}
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
})
it.concurrent('should detect change when variable value differs', () => {
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(deployedState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'plain',
value: 'old value',
},
}
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(currentState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'plain',
value: 'new value',
validationError: undefined,
},
}
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
})
it.concurrent('should detect change when variable is added', () => {
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(deployedState as any).variables = {}
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(currentState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'plain',
value: 'test',
},
}
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
})
it.concurrent('should detect change when variable is removed', () => {
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(deployedState as any).variables = {
var1: {
id: 'var1',
workflowId: 'workflow1',
name: 'myVar',
type: 'plain',
value: 'test',
},
}
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(currentState as any).variables = {}
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
})
it.concurrent('should not detect change when empty array vs empty object', () => {
const deployedState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(deployedState as any).variables = []
const currentState = createWorkflowState({
blocks: {
block1: createBlock('block1'),
},
})
;(currentState as any).variables = {}
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
})
})
})

View File

@@ -6,8 +6,10 @@ import {
normalizeLoop,
normalizeParallel,
normalizeValue,
normalizeVariables,
sanitizeInputFormat,
sanitizeTools,
sanitizeVariable,
sortEdges,
} from './normalize'
@@ -228,11 +230,17 @@ export function hasWorkflowChanged(
}
// 6. Compare variables
const currentVariables = (currentState as any).variables || {}
const deployedVariables = (deployedState as any).variables || {}
const currentVariables = normalizeVariables((currentState as any).variables)
const deployedVariables = normalizeVariables((deployedState as any).variables)
const normalizedCurrentVars = normalizeValue(currentVariables)
const normalizedDeployedVars = normalizeValue(deployedVariables)
const normalizedCurrentVars = normalizeValue(
Object.fromEntries(Object.entries(currentVariables).map(([id, v]) => [id, sanitizeVariable(v)]))
)
const normalizedDeployedVars = normalizeValue(
Object.fromEntries(
Object.entries(deployedVariables).map(([id, v]) => [id, sanitizeVariable(v)])
)
)
if (normalizedStringify(normalizedCurrentVars) !== normalizedStringify(normalizedDeployedVars)) {
return true

View File

@@ -88,6 +88,30 @@ export function sanitizeTools(tools: any[] | undefined): any[] {
return tools.map(({ isExpanded, ...rest }) => rest)
}
/**
* Sanitizes a variable by removing UI-only fields like validationError
* @param variable - The variable object
* @returns Sanitized variable object
*/
export function sanitizeVariable(variable: any): any {
if (!variable || typeof variable !== 'object') return variable
const { validationError, ...rest } = variable
return rest
}
/**
* Normalizes the variables structure to always be an object.
* Handles legacy data where variables might be stored as an empty array.
* @param variables - The variables to normalize
* @returns A normalized variables object
*/
export function normalizeVariables(variables: any): Record<string, any> {
if (!variables) return {}
if (Array.isArray(variables)) return {}
if (typeof variables !== 'object') return {}
return variables
}
/**
* Sanitizes inputFormat array by removing UI-only fields like value and collapsed
* @param inputFormat - Array of input format configurations