package actions import ( "bytes" "errors" "fmt" "io" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" "github.com/hashicorp/hcl/hcl/token" log "github.com/sirupsen/logrus" ) func parseWorkflowsFile(workflowReader io.Reader) (*workflowsFile, error) { // TODO: add validation logic // - check for circular dependencies // - check for valid local path refs // - check for valid dependencies buf := new(bytes.Buffer) _, err := buf.ReadFrom(workflowReader) if err != nil { log.Error(err) } workflows := new(workflowsFile) astFile, err := hcl.ParseBytes(buf.Bytes()) if err != nil { return nil, err } rootNode := ast.Walk(astFile.Node, cleanWorkflowsAST) err = hcl.DecodeObject(workflows, rootNode) if err != nil { return nil, err } return workflows, nil } func cleanWorkflowsAST(node ast.Node) (ast.Node, bool) { if objectItem, ok := node.(*ast.ObjectItem); ok { key := objectItem.Keys[0].Token.Value() // handle condition where value is a string but should be a list switch key { case "args", "runs": if literalType, ok := objectItem.Val.(*ast.LiteralType); ok { listType := new(ast.ListType) parts, err := parseCommand(literalType.Token.Value().(string)) if err != nil { return nil, false } quote := literalType.Token.Text[0] for _, part := range parts { part = fmt.Sprintf("%c%s%c", quote, part, quote) listType.Add(&ast.LiteralType{ Token: token.Token{ Type: token.STRING, Text: part, }, }) } objectItem.Val = listType } case "resolves", "needs": if literalType, ok := objectItem.Val.(*ast.LiteralType); ok { listType := new(ast.ListType) listType.Add(literalType) objectItem.Val = listType } } } return node, true } // reused from: https://github.com/laurent22/massren/blob/ae4c57da1e09a95d9383f7eb645a9f69790dec6c/main.go#L172 // nolint: gocyclo func parseCommand(cmd string) ([]string, error) { var args []string state := "start" current := "" quote := "\"" for i := 0; i < len(cmd); i++ { c := cmd[i] if state == "quotes" { if string(c) != quote { current += string(c) } else { args = append(args, current) current = "" state = "start" } continue } if c == '"' || c == '\'' { state = "quotes" quote = string(c) continue } if state == "arg" { if c == ' ' || c == '\t' { args = append(args, current) current = "" state = "start" } else { current += string(c) } continue } if c != ' ' && c != '\t' { state = "arg" current += string(c) } } if state == "quotes" { return []string{}, fmt.Errorf("unclosed quote in command line: %s", cmd) } if current != "" { args = append(args, current) } if len(args) == 0 { return []string{}, errors.New("empty command line") } log.Debugf("Parsed literal %+q to list %+q", cmd, args) return args, nil }