diff --git a/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_workflow_yaml_fields.test.ts b/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_workflow_yaml_fields.test.ts index d91111d0bb942..f9e285867e10e 100644 --- a/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_workflow_yaml_fields.test.ts +++ b/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_workflow_yaml_fields.test.ts @@ -63,6 +63,52 @@ steps: []`; expect(result).toContain('enabled: false'); }); + it('should preserve comments, blank lines, and formatting when toggling enabled', () => { + const yaml = `# Workflow configuration +name: Test Workflow + +# Whether the workflow is active +enabled: false + +steps: + # Create a Jira ticket + - name: step1 + type: jira + params: + summary: test`; + const workflow: Partial = { enabled: true }; + + const result = updateWorkflowYamlFields(yaml, workflow, true); + + expect(result).toContain('enabled: true'); + expect(result).not.toContain('enabled: false'); + expect(result).toContain('# Workflow configuration'); + expect(result).toContain('# Whether the workflow is active'); + expect(result).toContain('# Create a Jira ticket'); + const blankLineCount = (result.match(/\n\n/g) || []).length; + expect(blankLineCount).toBeGreaterThanOrEqual(2); + expect(result).toContain('name: Test Workflow'); + expect(result).toContain('summary: test'); + }); + + it('should preserve quoted template expressions when toggling enabled', () => { + const yaml = `name: Test Workflow +enabled: false +steps: + - name: step1 + type: jira + params: + comment: "{{ inputs.comment }}" + summary: "{{ inputs.summary }}"`; + const workflow: Partial = { enabled: true }; + + const result = updateWorkflowYamlFields(yaml, workflow, true); + + expect(result).toContain('enabled: true'); + expect(result).toContain('comment: "{{ inputs.comment }}"'); + expect(result).toContain('summary: "{{ inputs.summary }}"'); + }); + it('should use enabledValue parameter when provided', () => { const yaml = 'name: Test Workflow\nenabled: true\nsteps: []'; const workflow: Partial = { enabled: true }; // Request to enable diff --git a/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_yaml_field.test.ts b/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_yaml_field.test.ts index 1e4f542a77d3e..efb2962266f6d 100644 --- a/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_yaml_field.test.ts +++ b/src/platform/plugins/shared/workflows_management/common/lib/yaml/update_yaml_field.test.ts @@ -138,6 +138,64 @@ steps: expect(result).toContain('- name: step1'); }); + it('should preserve comments, blank lines, and formatting when toggling enabled', () => { + const yaml = `# Workflow configuration +name: Test Workflow +description: A workflow that does things + +# Whether the workflow is active +enabled: false + +steps: + # Create a Jira ticket + - name: step1 + type: jira + params: + summary: test + + # Notify the user + - name: step2 + type: slack + params: + channel: general`; + + const result = updateYamlField(yaml, 'enabled', true); + + // Only the enabled value should change + expect(result).toContain('enabled: true'); + expect(result).not.toContain('enabled: false'); + + // All comments preserved + expect(result).toContain('# Workflow configuration'); + expect(result).toContain('# Whether the workflow is active'); + expect(result).toContain('# Create a Jira ticket'); + expect(result).toContain('# Notify the user'); + + // Blank lines preserved + const blankLineCount = (result.match(/\n\n/g) || []).length; + expect(blankLineCount).toBeGreaterThanOrEqual(3); + + // Rest of content unchanged + expect(result).toContain('name: Test Workflow'); + expect(result).toContain('description: A workflow that does things'); + expect(result).toContain('channel: general'); + }); + + it('should preserve template expressions like {{ inputs.comment }}', () => { + const yaml = `name: Test Workflow +enabled: false +steps: + - name: step1 + type: jira + params: + comment: "{{ inputs.comment }}"`; + + const result = updateYamlField(yaml, 'enabled', true); + + expect(result).toContain('enabled: true'); + expect(result).toContain('comment: "{{ inputs.comment }}"'); + }); + it('should handle nested field paths with dot notation', () => { const yaml = `metadata: version: 1.0 diff --git a/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.test.ts b/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.test.ts index a5e08210f0654..ac3a4047cdffa 100644 --- a/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.test.ts +++ b/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.test.ts @@ -1486,6 +1486,76 @@ steps: }) ); }); + + it('should preserve YAML comments, formatting, and template expressions when toggling enabled', async () => { + const mockRequest = { + auth: { + credentials: { username: 'test-user' }, + }, + } as any; + + const yamlWithComments = `# Workflow configuration +name: Test Workflow +description: A test workflow + +# Whether the workflow is active +enabled: false + +triggers: + - type: manual + +steps: + # Create a Jira ticket + - type: console + name: first-step + with: + message: "{{ inputs.comment }}"`; + + const existingDoc = { + _id: 'test-workflow-id', + _source: { + ...mockWorkflowDocument._source, + enabled: false, + yaml: yamlWithComments, + definition: { + name: 'Test Workflow', + enabled: false, + triggers: [{ type: 'manual' }], + steps: [ + { + type: 'console', + name: 'first-step', + with: { message: '{{ inputs.comment }}' }, + }, + ], + }, + }, + }; + + mockEsClient.search.mockResolvedValue({ hits: { hits: [existingDoc] } } as any); + + // Toggle enabled without providing yaml (metadata-only update) + await service.updateWorkflow('test-workflow-id', { enabled: true }, 'default', mockRequest); + + const indexCall = mockEsClient.index.mock.calls[0][0] as any; + const savedYaml = indexCall.document.yaml; + + // enabled should be toggled + expect(savedYaml).toContain('enabled: true'); + expect(savedYaml).not.toContain('enabled: false'); + + // Comments should be preserved + expect(savedYaml).toContain('# Workflow configuration'); + expect(savedYaml).toContain('# Whether the workflow is active'); + expect(savedYaml).toContain('# Create a Jira ticket'); + + // Template expressions should not be corrupted + expect(savedYaml).toContain('{{ inputs.comment }}'); + expect(savedYaml).not.toContain('null'); + + // Blank lines should be preserved + expect((savedYaml.match(/\n\n/g) || []).length).toBeGreaterThanOrEqual(2); + }); }); describe('deleteWorkflows', () => { diff --git a/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.ts b/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.ts index b65381e6e9a40..deff5d97f30bc 100644 --- a/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.ts +++ b/src/platform/plugins/shared/workflows_management/server/workflows_management/workflows_management_service.ts @@ -69,11 +69,7 @@ import { } from '../../common/lib/errors'; import { validateStepNameUniqueness } from '../../common/lib/validate_step_names'; -import { - parseWorkflowYamlToJSON, - parseYamlToJSONWithoutValidation, - stringifyWorkflowDefinition, -} from '../../common/lib/yaml'; +import { parseWorkflowYamlToJSON, updateWorkflowYamlFields } from '../../common/lib/yaml'; import { getWorkflowZodSchema } from '../../common/schema'; import { getAuthenticatedUser } from '../lib/get_user'; import { hasScheduledTriggers } from '../lib/schedule_utils'; @@ -531,23 +527,17 @@ export class WorkflowsService { } if (yamlUpdated && existingDocument._source?.yaml) { - const originalYamlParse = parseYamlToJSONWithoutValidation(existingDocument._source.yaml); - const baseDefinition = originalYamlParse.success - ? originalYamlParse.json - : existingDocument._source?.definition; - - if (baseDefinition) { - const fieldUpdates = { - ...(workflow.name !== undefined && { name: workflow.name }), - ...(workflow.enabled !== undefined && { enabled: updatedData.enabled }), - ...(workflow.description !== undefined && { description: workflow.description }), - ...(workflow.tags !== undefined && { tags: workflow.tags }), - }; - updatedData.yaml = stringifyWorkflowDefinition({ - ...baseDefinition, - ...fieldUpdates, - }); - } + // Use in-place YAML field updates to preserve formatting, comments, + // and template expressions that would be corrupted by a parse-to-JSON then re-stringify cycle. + // `enabledValue` is passed separately because the server may override + // the requested value (e.g. force `false` when the workflow has no valid + // definition). Other fields (name, description, tags) are read directly + // from the `workflow` object inside `updateWorkflowYamlFields`. + updatedData.yaml = updateWorkflowYamlFields( + existingDocument._source.yaml, + workflow, + updatedData.enabled + ); } }