forgejo-runner-act/actions/parser.go
2019-01-16 13:58:18 +01:00

173 lines
3.7 KiB
Go

package actions
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/hcl/hcl/token"
log "github.com/sirupsen/logrus"
)
// ParseWorkflows will read in the set of actions from the workflow file
func ParseWorkflows(workingDir string, workflowPath string) (Workflows, error) {
workingDir, err := filepath.Abs(workingDir)
if err != nil {
return nil, err
}
log.Debugf("Setting working dir to %s", workingDir)
if !filepath.IsAbs(workflowPath) {
workflowPath = filepath.Join(workingDir, workflowPath)
}
log.Debugf("Loading workflow config from %s", workflowPath)
workflowReader, err := os.Open(workflowPath)
if err != nil {
return nil, err
}
workflows, err := parseWorkflowsFile(workflowReader)
if err != nil {
return nil, err
}
workflows.WorkingDir = workingDir
workflows.WorkflowPath = workflowPath
workflows.TempDir, err = ioutil.TempDir("", "act-")
if err != nil {
return nil, err
}
// TODO: add validation logic
// - check for circular dependencies
// - check for valid local path refs
// - check for valid dependencies
return workflows, nil
}
func parseWorkflowsFile(workflowReader io.Reader) (*workflowsFile, error) {
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
}