-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor actions to improve testability
- Loading branch information
Showing
10 changed files
with
583 additions
and
845 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package actions | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
|
||
"github.com/nektos/act/common" | ||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// imageURL is the directory where a `Dockerfile` should exist | ||
func parseImageLocal(workingDir string, contextDir string) (contextDirOut string, tag string, ok bool) { | ||
if !filepath.IsAbs(contextDir) { | ||
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 | ||
} | ||
|
||
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 | ||
} | ||
|
||
// 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 | ||
} | ||
|
||
// 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 | ||
} | ||
|
||
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 | ||
} | ||
|
||
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 | ||
} | ||
|
||
ref = matches[6] | ||
if ref == "" { | ||
ref = "master" | ||
} | ||
|
||
path = matches[4] | ||
if path == "" { | ||
path = "." | ||
} | ||
|
||
return cloneURL, ref, path, true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package actions | ||
|
||
import ( | ||
"context" | ||
"io" | ||
) | ||
|
||
// Runner provides capabilities to run GitHub actions | ||
type Runner interface { | ||
EventGrapher | ||
EventLister | ||
EventRunner | ||
ActionRunner | ||
io.Closer | ||
} | ||
|
||
// EventGrapher to list the actions | ||
type EventGrapher interface { | ||
GraphEvent(eventName string) ([][]string, error) | ||
} | ||
|
||
// EventLister to list the events | ||
type EventLister interface { | ||
ListEvents() []string | ||
} | ||
|
||
// EventRunner to run the actions for a given event | ||
type EventRunner interface { | ||
RunEvent() error | ||
} | ||
|
||
// ActionRunner to run a specific actions | ||
type ActionRunner interface { | ||
RunActions(actionNames ...string) error | ||
} | ||
|
||
// RunnerConfig contains the config for a new runner | ||
type RunnerConfig struct { | ||
Ctx context.Context // context to use for the run | ||
Dryrun bool // don't start any of the containers | ||
WorkingDir string // base directory to use | ||
WorkflowPath string // path to load main.workflow file, relative to WorkingDir | ||
EventName string // name of event to run | ||
EventPath string // path to JSON file to use for event.json in containers, relative to WorkingDir | ||
} | ||
|
||
type environmentApplier interface { | ||
applyEnvironment(map[string]string) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
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] | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.