Skip to content

Commit 895e167

Browse files
dej611flash1293
authored andcommitted
[One Workflow] Fix the variable type detection within liquidjs for loops (elastic#270596)
## Summary Liquid `{% for %}` loops in workflow YAML were not validated or autocompleted reliably. Invalid collection paths could slip through, and loop variables inside block-folded (`>-`) message fields were often treated as unknown because offset mapping used the wrong source text. This change validates for-loop collection paths against the step context schema (same rules as `foreach` steps) and reports dedicated diagnostics, separate from variable validation. Loop variables in invalid collections stay permissive in the variable pass so users get one clear error on the collection, not duplicate markers. Block literal and folded scalars now map cursor offsets using the editor’s YAML source, so template-local context (assign, capture, for-loop scope) lines up with what the user actually typed. Liquid syntax and for-loop collection checks share a single YAML pass when the workflow graph is available, and template parsing skips work when a scalar has no `{%` tags. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
1 parent 79e1221 commit 895e167

18 files changed

Lines changed: 1943 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { WorkflowYaml } from '@kbn/workflows';
11+
12+
/** Foreach over consts.items; summarize step uses liquid for-loops against predecessor schemas. */
13+
export const FOR_LOOP_VALIDATION_YAML = `name: For-loop collection validation
14+
enabled: false
15+
triggers:
16+
- type: manual
17+
consts:
18+
items:
19+
- name: Alice
20+
- name: Bob
21+
steps:
22+
- name: iterate_items
23+
type: foreach
24+
foreach: '{{ consts.items }}'
25+
steps:
26+
- name: log_item
27+
type: console
28+
with:
29+
message: '{{ foreach.item.name }}'
30+
- name: summarize
31+
type: console
32+
with:
33+
message: >-
34+
{%- for xx in steps.non_existing_step %}
35+
- {{ xx }}
36+
{%- endfor %}
37+
{%- for yy in steps.iterate_items.items %}
38+
- {{ yy.name }}
39+
{%- endfor %}
40+
{%- for zz in steps.log_item %}
41+
- {{ zz }}
42+
{%- endfor %}
43+
`;
44+
45+
export const FOR_LOOP_VALIDATION_FOLDED_MESSAGE =
46+
'{%- for xx in steps.non_existing_step %} - {{ xx }} {%- endfor %} {%- for yy in steps.iterate_items.items %} - {{ yy.name }} {%- endfor %} {%- for zz in steps.log_item %} - {{ zz }} {%- endfor %}';
47+
48+
export const forLoopValidationWorkflowDefinition = {
49+
version: '1',
50+
name: 'For-loop collection validation',
51+
enabled: false,
52+
triggers: [{ type: 'manual' }],
53+
consts: {
54+
items: [{ name: 'Alice' }, { name: 'Bob' }],
55+
},
56+
steps: [
57+
{
58+
name: 'iterate_items',
59+
type: 'foreach',
60+
foreach: '{{ consts.items }}',
61+
steps: [
62+
{
63+
name: 'log_item',
64+
type: 'console',
65+
with: {
66+
message: '{{ foreach.item.name }}',
67+
},
68+
},
69+
],
70+
},
71+
{
72+
name: 'summarize',
73+
type: 'console',
74+
with: { message: FOR_LOOP_VALIDATION_FOLDED_MESSAGE },
75+
},
76+
],
77+
} as WorkflowYaml;
78+
79+
export const FOR_LOOP_FOLDED_ONLY_YAML = `name: Folded for-loop validation
80+
enabled: false
81+
triggers:
82+
- type: manual
83+
consts:
84+
items:
85+
- name: Alice
86+
steps:
87+
- name: iterate_items
88+
type: foreach
89+
foreach: '{{ consts.items }}'
90+
steps:
91+
- name: log_item
92+
type: console
93+
with:
94+
message: '{{ foreach.item.name }}'
95+
- name: summarize
96+
type: console
97+
with:
98+
message: >-
99+
{%- for bad in steps.non_existing_step %}
100+
- {{ bad }}
101+
{%- endfor %}
102+
`;
103+
104+
export const forLoopFoldedOnlyWorkflowDefinition = {
105+
version: '1',
106+
name: 'Folded for-loop validation',
107+
enabled: false,
108+
triggers: [{ type: 'manual' }],
109+
consts: {
110+
items: [{ name: 'Alice' }],
111+
},
112+
steps: [
113+
{
114+
name: 'iterate_items',
115+
type: 'foreach',
116+
foreach: '{{ consts.items }}',
117+
steps: [
118+
{
119+
name: 'log_item',
120+
type: 'console',
121+
with: { message: '{{ foreach.item.name }}' },
122+
},
123+
],
124+
},
125+
{
126+
name: 'summarize',
127+
type: 'console',
128+
with: {
129+
message: '{%- for bad in steps.non_existing_step %} - {{ bad }} {%- endfor %}',
130+
},
131+
},
132+
],
133+
} as WorkflowYaml;
134+
135+
export const FOR_LOOP_NESTED_YAML = `name: Nested for-loop validation
136+
enabled: false
137+
triggers:
138+
- type: manual
139+
consts:
140+
items:
141+
- name: Alice
142+
steps:
143+
- name: iterate_items
144+
type: foreach
145+
foreach: '{{ consts.items }}'
146+
steps:
147+
- name: log_item
148+
type: console
149+
with:
150+
message: '{{ foreach.item.name }}'
151+
- name: summarize
152+
type: console
153+
with:
154+
message: >-
155+
{%- for outer in steps.iterate_items.items %}
156+
{%- for inner in steps.non_existing_step %}
157+
- {{ inner }}
158+
{%- endfor %}
159+
{%- endfor %}
160+
`;
161+
162+
export const forLoopNestedWorkflowDefinition = {
163+
version: '1',
164+
name: 'Nested for-loop validation',
165+
enabled: false,
166+
triggers: [{ type: 'manual' }],
167+
consts: {
168+
items: [{ name: 'Alice' }],
169+
},
170+
steps: [
171+
{
172+
name: 'iterate_items',
173+
type: 'foreach',
174+
foreach: '{{ consts.items }}',
175+
steps: [
176+
{
177+
name: 'log_item',
178+
type: 'console',
179+
with: { message: '{{ foreach.item.name }}' },
180+
},
181+
],
182+
},
183+
{
184+
name: 'summarize',
185+
type: 'console',
186+
with: {
187+
message:
188+
'{%- for outer in steps.iterate_items.items %}{%- for inner in steps.non_existing_step %}- {{ inner }}{%- endfor %}{%- endfor %}',
189+
},
190+
},
191+
],
192+
} as WorkflowYaml;
193+
194+
export const FOR_LOOP_RUNTIME_JSON_YAML = `name: Runtime JSON for-loop
195+
enabled: false
196+
triggers:
197+
- type: manual
198+
steps:
199+
- name: fetch
200+
type: console
201+
with:
202+
message: 'done'
203+
- name: summarize
204+
type: console
205+
with:
206+
message: '{% for row in steps.fetch.output %}{{ row }}{% endfor %}'
207+
`;
208+
209+
export const forLoopRuntimeJsonWorkflowDefinition = {
210+
version: '1',
211+
name: 'Runtime JSON for-loop',
212+
enabled: false,
213+
triggers: [{ type: 'manual' }],
214+
steps: [
215+
{
216+
name: 'fetch',
217+
type: 'console',
218+
with: { message: 'done' },
219+
},
220+
{
221+
name: 'summarize',
222+
type: 'console',
223+
with: {
224+
message: '{% for row in steps.fetch.output %}{{ row }}{% endfor %}',
225+
},
226+
},
227+
],
228+
} satisfies WorkflowYaml;

src/platform/plugins/shared/workflows_management/public/features/validate_workflow_yaml/lib/use_yaml_validation.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { validateConnectorIds } from './validate_connector_ids';
1818
import { validateDeprecatedStepTypes } from './validate_deprecated_step_types';
1919
import { validateIfConditions } from './validate_if_conditions';
2020
import { validateJsonSchemaDefaults } from './validate_json_schema_defaults';
21-
import { validateLiquidTemplate } from './validate_liquid_template';
21+
import { validateLiquidYamlScalars } from './validate_liquid_yaml_scalars';
2222
import { validateStepNameUniqueness } from './validate_step_name_uniqueness';
2323
import { validateStepProperties } from './validate_step_properties';
2424
import { validateTriggerConditions } from './validate_trigger_conditions';
@@ -150,9 +150,20 @@ export function useYamlValidation(
150150
// (e.g. during editing when the YAML doesn't fully match the workflow schema yet)
151151
// so that connector-id, step-name, liquid-template, step-property, and
152152
// workflow-inputs validation still provide feedback.
153+
const yamlString = model.getValue();
154+
const liquidScalarResults =
155+
workflowGraph && workflowDefinition
156+
? validateLiquidYamlScalars(
157+
yamlString,
158+
yamlDocument,
159+
model,
160+
workflowGraph,
161+
workflowDefinition
162+
)
163+
: validateLiquidYamlScalars(yamlString, yamlDocument, null);
153164
const results: YamlValidationResult[] = [
154165
...(lineCounter ? validateStepNameUniqueness(yamlDocument, lineCounter) : []),
155-
...validateLiquidTemplate(model.getValue(), yamlDocument),
166+
...liquidScalarResults.filter((result) => result.owner === 'liquid-template-validation'),
156167
...validateConnectorIds(connectorIdItems, dynamicConnectorTypes, connectorsManagementUrl),
157168
...validateWorkflowOutputsInYaml(yamlDocument, model, workflowDefinition?.outputs),
158169
...(stepPropertyItems ? await validateStepProperties(stepPropertyItems) : []),
@@ -191,6 +202,7 @@ export function useYamlValidation(
191202
yamlDocument,
192203
model
193204
),
205+
...liquidScalarResults.filter((result) => result.owner === 'variable-validation'),
194206
...validateJsonSchemaDefaults(yamlDocument, workflowDefinition, model)
195207
);
196208
}

0 commit comments

Comments
 (0)