replace parser with actions/workflow-parser
This commit is contained in:
@@ -2,81 +2,46 @@ package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/nektos/act/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/actions/workflow-parser/model"
|
||||
"github.com/howeyc/gopass"
|
||||
)
|
||||
|
||||
// imageURL is the directory where a `Dockerfile` should exist
|
||||
func parseImageLocal(workingDir string, contextDir string) (contextDirOut string, tag string, ok bool) {
|
||||
if !strings.HasPrefix(contextDir, "./") {
|
||||
return "", "", false
|
||||
}
|
||||
contextDir = filepath.Join(workingDir, contextDir)
|
||||
if _, err := os.Stat(filepath.Join(contextDir, "Dockerfile")); os.IsNotExist(err) {
|
||||
log.Debugf("Ignoring missing Dockerfile '%s/Dockerfile'", contextDir)
|
||||
return "", "", false
|
||||
}
|
||||
var secretCache map[string]string
|
||||
|
||||
sha, _, err := common.FindGitRevision(contextDir)
|
||||
if err != nil {
|
||||
log.Warnf("Unable to determine git revision: %v", err)
|
||||
sha = "latest"
|
||||
}
|
||||
return contextDir, fmt.Sprintf("%s:%s", filepath.Base(contextDir), sha), true
|
||||
type actionEnvironmentApplier struct {
|
||||
*model.Action
|
||||
}
|
||||
|
||||
// imageURL is the URL for a docker repo
|
||||
func parseImageReference(image string) (ref string, ok bool) {
|
||||
imageURL, err := url.Parse(image)
|
||||
if err != nil {
|
||||
log.Debugf("Unable to parse image as url: %v", err)
|
||||
return "", false
|
||||
}
|
||||
if imageURL.Scheme != "docker" {
|
||||
log.Debugf("Ignoring non-docker ref '%s'", imageURL.String())
|
||||
return "", false
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", imageURL.Host, imageURL.Path), true
|
||||
func newActionEnvironmentApplier(action *model.Action) environmentApplier {
|
||||
return &actionEnvironmentApplier{action}
|
||||
}
|
||||
|
||||
// imageURL is the directory where a `Dockerfile` should exist
|
||||
func parseImageGithub(image string) (cloneURL *url.URL, ref string, path string, ok bool) {
|
||||
re := regexp.MustCompile("^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$")
|
||||
matches := re.FindStringSubmatch(image)
|
||||
|
||||
if matches == nil {
|
||||
return nil, "", "", false
|
||||
func (action *actionEnvironmentApplier) applyEnvironment(env map[string]string) {
|
||||
for envKey, envValue := range action.Env {
|
||||
env[envKey] = envValue
|
||||
}
|
||||
|
||||
cloneURL, err := url.Parse(fmt.Sprintf("https://github.com/%s/%s", matches[1], matches[2]))
|
||||
if err != nil {
|
||||
log.Debugf("Unable to parse as URL: %v", err)
|
||||
return nil, "", "", false
|
||||
}
|
||||
for _, secret := range action.Secrets {
|
||||
if secretVal, ok := os.LookupEnv(secret); ok {
|
||||
env[secret] = secretVal
|
||||
} else {
|
||||
if secretCache == nil {
|
||||
secretCache = make(map[string]string)
|
||||
}
|
||||
|
||||
resp, err := http.Head(cloneURL.String())
|
||||
if resp.StatusCode >= 400 || err != nil {
|
||||
log.Debugf("Unable to HEAD URL %s status=%v err=%v", cloneURL.String(), resp.StatusCode, err)
|
||||
return nil, "", "", false
|
||||
}
|
||||
if _, ok := secretCache[secret]; !ok {
|
||||
fmt.Printf("Provide value for '%s': ", secret)
|
||||
val, err := gopass.GetPasswdMasked()
|
||||
if err != nil {
|
||||
log.Fatal("abort")
|
||||
}
|
||||
|
||||
ref = matches[6]
|
||||
if ref == "" {
|
||||
ref = "master"
|
||||
secretCache[secret] = string(val)
|
||||
}
|
||||
env[secret] = secretCache[secret]
|
||||
}
|
||||
}
|
||||
|
||||
path = matches[4]
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
|
||||
return cloneURL, ref, path, true
|
||||
}
|
||||
|
64
actions/graph.go
Normal file
64
actions/graph.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/actions/workflow-parser/model"
|
||||
)
|
||||
|
||||
// return a pipeline that is run in series. pipeline is a list of steps to run in parallel
|
||||
func newExecutionGraph(workflowConfig *model.Configuration, actionNames ...string) [][]string {
|
||||
// first, build a list of all the necessary actions to run, and their dependencies
|
||||
actionDependencies := make(map[string][]string)
|
||||
for len(actionNames) > 0 {
|
||||
newActionNames := make([]string, 0)
|
||||
for _, aName := range actionNames {
|
||||
// make sure we haven't visited this action yet
|
||||
if _, ok := actionDependencies[aName]; !ok {
|
||||
action := workflowConfig.GetAction(aName)
|
||||
if action != nil {
|
||||
actionDependencies[aName] = action.Needs
|
||||
newActionNames = append(newActionNames, action.Needs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
actionNames = newActionNames
|
||||
}
|
||||
|
||||
// next, build an execution graph
|
||||
graph := make([][]string, 0)
|
||||
for len(actionDependencies) > 0 {
|
||||
stage := make([]string, 0)
|
||||
for aName, aDeps := range actionDependencies {
|
||||
// make sure all deps are in the graph already
|
||||
if listInLists(aDeps, graph...) {
|
||||
stage = append(stage, aName)
|
||||
delete(actionDependencies, aName)
|
||||
}
|
||||
}
|
||||
if len(stage) == 0 {
|
||||
log.Fatalf("Unable to build dependency graph!")
|
||||
}
|
||||
graph = append(graph, stage)
|
||||
}
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
// return true iff all strings in srcList exist in at least one of the searchLists
|
||||
func listInLists(srcList []string, searchLists ...[]string) bool {
|
||||
for _, src := range srcList {
|
||||
found := false
|
||||
for _, searchList := range searchLists {
|
||||
for _, search := range searchList {
|
||||
if src == search {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
129
actions/model.go
129
actions/model.go
@@ -1,129 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/howeyc/gopass"
|
||||
)
|
||||
|
||||
type workflowModel struct {
|
||||
On string
|
||||
Resolves []string
|
||||
}
|
||||
|
||||
type actionModel struct {
|
||||
Needs []string
|
||||
Uses string
|
||||
Runs []string
|
||||
Args []string
|
||||
Env map[string]string
|
||||
Secrets []string
|
||||
}
|
||||
|
||||
type workflowsFile struct {
|
||||
Workflow map[string]workflowModel
|
||||
Action map[string]actionModel
|
||||
}
|
||||
|
||||
func (wFile *workflowsFile) getWorkflow(eventName string) (*workflowModel, string, error) {
|
||||
var rtn workflowModel
|
||||
for wName, w := range wFile.Workflow {
|
||||
if w.On == eventName {
|
||||
rtn = w
|
||||
return &rtn, wName, nil
|
||||
}
|
||||
}
|
||||
return nil, "", fmt.Errorf("unsupported event: %v", eventName)
|
||||
}
|
||||
|
||||
func (wFile *workflowsFile) getAction(actionName string) (*actionModel, error) {
|
||||
if a, ok := wFile.Action[actionName]; ok {
|
||||
return &a, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported action: %v", actionName)
|
||||
}
|
||||
|
||||
// return a pipeline that is run in series. pipeline is a list of steps to run in parallel
|
||||
func (wFile *workflowsFile) newExecutionGraph(actionNames ...string) [][]string {
|
||||
// first, build a list of all the necessary actions to run, and their dependencies
|
||||
actionDependencies := make(map[string][]string)
|
||||
for len(actionNames) > 0 {
|
||||
newActionNames := make([]string, 0)
|
||||
for _, aName := range actionNames {
|
||||
// make sure we haven't visited this action yet
|
||||
if _, ok := actionDependencies[aName]; !ok {
|
||||
actionDependencies[aName] = wFile.Action[aName].Needs
|
||||
newActionNames = append(newActionNames, wFile.Action[aName].Needs...)
|
||||
}
|
||||
}
|
||||
actionNames = newActionNames
|
||||
}
|
||||
|
||||
// next, build an execution graph
|
||||
graph := make([][]string, 0)
|
||||
for len(actionDependencies) > 0 {
|
||||
stage := make([]string, 0)
|
||||
for aName, aDeps := range actionDependencies {
|
||||
// make sure all deps are in the graph already
|
||||
if listInLists(aDeps, graph...) {
|
||||
stage = append(stage, aName)
|
||||
delete(actionDependencies, aName)
|
||||
}
|
||||
}
|
||||
if len(stage) == 0 {
|
||||
log.Fatalf("Unable to build dependency graph!")
|
||||
}
|
||||
graph = append(graph, stage)
|
||||
}
|
||||
|
||||
return graph
|
||||
}
|
||||
|
||||
// return true iff all strings in srcList exist in at least one of the searchLists
|
||||
func listInLists(srcList []string, searchLists ...[]string) bool {
|
||||
for _, src := range srcList {
|
||||
found := false
|
||||
for _, searchList := range searchLists {
|
||||
for _, search := range searchList {
|
||||
if src == search {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var secretCache map[string]string
|
||||
|
||||
func (action *actionModel) applyEnvironment(env map[string]string) {
|
||||
for envKey, envValue := range action.Env {
|
||||
env[envKey] = envValue
|
||||
}
|
||||
|
||||
for _, secret := range action.Secrets {
|
||||
if secretVal, ok := os.LookupEnv(secret); ok {
|
||||
env[secret] = secretVal
|
||||
} else {
|
||||
if secretCache == nil {
|
||||
secretCache = make(map[string]string)
|
||||
}
|
||||
|
||||
if _, ok := secretCache[secret]; !ok {
|
||||
fmt.Printf("Provide value for '%s': ", secret)
|
||||
val, err := gopass.GetPasswdMasked()
|
||||
if err != nil {
|
||||
log.Fatal("abort")
|
||||
}
|
||||
|
||||
secretCache[secret] = string(val)
|
||||
}
|
||||
env[secret] = secretCache[secret]
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,140 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"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, strings.Replace(part, "\\", "\\\\", -1), 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
|
||||
}
|
@@ -1,161 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseWorkflowsFile(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
conf := `
|
||||
workflow "build-and-deploy" {
|
||||
on = "push"
|
||||
resolves = ["deploy"]
|
||||
}
|
||||
|
||||
action "build" {
|
||||
uses = "./action1"
|
||||
args = "echo 'build'"
|
||||
}
|
||||
|
||||
action "test" {
|
||||
uses = "docker://ubuntu:18.04"
|
||||
runs = "echo 'test'"
|
||||
needs = ["build"]
|
||||
}
|
||||
|
||||
action "deploy" {
|
||||
uses = "./action2"
|
||||
args = ["echo","deploy"]
|
||||
needs = ["test"]
|
||||
}
|
||||
|
||||
action "docker-login" {
|
||||
uses = "docker://docker"
|
||||
runs = ["sh", "-c", "echo $DOCKER_AUTH | docker login --username $REGISTRY_USER --password-stdin"]
|
||||
secrets = ["DOCKER_AUTH"]
|
||||
env = {
|
||||
REGISTRY_USER = "username"
|
||||
}
|
||||
}
|
||||
|
||||
action "unit-tests" {
|
||||
uses = "./scripts/github_actions"
|
||||
runs = "yarn test:ci-unittest || echo \"Unit tests failed, but running danger to present the results!\" 2>&1"
|
||||
}
|
||||
|
||||
action "regex-in-args" {
|
||||
uses = "actions/bin/filter@master"
|
||||
args = "tag v?[0-9]+\\.[0-9]+\\.[0-9]+"
|
||||
}
|
||||
|
||||
action "regex-in-args-array" {
|
||||
uses = "actions/bin/filter@master"
|
||||
args = ["tag","v?[0-9]+\\.[0-9]+\\.[0-9]+"]
|
||||
}
|
||||
`
|
||||
|
||||
workflows, err := parseWorkflowsFile(strings.NewReader(conf))
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(workflows.Workflow))
|
||||
|
||||
w, wName, _ := workflows.getWorkflow("push")
|
||||
assert.Equal(t, "build-and-deploy", wName)
|
||||
assert.ElementsMatch(t, []string{"deploy"}, w.Resolves)
|
||||
|
||||
actions := []struct {
|
||||
name string
|
||||
uses string
|
||||
needs []string
|
||||
runs []string
|
||||
args []string
|
||||
secrets []string
|
||||
}{
|
||||
{"build",
|
||||
"./action1",
|
||||
nil,
|
||||
nil,
|
||||
[]string{"echo", "build"},
|
||||
nil,
|
||||
},
|
||||
{"test",
|
||||
"docker://ubuntu:18.04",
|
||||
[]string{"build"},
|
||||
[]string{"echo", "test"},
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{"deploy",
|
||||
"./action2",
|
||||
[]string{"test"},
|
||||
nil,
|
||||
[]string{"echo", "deploy"},
|
||||
nil,
|
||||
},
|
||||
{"docker-login",
|
||||
"docker://docker",
|
||||
nil,
|
||||
[]string{"sh", "-c", "echo $DOCKER_AUTH | docker login --username $REGISTRY_USER --password-stdin"},
|
||||
nil,
|
||||
[]string{"DOCKER_AUTH"},
|
||||
},
|
||||
{"unit-tests",
|
||||
"./scripts/github_actions",
|
||||
nil,
|
||||
[]string{"yarn", "test:ci-unittest", "||", "echo", "Unit tests failed, but running danger to present the results!", "2>&1"},
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
{"regex-in-args",
|
||||
"actions/bin/filter@master",
|
||||
nil,
|
||||
nil,
|
||||
[]string{"tag", `v?[0-9]+\.[0-9]+\.[0-9]+`},
|
||||
nil,
|
||||
},
|
||||
{"regex-in-args-array",
|
||||
"actions/bin/filter@master",
|
||||
nil,
|
||||
nil,
|
||||
[]string{"tag", `v?[0-9]+\.[0-9]+\.[0-9]+`},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, exp := range actions {
|
||||
act, _ := workflows.getAction(exp.name)
|
||||
assert.Equal(t, exp.uses, act.Uses, "[%s] Uses", exp.name)
|
||||
if exp.needs == nil {
|
||||
assert.Nil(t, act.Needs, "[%s] Needs", exp.name)
|
||||
} else {
|
||||
assert.ElementsMatch(t, exp.needs, act.Needs, "[%s] Needs", exp.name)
|
||||
}
|
||||
if exp.runs == nil {
|
||||
assert.Nil(t, act.Runs, "[%s] Runs", exp.name)
|
||||
} else {
|
||||
assert.ElementsMatch(t, exp.runs, act.Runs, "[%s] Runs", exp.name)
|
||||
}
|
||||
if exp.args == nil {
|
||||
assert.Nil(t, act.Args, "[%s] Args", exp.name)
|
||||
} else {
|
||||
assert.ElementsMatch(t, exp.args, act.Args, "[%s] Args", exp.name)
|
||||
}
|
||||
/*
|
||||
if exp.env == nil {
|
||||
assert.Nil(t, act.Env, "[%s] Env", exp.name)
|
||||
} else {
|
||||
assert.ElementsMatch(t, exp.env, act.Env, "[%s] Env", exp.name)
|
||||
}
|
||||
*/
|
||||
if exp.secrets == nil {
|
||||
assert.Nil(t, act.Secrets, "[%s] Secrets", exp.name)
|
||||
} else {
|
||||
assert.ElementsMatch(t, exp.secrets, act.Secrets, "[%s] Secrets", exp.name)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,20 +1,23 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/actions/workflow-parser/model"
|
||||
"github.com/actions/workflow-parser/parser"
|
||||
"github.com/nektos/act/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type runnerImpl struct {
|
||||
config *RunnerConfig
|
||||
workflows *workflowsFile
|
||||
tempDir string
|
||||
eventJSON string
|
||||
config *RunnerConfig
|
||||
workflowConfig *model.Configuration
|
||||
tempDir string
|
||||
eventJSON string
|
||||
}
|
||||
|
||||
// NewRunner Creates a new Runner
|
||||
@@ -56,7 +59,13 @@ func (runner *runnerImpl) setupWorkflows() error {
|
||||
|
||||
defer workflowReader.Close()
|
||||
|
||||
runner.workflows, err = parseWorkflowsFile(workflowReader)
|
||||
runner.workflowConfig, err = parser.Parse(workflowReader)
|
||||
if err != nil {
|
||||
parserError := err.(*parser.ParserError)
|
||||
for _, e := range parserError.Errors {
|
||||
fmt.Fprintln(os.Stderr, e)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -88,7 +97,7 @@ func (runner *runnerImpl) resolvePath(path string) string {
|
||||
func (runner *runnerImpl) ListEvents() []string {
|
||||
log.Debugf("Listing all events")
|
||||
events := make([]string, 0)
|
||||
for _, w := range runner.workflows.Workflow {
|
||||
for _, w := range runner.workflowConfig.Workflows {
|
||||
events = append(events, w.On)
|
||||
}
|
||||
|
||||
@@ -103,17 +112,14 @@ func (runner *runnerImpl) ListEvents() []string {
|
||||
// GraphEvent builds an execution path
|
||||
func (runner *runnerImpl) GraphEvent(eventName string) ([][]string, error) {
|
||||
log.Debugf("Listing actions for event '%s'", eventName)
|
||||
workflow, _, err := runner.workflows.getWorkflow(eventName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return runner.workflows.newExecutionGraph(workflow.Resolves...), nil
|
||||
resolves := runner.resolveEvent(runner.config.EventName)
|
||||
return newExecutionGraph(runner.workflowConfig, resolves...), nil
|
||||
}
|
||||
|
||||
// RunAction runs a set of actions in parallel, and their dependencies
|
||||
func (runner *runnerImpl) RunActions(actionNames ...string) error {
|
||||
log.Debugf("Running actions %+q", actionNames)
|
||||
graph := runner.workflows.newExecutionGraph(actionNames...)
|
||||
graph := newExecutionGraph(runner.workflowConfig, actionNames...)
|
||||
|
||||
pipeline := make([]common.Executor, 0)
|
||||
for _, actions := range graph {
|
||||
@@ -131,15 +137,32 @@ func (runner *runnerImpl) RunActions(actionNames ...string) error {
|
||||
// RunEvent runs the actions for a single event
|
||||
func (runner *runnerImpl) RunEvent() error {
|
||||
log.Debugf("Running event '%s'", runner.config.EventName)
|
||||
workflow, _, err := runner.workflows.getWorkflow(runner.config.EventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Running actions %s -> %s", runner.config.EventName, workflow.Resolves)
|
||||
return runner.RunActions(workflow.Resolves...)
|
||||
resolves := runner.resolveEvent(runner.config.EventName)
|
||||
log.Debugf("Running actions %s -> %s", runner.config.EventName, resolves)
|
||||
return runner.RunActions(resolves...)
|
||||
}
|
||||
|
||||
func (runner *runnerImpl) Close() error {
|
||||
return os.RemoveAll(runner.tempDir)
|
||||
}
|
||||
|
||||
// get list of resolves for an event
|
||||
func (runner *runnerImpl) resolveEvent(eventName string) []string {
|
||||
workflows := runner.workflowConfig.GetWorkflows(runner.config.EventName)
|
||||
resolves := make([]string, 0)
|
||||
for _, workflow := range workflows {
|
||||
for _, resolve := range workflow.Resolves {
|
||||
found := false
|
||||
for _, r := range resolves {
|
||||
if r == resolve {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
resolves = append(resolves, resolve)
|
||||
}
|
||||
}
|
||||
}
|
||||
return resolves
|
||||
}
|
||||
|
@@ -9,19 +9,20 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/actions/workflow-parser/model"
|
||||
"github.com/nektos/act/common"
|
||||
"github.com/nektos/act/container"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
||||
action, err := runner.workflows.getAction(actionName)
|
||||
if err != nil {
|
||||
return common.NewErrorExecutor(err)
|
||||
action := runner.workflowConfig.GetAction(actionName)
|
||||
if action == nil {
|
||||
return common.NewErrorExecutor(fmt.Errorf("Unable to find action named '%s'", actionName))
|
||||
}
|
||||
|
||||
env := make(map[string]string)
|
||||
for _, applier := range []environmentApplier{action, runner} {
|
||||
for _, applier := range []environmentApplier{newActionEnvironmentApplier(action), runner} {
|
||||
applier.applyEnvironment(env)
|
||||
}
|
||||
env["GITHUB_ACTION"] = actionName
|
||||
@@ -37,39 +38,51 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
||||
|
||||
var image string
|
||||
executors := make([]common.Executor, 0)
|
||||
if imageRef, ok := parseImageReference(action.Uses); ok {
|
||||
switch uses := action.Uses.(type) {
|
||||
|
||||
case *model.UsesDockerImage:
|
||||
image = uses.Image
|
||||
executors = append(executors, container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{
|
||||
DockerExecutorInput: in,
|
||||
Image: imageRef,
|
||||
Image: image,
|
||||
}))
|
||||
image = imageRef
|
||||
} else if contextDir, imageTag, ok := parseImageLocal(runner.config.WorkingDir, action.Uses); ok {
|
||||
|
||||
case *model.UsesPath:
|
||||
contextDir := filepath.Join(runner.config.WorkingDir, uses.String())
|
||||
sha, _, err := common.FindGitRevision(contextDir)
|
||||
if err != nil {
|
||||
log.Warnf("Unable to determine git revision: %v", err)
|
||||
sha = "latest"
|
||||
}
|
||||
image = fmt.Sprintf("%s:%s", filepath.Base(contextDir), sha)
|
||||
|
||||
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||
DockerExecutorInput: in,
|
||||
ContextDir: contextDir,
|
||||
ImageTag: imageTag,
|
||||
ImageTag: image,
|
||||
}))
|
||||
image = imageTag
|
||||
} else if cloneURL, ref, path, ok := parseImageGithub(action.Uses); ok {
|
||||
cloneDir := filepath.Join(os.TempDir(), "act", action.Uses)
|
||||
|
||||
case *model.UsesRepository:
|
||||
image = fmt.Sprintf("%s:%s", filepath.Base(uses.Repository), uses.Ref)
|
||||
cloneURL := fmt.Sprintf("https://github.com/%s", uses.Repository)
|
||||
|
||||
cloneDir := filepath.Join(os.TempDir(), "act", action.Uses.String())
|
||||
executors = append(executors, common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
|
||||
URL: cloneURL,
|
||||
Ref: ref,
|
||||
Ref: uses.Ref,
|
||||
Dir: cloneDir,
|
||||
Logger: logger,
|
||||
Dryrun: runner.config.Dryrun,
|
||||
}))
|
||||
|
||||
contextDir := filepath.Join(cloneDir, path)
|
||||
imageTag := fmt.Sprintf("%s:%s", filepath.Base(cloneURL.Path), ref)
|
||||
|
||||
contextDir := filepath.Join(cloneDir, uses.Path)
|
||||
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||
DockerExecutorInput: in,
|
||||
ContextDir: contextDir,
|
||||
ImageTag: imageTag,
|
||||
ImageTag: image,
|
||||
}))
|
||||
image = imageTag
|
||||
} else {
|
||||
|
||||
default:
|
||||
return common.NewErrorExecutor(fmt.Errorf("unable to determine executor type for image '%s'", action.Uses))
|
||||
}
|
||||
|
||||
@@ -84,8 +97,8 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
||||
}
|
||||
executors = append(executors, container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
|
||||
DockerExecutorInput: in,
|
||||
Cmd: action.Args,
|
||||
Entrypoint: action.Runs,
|
||||
Cmd: action.Args.Parsed,
|
||||
Entrypoint: action.Runs.Parsed,
|
||||
Image: image,
|
||||
WorkingDir: "/github/workspace",
|
||||
Env: envList,
|
||||
@@ -105,7 +118,11 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
|
||||
func (runner *runnerImpl) applyEnvironment(env map[string]string) {
|
||||
repoPath := runner.config.WorkingDir
|
||||
|
||||
_, workflowName, _ := runner.workflows.getWorkflow(runner.config.EventName)
|
||||
workflows := runner.workflowConfig.GetWorkflows(runner.config.EventName)
|
||||
if len(workflows) == 0 {
|
||||
return
|
||||
}
|
||||
workflowName := workflows[0].Identifier
|
||||
|
||||
env["HOME"] = "/github/home"
|
||||
env["GITHUB_ACTOR"] = "nektos/act"
|
||||
|
@@ -1,91 +0,0 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/common"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseImageReference(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
tables := []struct {
|
||||
refIn string
|
||||
refOut string
|
||||
ok bool
|
||||
}{
|
||||
{"docker://myhost.com/foo/bar", "myhost.com/foo/bar", true},
|
||||
{"docker://ubuntu", "ubuntu", true},
|
||||
{"docker://ubuntu:18.04", "ubuntu:18.04", true},
|
||||
{"docker://cibuilds/hugo:0.53", "cibuilds/hugo:0.53", true},
|
||||
{"http://google.com:8080", "", false},
|
||||
{"./foo", "", false},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
refOut, ok := parseImageReference(table.refIn)
|
||||
assert.Equal(t, table.refOut, refOut)
|
||||
assert.Equal(t, table.ok, ok)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestParseImageLocal(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
tables := []struct {
|
||||
pathIn string
|
||||
contextDir string
|
||||
refTag string
|
||||
ok bool
|
||||
}{
|
||||
{"docker://myhost.com/foo/bar", "", "", false},
|
||||
{"http://google.com:8080", "", "", false},
|
||||
{"example/action1", "", "", false},
|
||||
{"./example/action1", "/example/action1", "action1:", true},
|
||||
}
|
||||
|
||||
revision, _, err := common.FindGitRevision(".")
|
||||
assert.Nil(t, err)
|
||||
basedir, err := filepath.Abs("..")
|
||||
assert.Nil(t, err)
|
||||
for _, table := range tables {
|
||||
contextDir, refTag, ok := parseImageLocal(basedir, table.pathIn)
|
||||
assert.Equal(t, table.ok, ok, "ok match for %s", table.pathIn)
|
||||
if ok {
|
||||
assert.Equal(t, fmt.Sprintf("%s%s", basedir, table.contextDir), contextDir, "context dir doesn't match for %s", table.pathIn)
|
||||
assert.Equal(t, fmt.Sprintf("%s%s", table.refTag, revision), refTag)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
func TestParseImageGithub(t *testing.T) {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
tables := []struct {
|
||||
image string
|
||||
cloneURL string
|
||||
ref string
|
||||
path string
|
||||
ok bool
|
||||
}{
|
||||
{"nektos/act", "https://github.com/nektos/act", "master", ".", true},
|
||||
{"nektos/act/foo", "https://github.com/nektos/act", "master", "foo", true},
|
||||
{"nektos/act@xxxxx", "https://github.com/nektos/act", "xxxxx", ".", true},
|
||||
{"nektos/act/bar/baz@zzzzz", "https://github.com/nektos/act", "zzzzz", "bar/baz", true},
|
||||
{"assimovt/actions-github-deploy/github-deploy@deployment-status-metadata", "https://github.com/assimovt/actions-github-deploy", "deployment-status-metadata", "github-deploy", true},
|
||||
{"nektos/zzzzundefinedzzzz", "", "", "", false},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
cloneURL, ref, path, ok := parseImageGithub(table.image)
|
||||
assert.Equal(t, table.ok, ok, "ok match for %s", table.image)
|
||||
if ok {
|
||||
assert.Equal(t, table.cloneURL, cloneURL.String())
|
||||
assert.Equal(t, table.ref, ref)
|
||||
assert.Equal(t, table.path, path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user