Skip to content

Commit

Permalink
feat: extend the schema of the requires stanza (#315)
Browse files Browse the repository at this point in the history
  • Loading branch information
briceicle authored Nov 27, 2024
1 parent d8bf965 commit d64302f
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 16 deletions.
7 changes: 4 additions & 3 deletions pkg/ast/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type JobRef struct {
// not the job that will be executed
StepName string
StepNameRange protocol.Range
Requires []TextAndRange
Requires []Require
Context []TextAndRange
Type string
TypeRange protocol.Range
Expand All @@ -44,8 +44,9 @@ type JobRef struct {
}

type Require struct {
Name string
Range protocol.Range
Name string
Status []string
Range protocol.Range
}

type WorkflowTrigger struct {
Expand Down
4 changes: 2 additions & 2 deletions pkg/parser/validate/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ func (val Validate) validateSingleWorkflow(workflow ast.Workflow) error {
val.validateWorkflowParameters(jobRef, jobRef.JobName, jobRef.JobRefRange)
}
for _, require := range jobRef.Requires {
if !val.doesJobRefExist(workflow, require.Text) && !utils.CheckIfMatrixParamIsPartiallyReferenced(require.Text) {
if !val.doesJobRefExist(workflow, require.Name) && !utils.CheckIfMatrixParamIsPartiallyReferenced(require.Name) {
val.addDiagnostic(utils.CreateErrorDiagnosticFromRange(
require.Range,
fmt.Sprintf("Cannot find declaration for job reference %s", require.Text)))
fmt.Sprintf("Cannot find declaration for job reference %s", require.Name)))
}
}

Expand Down
70 changes: 64 additions & 6 deletions pkg/parser/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (doc *YamlDocument) buildJobsDAG(jobRefs []ast.JobRef) map[string][]string
res := make(map[string][]string)
for _, jobRef := range jobRefs {
for _, requirement := range jobRef.Requires {
res[requirement.Text] = append(res[requirement.Text], jobRef.StepName)
res[requirement.Name] = append(res[requirement.Name], jobRef.StepName)
}
}
return res
Expand Down Expand Up @@ -246,12 +246,70 @@ func (doc *YamlDocument) parseContext(node *sitter.Node) []ast.TextAndRange {
return doc.getNodeTextArrayWithRange(node)
}

func (doc *YamlDocument) parseSingleJobRequires(node *sitter.Node) []ast.TextAndRange {
array := doc.getNodeTextArrayWithRange(node)
res := []ast.TextAndRange{}
for _, require := range array {
res = append(res, ast.TextAndRange{Text: require.Text, Range: require.Range})
func (doc *YamlDocument) parseSingleJobRequires(requiresNode *sitter.Node) []ast.Require {
blockSequenceNode := GetChildSequence(requiresNode)
res := make([]ast.Require, 0, requiresNode.ChildCount())

if blockSequenceNode == nil {
return res
}

iterateOnBlockSequence(blockSequenceNode, func(requiresItemNode *sitter.Node) {
getRequire := func(node *sitter.Node) ast.Require {
defaultStatus := []string{"success"}
if alias := GetChildOfType(node, "alias"); alias != nil {
anchor, ok := doc.YamlAnchors[strings.TrimLeft(doc.GetNodeText(alias), "*")]
if !ok {
return ast.Require{Name: ""}
}
anchorValueNode := GetFirstChild(anchor.ValueNode)
text := doc.GetNodeText(anchorValueNode)
return ast.Require{Name: text, Status: defaultStatus, Range: doc.NodeToRange(anchorValueNode)}
} else {
return ast.Require{Name: doc.GetNodeText(node), Status: defaultStatus, Range: doc.NodeToRange(node)}
}
}

// If blockSequenceNode is a flow_sequence, then requiresItemNode is directly a flow_node
if requiresItemNode.Type() == "flow_node" {
res = append(res, getRequire(requiresItemNode))
} else {
// But if blockSequenceNode is a block_sequence, then requiresItemNode is a block_sequence_item
// The first child of requiresItemNode is the hyphen node, the second child is what we need
element := requiresItemNode.Child(1)
// If the second child is a flow_node, then it is a simple require
if element != nil && element.Type() == "flow_node" {
res = append(res, getRequire(element))
} else {
// Otherwise the second child is a block_mapping, then it is a require with status
blockMappingNode := GetChildOfType(element, "block_mapping")
blockMappingPair := GetChildOfType(blockMappingNode, "block_mapping_pair")
key, value := doc.GetKeyValueNodes(blockMappingPair)

if key == nil || value == nil {
return
}
if GetFirstChild(value).Type() == "plain_scalar" {
status := make([]string, 1)
status[0] = doc.GetNodeText(value)
res = append(res, ast.Require{Name: doc.GetNodeText(key), Status: status, Range: doc.NodeToRange(key)})
} else {
statusesNode := GetFirstChild(value)
status := make([]string, 0, statusesNode.ChildCount())
iterateOnBlockSequence(statusesNode, func(statusItemNode *sitter.Node) {
if statusItemNode.Type() == "flow_node" {
status = append(status, doc.GetNodeText(statusItemNode))
}
if statusItemNode.Type() == "block_sequence_item" {
status = append(status, doc.GetNodeText(statusItemNode.Child(1)))
}
})
res = append(res, ast.Require{Name: doc.GetNodeText(key), Status: status, Range: doc.NodeToRange(key)})
}
}
}
})

return res
}

Expand Down
115 changes: 113 additions & 2 deletions pkg/parser/workflows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) {
const jobRef4 = `
- test:
name: say-my-name`
const jobRef5 = `
- test:
requires:
- setup: failed`
const jobRef6 = `
- test:
requires:
- setup: [success, canceled]`

type fields struct {
Content []byte
Expand Down Expand Up @@ -128,9 +136,10 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) {
Character: 6,
},
},
Requires: []ast.TextAndRange{
Requires: []ast.Require{
{
Text: "setup",
Name: "setup",
Status: []string{"success"},
Range: protocol.Range{
Start: protocol.Position{Line: 3, Character: 10},
End: protocol.Position{Line: 3, Character: 15},
Expand Down Expand Up @@ -304,6 +313,108 @@ func TestYamlDocument_parseSingleJobReference(t *testing.T) {
MatrixParams: make(map[string][]ast.ParameterValue),
},
},
{
name: "Job reference with requires and single status",
fields: fields{Content: []byte(jobRef5)},
args: args{jobRefNode: getFirstChildOfType(GetRootNode([]byte(jobRef5)), "block_sequence_item")},
want: ast.JobRef{
JobName: "test",
JobRefRange: protocol.Range{
Start: protocol.Position{
Line: 1,
Character: 0,
},
End: protocol.Position{
Line: 3,
Character: 23,
},
},
JobNameRange: protocol.Range{
Start: protocol.Position{
Line: 1,
Character: 2,
},
End: protocol.Position{
Line: 1,
Character: 6,
},
},
StepName: "test",
StepNameRange: protocol.Range{
Start: protocol.Position{
Line: 1,
Character: 2,
},
End: protocol.Position{
Line: 1,
Character: 6,
},
},
Requires: []ast.Require{
{
Name: "setup",
Status: []string{"failed"},
Range: protocol.Range{
Start: protocol.Position{Line: 3, Character: 10},
End: protocol.Position{Line: 3, Character: 15},
},
},
},
MatrixParams: make(map[string][]ast.ParameterValue),
Parameters: make(map[string]ast.ParameterValue),
},
},
{
name: "Job reference with requires and multiple statuses",
fields: fields{Content: []byte(jobRef6)},
args: args{jobRefNode: getFirstChildOfType(GetRootNode([]byte(jobRef6)), "block_sequence_item")},
want: ast.JobRef{
JobName: "test",
JobRefRange: protocol.Range{
Start: protocol.Position{
Line: 1,
Character: 0,
},
End: protocol.Position{
Line: 3,
Character: 36,
},
},
JobNameRange: protocol.Range{
Start: protocol.Position{
Line: 1,
Character: 2,
},
End: protocol.Position{
Line: 1,
Character: 6,
},
},
StepName: "test",
StepNameRange: protocol.Range{
Start: protocol.Position{
Line: 1,
Character: 2,
},
End: protocol.Position{
Line: 1,
Character: 6,
},
},
Requires: []ast.Require{
{
Name: "setup",
Status: []string{"success", "canceled"},
Range: protocol.Range{
Start: protocol.Position{Line: 3, Character: 10},
End: protocol.Position{Line: 3, Character: 15},
},
},
},
MatrixParams: make(map[string][]ast.ParameterValue),
Parameters: make(map[string]ast.ParameterValue),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/services/definition/workflows.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ func (def DefinitionStruct) searchForWorkflows() []protocol.Location {
return []protocol.Location{}
}

func (def DefinitionStruct) searchForWorkflowJobsRequires(requires []ast.TextAndRange, workflow ast.Workflow) []protocol.Location {
func (def DefinitionStruct) searchForWorkflowJobsRequires(requires []ast.Require, workflow ast.Workflow) []protocol.Location {
for _, require := range requires {
if utils.PosInRange(require.Range, def.Params.Position) {
for _, jobRef := range workflow.JobRefs {
if jobRef.JobName == require.Text {
if jobRef.JobName == require.Name {
return []protocol.Location{
{
URI: def.Params.TextDocument.URI,
Expand Down
5 changes: 5 additions & 0 deletions pkg/services/diagnostics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func TestFindErrors(t *testing.T) {
args: args{filePath: "./testdata/anchorNoErrors.yml"},
want: make([]protocol.Diagnostic, 0),
},
{
name: "No errors",
args: args{filePath: "./testdata/requiresNoErrors.yml"},
want: make([]protocol.Diagnostic, 0),
},
}

for _, tt := range tests {
Expand Down
41 changes: 41 additions & 0 deletions pkg/services/testdata/requiresNoErrors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
version: 2.1

jobs:
build:
docker:
- image: node:latest
steps:
- checkout
- run: echo "build"
somejob:
docker:
- image: node:latest
steps:
- checkout
- run: echo "somejob"
someotherjob:
docker:
- image: node:latest
steps:
- checkout
- run: echo "somejob"
anotherjob:
docker:
- image: node:latest
steps:
- checkout
- run: echo "anotherjob"

workflows:
test-build:
jobs:
- build
- somejob
- someotherjob
- anotherjob:
requires:
- build: failed
- somejob:
- success
- canceled
- someotherjob: [canceled, failed]
28 changes: 27 additions & 1 deletion schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1525,7 +1525,33 @@
"requires": {
"type": "array",
"items": {
"type": "string"
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"patternProperties": {
"^[A-Za-z][A-Za-z\\s\\d_-]*$": {
"oneOf": [
{
"type": "string",
"enum": ["success", "failed", "canceled"]
},
{
"type": "array",
"minLength": 1,
"items": {
"type": "string",
"enum": ["success", "failed", "canceled"]
}
}
]
}}
}
]
}
},
"filters": {
Expand Down

0 comments on commit d64302f

Please sign in to comment.