Skip to content

Commit d64302f

Browse files
authored
feat: extend the schema of the requires stanza (#315)
1 parent d8bf965 commit d64302f

File tree

8 files changed

+258
-16
lines changed

8 files changed

+258
-16
lines changed

pkg/ast/workflow.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type JobRef struct {
2727
// not the job that will be executed
2828
StepName string
2929
StepNameRange protocol.Range
30-
Requires []TextAndRange
30+
Requires []Require
3131
Context []TextAndRange
3232
Type string
3333
TypeRange protocol.Range
@@ -44,8 +44,9 @@ type JobRef struct {
4444
}
4545

4646
type Require struct {
47-
Name string
48-
Range protocol.Range
47+
Name string
48+
Status []string
49+
Range protocol.Range
4950
}
5051

5152
type WorkflowTrigger struct {

pkg/parser/validate/workflow.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@ func (val Validate) validateSingleWorkflow(workflow ast.Workflow) error {
4242
val.validateWorkflowParameters(jobRef, jobRef.JobName, jobRef.JobRefRange)
4343
}
4444
for _, require := range jobRef.Requires {
45-
if !val.doesJobRefExist(workflow, require.Text) && !utils.CheckIfMatrixParamIsPartiallyReferenced(require.Text) {
45+
if !val.doesJobRefExist(workflow, require.Name) && !utils.CheckIfMatrixParamIsPartiallyReferenced(require.Name) {
4646
val.addDiagnostic(utils.CreateErrorDiagnosticFromRange(
4747
require.Range,
48-
fmt.Sprintf("Cannot find declaration for job reference %s", require.Text)))
48+
fmt.Sprintf("Cannot find declaration for job reference %s", require.Name)))
4949
}
5050
}
5151

pkg/parser/workflows.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func (doc *YamlDocument) buildJobsDAG(jobRefs []ast.JobRef) map[string][]string
102102
res := make(map[string][]string)
103103
for _, jobRef := range jobRefs {
104104
for _, requirement := range jobRef.Requires {
105-
res[requirement.Text] = append(res[requirement.Text], jobRef.StepName)
105+
res[requirement.Name] = append(res[requirement.Name], jobRef.StepName)
106106
}
107107
}
108108
return res
@@ -246,12 +246,70 @@ func (doc *YamlDocument) parseContext(node *sitter.Node) []ast.TextAndRange {
246246
return doc.getNodeTextArrayWithRange(node)
247247
}
248248

249-
func (doc *YamlDocument) parseSingleJobRequires(node *sitter.Node) []ast.TextAndRange {
250-
array := doc.getNodeTextArrayWithRange(node)
251-
res := []ast.TextAndRange{}
252-
for _, require := range array {
253-
res = append(res, ast.TextAndRange{Text: require.Text, Range: require.Range})
249+
func (doc *YamlDocument) parseSingleJobRequires(requiresNode *sitter.Node) []ast.Require {
250+
blockSequenceNode := GetChildSequence(requiresNode)
251+
res := make([]ast.Require, 0, requiresNode.ChildCount())
252+
253+
if blockSequenceNode == nil {
254+
return res
254255
}
256+
257+
iterateOnBlockSequence(blockSequenceNode, func(requiresItemNode *sitter.Node) {
258+
getRequire := func(node *sitter.Node) ast.Require {
259+
defaultStatus := []string{"success"}
260+
if alias := GetChildOfType(node, "alias"); alias != nil {
261+
anchor, ok := doc.YamlAnchors[strings.TrimLeft(doc.GetNodeText(alias), "*")]
262+
if !ok {
263+
return ast.Require{Name: ""}
264+
}
265+
anchorValueNode := GetFirstChild(anchor.ValueNode)
266+
text := doc.GetNodeText(anchorValueNode)
267+
return ast.Require{Name: text, Status: defaultStatus, Range: doc.NodeToRange(anchorValueNode)}
268+
} else {
269+
return ast.Require{Name: doc.GetNodeText(node), Status: defaultStatus, Range: doc.NodeToRange(node)}
270+
}
271+
}
272+
273+
// If blockSequenceNode is a flow_sequence, then requiresItemNode is directly a flow_node
274+
if requiresItemNode.Type() == "flow_node" {
275+
res = append(res, getRequire(requiresItemNode))
276+
} else {
277+
// But if blockSequenceNode is a block_sequence, then requiresItemNode is a block_sequence_item
278+
// The first child of requiresItemNode is the hyphen node, the second child is what we need
279+
element := requiresItemNode.Child(1)
280+
// If the second child is a flow_node, then it is a simple require
281+
if element != nil && element.Type() == "flow_node" {
282+
res = append(res, getRequire(element))
283+
} else {
284+
// Otherwise the second child is a block_mapping, then it is a require with status
285+
blockMappingNode := GetChildOfType(element, "block_mapping")
286+
blockMappingPair := GetChildOfType(blockMappingNode, "block_mapping_pair")
287+
key, value := doc.GetKeyValueNodes(blockMappingPair)
288+
289+
if key == nil || value == nil {
290+
return
291+
}
292+
if GetFirstChild(value).Type() == "plain_scalar" {
293+
status := make([]string, 1)
294+
status[0] = doc.GetNodeText(value)
295+
res = append(res, ast.Require{Name: doc.GetNodeText(key), Status: status, Range: doc.NodeToRange(key)})
296+
} else {
297+
statusesNode := GetFirstChild(value)
298+
status := make([]string, 0, statusesNode.ChildCount())
299+
iterateOnBlockSequence(statusesNode, func(statusItemNode *sitter.Node) {
300+
if statusItemNode.Type() == "flow_node" {
301+
status = append(status, doc.GetNodeText(statusItemNode))
302+
}
303+
if statusItemNode.Type() == "block_sequence_item" {
304+
status = append(status, doc.GetNodeText(statusItemNode.Child(1)))
305+
}
306+
})
307+
res = append(res, ast.Require{Name: doc.GetNodeText(key), Status: status, Range: doc.NodeToRange(key)})
308+
}
309+
}
310+
}
311+
})
312+
255313
return res
256314
}
257315

pkg/parser/workflows_test.go

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) {
3535
const jobRef4 = `
3636
- test:
3737
name: say-my-name`
38+
const jobRef5 = `
39+
- test:
40+
requires:
41+
- setup: failed`
42+
const jobRef6 = `
43+
- test:
44+
requires:
45+
- setup: [success, canceled]`
3846

3947
type fields struct {
4048
Content []byte
@@ -128,9 +136,10 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) {
128136
Character: 6,
129137
},
130138
},
131-
Requires: []ast.TextAndRange{
139+
Requires: []ast.Require{
132140
{
133-
Text: "setup",
141+
Name: "setup",
142+
Status: []string{"success"},
134143
Range: protocol.Range{
135144
Start: protocol.Position{Line: 3, Character: 10},
136145
End: protocol.Position{Line: 3, Character: 15},
@@ -304,6 +313,108 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) {
304313
MatrixParams: make(map[string][]ast.ParameterValue),
305314
},
306315
},
316+
{
317+
name: "Job reference with requires and single status",
318+
fields: fields{Content: []byte(jobRef5)},
319+
args: args{jobRefNode: getFirstChildOfType(GetRootNode([]byte(jobRef5)), "block_sequence_item")},
320+
want: ast.JobRef{
321+
JobName: "test",
322+
JobRefRange: protocol.Range{
323+
Start: protocol.Position{
324+
Line: 1,
325+
Character: 0,
326+
},
327+
End: protocol.Position{
328+
Line: 3,
329+
Character: 23,
330+
},
331+
},
332+
JobNameRange: protocol.Range{
333+
Start: protocol.Position{
334+
Line: 1,
335+
Character: 2,
336+
},
337+
End: protocol.Position{
338+
Line: 1,
339+
Character: 6,
340+
},
341+
},
342+
StepName: "test",
343+
StepNameRange: protocol.Range{
344+
Start: protocol.Position{
345+
Line: 1,
346+
Character: 2,
347+
},
348+
End: protocol.Position{
349+
Line: 1,
350+
Character: 6,
351+
},
352+
},
353+
Requires: []ast.Require{
354+
{
355+
Name: "setup",
356+
Status: []string{"failed"},
357+
Range: protocol.Range{
358+
Start: protocol.Position{Line: 3, Character: 10},
359+
End: protocol.Position{Line: 3, Character: 15},
360+
},
361+
},
362+
},
363+
MatrixParams: make(map[string][]ast.ParameterValue),
364+
Parameters: make(map[string]ast.ParameterValue),
365+
},
366+
},
367+
{
368+
name: "Job reference with requires and multiple statuses",
369+
fields: fields{Content: []byte(jobRef6)},
370+
args: args{jobRefNode: getFirstChildOfType(GetRootNode([]byte(jobRef6)), "block_sequence_item")},
371+
want: ast.JobRef{
372+
JobName: "test",
373+
JobRefRange: protocol.Range{
374+
Start: protocol.Position{
375+
Line: 1,
376+
Character: 0,
377+
},
378+
End: protocol.Position{
379+
Line: 3,
380+
Character: 36,
381+
},
382+
},
383+
JobNameRange: protocol.Range{
384+
Start: protocol.Position{
385+
Line: 1,
386+
Character: 2,
387+
},
388+
End: protocol.Position{
389+
Line: 1,
390+
Character: 6,
391+
},
392+
},
393+
StepName: "test",
394+
StepNameRange: protocol.Range{
395+
Start: protocol.Position{
396+
Line: 1,
397+
Character: 2,
398+
},
399+
End: protocol.Position{
400+
Line: 1,
401+
Character: 6,
402+
},
403+
},
404+
Requires: []ast.Require{
405+
{
406+
Name: "setup",
407+
Status: []string{"success", "canceled"},
408+
Range: protocol.Range{
409+
Start: protocol.Position{Line: 3, Character: 10},
410+
End: protocol.Position{Line: 3, Character: 15},
411+
},
412+
},
413+
},
414+
MatrixParams: make(map[string][]ast.ParameterValue),
415+
Parameters: make(map[string]ast.ParameterValue),
416+
},
417+
},
307418
}
308419
for _, tt := range tests {
309420
t.Run(tt.name, func(t *testing.T) {

pkg/services/definition/workflows.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ func (def DefinitionStruct) searchForWorkflows() []protocol.Location {
2929
return []protocol.Location{}
3030
}
3131

32-
func (def DefinitionStruct) searchForWorkflowJobsRequires(requires []ast.TextAndRange, workflow ast.Workflow) []protocol.Location {
32+
func (def DefinitionStruct) searchForWorkflowJobsRequires(requires []ast.Require, workflow ast.Workflow) []protocol.Location {
3333
for _, require := range requires {
3434
if utils.PosInRange(require.Range, def.Params.Position) {
3535
for _, jobRef := range workflow.JobRefs {
36-
if jobRef.JobName == require.Text {
36+
if jobRef.JobName == require.Name {
3737
return []protocol.Location{
3838
{
3939
URI: def.Params.TextDocument.URI,

pkg/services/diagnostics_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ func TestFindErrors(t *testing.T) {
3636
args: args{filePath: "./testdata/anchorNoErrors.yml"},
3737
want: make([]protocol.Diagnostic, 0),
3838
},
39+
{
40+
name: "No errors",
41+
args: args{filePath: "./testdata/requiresNoErrors.yml"},
42+
want: make([]protocol.Diagnostic, 0),
43+
},
3944
}
4045

4146
for _, tt := range tests {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
version: 2.1
2+
3+
jobs:
4+
build:
5+
docker:
6+
- image: node:latest
7+
steps:
8+
- checkout
9+
- run: echo "build"
10+
somejob:
11+
docker:
12+
- image: node:latest
13+
steps:
14+
- checkout
15+
- run: echo "somejob"
16+
someotherjob:
17+
docker:
18+
- image: node:latest
19+
steps:
20+
- checkout
21+
- run: echo "somejob"
22+
anotherjob:
23+
docker:
24+
- image: node:latest
25+
steps:
26+
- checkout
27+
- run: echo "anotherjob"
28+
29+
workflows:
30+
test-build:
31+
jobs:
32+
- build
33+
- somejob
34+
- someotherjob
35+
- anotherjob:
36+
requires:
37+
- build: failed
38+
- somejob:
39+
- success
40+
- canceled
41+
- someotherjob: [canceled, failed]

schema.json

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1525,7 +1525,33 @@
15251525
"requires": {
15261526
"type": "array",
15271527
"items": {
1528-
"type": "string"
1528+
"oneOf": [
1529+
{
1530+
"type": "string"
1531+
},
1532+
{
1533+
"type": "object",
1534+
"minProperties": 1,
1535+
"maxProperties": 1,
1536+
"patternProperties": {
1537+
"^[A-Za-z][A-Za-z\\s\\d_-]*$": {
1538+
"oneOf": [
1539+
{
1540+
"type": "string",
1541+
"enum": ["success", "failed", "canceled"]
1542+
},
1543+
{
1544+
"type": "array",
1545+
"minLength": 1,
1546+
"items": {
1547+
"type": "string",
1548+
"enum": ["success", "failed", "canceled"]
1549+
}
1550+
}
1551+
]
1552+
}}
1553+
}
1554+
]
15291555
}
15301556
},
15311557
"filters": {

0 commit comments

Comments
 (0)