mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-09-15 18:57:01 +00:00
refactor actions to improve testability
This commit is contained in:
parent
b23fbbcc94
commit
f26a1a3f0c
8 changed files with 601 additions and 523 deletions
80
actions/action.go
Normal file
80
actions/action.go
Normal file
|
@ -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
|
||||||
|
}
|
49
actions/api.go
Normal file
49
actions/api.go
Normal file
|
@ -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)
|
||||||
|
}
|
129
actions/model.go
Normal file
129
actions/model.go
Normal file
|
@ -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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,9 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/hashicorp/hcl"
|
"github.com/hashicorp/hcl"
|
||||||
"github.com/hashicorp/hcl/hcl/ast"
|
"github.com/hashicorp/hcl/hcl/ast"
|
||||||
|
@ -15,43 +12,12 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseWorkflows will read in the set of actions from the workflow file
|
func parseWorkflowsFile(workflowReader io.Reader) (*workflowsFile, error) {
|
||||||
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
|
// TODO: add validation logic
|
||||||
// - check for circular dependencies
|
// - check for circular dependencies
|
||||||
// - check for valid local path refs
|
// - check for valid local path refs
|
||||||
// - check for valid dependencies
|
// - check for valid dependencies
|
||||||
|
|
||||||
return workflows, nil
|
|
||||||
}
|
|
||||||
func parseWorkflowsFile(workflowReader io.Reader) (*workflowsFile, error) {
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
_, err := buf.ReadFrom(workflowReader)
|
_, err := buf.ReadFrom(workflowReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,31 +1,94 @@
|
||||||
package actions
|
package actions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"io/ioutil"
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/howeyc/gopass"
|
|
||||||
"github.com/nektos/act/common"
|
"github.com/nektos/act/common"
|
||||||
"github.com/nektos/act/container"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var secretCache map[string]string
|
type runnerImpl struct {
|
||||||
|
config *RunnerConfig
|
||||||
|
workflows *workflowsFile
|
||||||
|
tempDir string
|
||||||
|
eventJSON string
|
||||||
|
}
|
||||||
|
|
||||||
func (wFile *workflowsFile) ListEvents() []string {
|
// NewRunner Creates a new Runner
|
||||||
|
func NewRunner(runnerConfig *RunnerConfig) (Runner, error) {
|
||||||
|
runner := &runnerImpl{
|
||||||
|
config: runnerConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
init := common.NewPipelineExecutor(
|
||||||
|
runner.setupTempDir,
|
||||||
|
runner.setupWorkingDir,
|
||||||
|
runner.setupWorkflows,
|
||||||
|
runner.setupEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
return runner, init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) setupTempDir() error {
|
||||||
|
var err error
|
||||||
|
runner.tempDir, err = ioutil.TempDir("", "act-")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) setupWorkingDir() error {
|
||||||
|
var err error
|
||||||
|
runner.config.WorkingDir, err = filepath.Abs(runner.config.WorkingDir)
|
||||||
|
log.Debugf("Setting working dir to %s", runner.config.WorkingDir)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) setupWorkflows() error {
|
||||||
|
runner.config.WorkflowPath = runner.resolvePath(runner.config.WorkflowPath)
|
||||||
|
log.Debugf("Loading workflow config from %s", runner.config.WorkflowPath)
|
||||||
|
workflowReader, err := os.Open(runner.config.WorkflowPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer workflowReader.Close()
|
||||||
|
|
||||||
|
runner.workflows, err = parseWorkflowsFile(workflowReader)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) setupEvent() error {
|
||||||
|
runner.eventJSON = "{}"
|
||||||
|
if runner.config.EventPath != "" {
|
||||||
|
runner.config.EventPath = runner.resolvePath(runner.config.EventPath)
|
||||||
|
log.Debugf("Reading event.json from %s", runner.config.EventPath)
|
||||||
|
eventJSONBytes, err := ioutil.ReadFile(runner.config.EventPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
runner.eventJSON = string(eventJSONBytes)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) resolvePath(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
path = filepath.Join(runner.config.WorkingDir, path)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListEvents gets all the events in the workflows file
|
||||||
|
func (runner *runnerImpl) ListEvents() []string {
|
||||||
log.Debugf("Listing all events")
|
log.Debugf("Listing all events")
|
||||||
events := make([]string, 0)
|
events := make([]string, 0)
|
||||||
for _, w := range wFile.Workflow {
|
for _, w := range runner.workflows.Workflow {
|
||||||
events = append(events, w.On)
|
events = append(events, w.On)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,380 +100,46 @@ func (wFile *workflowsFile) ListEvents() []string {
|
||||||
return events
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wFile *workflowsFile) GraphEvent(eventName string) ([][]string, error) {
|
// GraphEvent builds an execution path
|
||||||
|
func (runner *runnerImpl) GraphEvent(eventName string) ([][]string, error) {
|
||||||
log.Debugf("Listing actions for event '%s'", eventName)
|
log.Debugf("Listing actions for event '%s'", eventName)
|
||||||
workflow, _, err := wFile.getWorkflow(eventName)
|
workflow, _, err := runner.workflows.getWorkflow(eventName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return wFile.newExecutionGraph(workflow.Resolves...), nil
|
return runner.workflows.newExecutionGraph(workflow.Resolves...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wFile *workflowsFile) RunAction(ctx context.Context, dryrun bool, actionName string, eventJSON string) error {
|
// RunAction runs a set of actions in parallel, and their dependencies
|
||||||
log.Debugf("Running action '%s'", actionName)
|
func (runner *runnerImpl) RunActions(actionNames ...string) error {
|
||||||
return wFile.newActionExecutor(ctx, dryrun, "", eventJSON, actionName)()
|
log.Debugf("Running actions %+q", actionNames)
|
||||||
}
|
graph := runner.workflows.newExecutionGraph(actionNames...)
|
||||||
|
|
||||||
func (wFile *workflowsFile) RunEvent(ctx context.Context, dryrun bool, eventName string, eventJSON string) error {
|
|
||||||
log.Debugf("Running event '%s'", eventName)
|
|
||||||
workflow, _, err := wFile.getWorkflow(eventName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Running actions %s -> %s", eventName, workflow.Resolves)
|
|
||||||
return wFile.newActionExecutor(ctx, dryrun, eventName, eventJSON, workflow.Resolves...)()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wFile *workflowsFile) getWorkflow(eventName string) (*workflowDef, string, error) {
|
|
||||||
var rtn workflowDef
|
|
||||||
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) (*actionDef, error) {
|
|
||||||
if a, ok := wFile.Action[actionName]; ok {
|
|
||||||
return &a, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unsupported action: %v", actionName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wFile *workflowsFile) Close() {
|
|
||||||
os.RemoveAll(wFile.TempDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wFile *workflowsFile) newActionExecutor(ctx context.Context, dryrun bool, eventName string, eventJSON string, actionNames ...string) common.Executor {
|
|
||||||
graph := wFile.newExecutionGraph(actionNames...)
|
|
||||||
|
|
||||||
pipeline := make([]common.Executor, 0)
|
pipeline := make([]common.Executor, 0)
|
||||||
for _, actions := range graph {
|
for _, actions := range graph {
|
||||||
stage := make([]common.Executor, 0)
|
stage := make([]common.Executor, 0)
|
||||||
for _, actionName := range actions {
|
for _, actionName := range actions {
|
||||||
action, err := wFile.getAction(actionName)
|
stage = append(stage, runner.newActionExecutor(actionName))
|
||||||
if err != nil {
|
|
||||||
return common.NewErrorExecutor(err)
|
|
||||||
}
|
|
||||||
actionExecutor := action.asExecutor(ctx, dryrun, wFile.WorkingDir, wFile.TempDir, actionName, wFile.setupEnvironment(eventName, actionName, dryrun), eventJSON)
|
|
||||||
stage = append(stage, actionExecutor)
|
|
||||||
}
|
}
|
||||||
pipeline = append(pipeline, common.NewParallelExecutor(stage...))
|
pipeline = append(pipeline, common.NewParallelExecutor(stage...))
|
||||||
}
|
}
|
||||||
|
|
||||||
return common.NewPipelineExecutor(pipeline...)
|
executor := common.NewPipelineExecutor(pipeline...)
|
||||||
|
return executor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (action *actionDef) asExecutor(ctx context.Context, dryrun bool, workingDir string, tempDir string, actionName string, env []string, eventJSON string) common.Executor {
|
// RunEvent runs the actions for a single event
|
||||||
logger := newActionLogger(actionName, dryrun)
|
func (runner *runnerImpl) RunEvent() error {
|
||||||
log.Debugf("Using '%s' for action '%s'", action.Uses, actionName)
|
log.Debugf("Running event '%s'", runner.config.EventName)
|
||||||
|
workflow, _, err := runner.workflows.getWorkflow(runner.config.EventName)
|
||||||
in := container.DockerExecutorInput{
|
|
||||||
Ctx: ctx,
|
|
||||||
Logger: logger,
|
|
||||||
Dryrun: dryrun,
|
|
||||||
}
|
|
||||||
|
|
||||||
var image string
|
|
||||||
executors := make([]common.Executor, 0)
|
|
||||||
if imageRef, ok := parseImageReference(action.Uses); ok {
|
|
||||||
executors = append(executors, container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{
|
|
||||||
DockerExecutorInput: in,
|
|
||||||
Image: imageRef,
|
|
||||||
}))
|
|
||||||
image = imageRef
|
|
||||||
} else if contextDir, imageTag, ok := parseImageLocal(workingDir, action.Uses); ok {
|
|
||||||
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
|
||||||
DockerExecutorInput: in,
|
|
||||||
ContextDir: contextDir,
|
|
||||||
ImageTag: imageTag,
|
|
||||||
}))
|
|
||||||
image = imageTag
|
|
||||||
} else if cloneURL, ref, path, ok := parseImageGithub(action.Uses); ok {
|
|
||||||
cloneDir := filepath.Join(os.TempDir(), "act", action.Uses)
|
|
||||||
executors = append(executors, common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
|
|
||||||
URL: cloneURL,
|
|
||||||
Ref: ref,
|
|
||||||
Dir: cloneDir,
|
|
||||||
Logger: logger,
|
|
||||||
Dryrun: dryrun,
|
|
||||||
}))
|
|
||||||
|
|
||||||
contextDir := filepath.Join(cloneDir, path)
|
|
||||||
imageTag := fmt.Sprintf("%s:%s", filepath.Base(cloneURL.Path), ref)
|
|
||||||
|
|
||||||
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
|
||||||
DockerExecutorInput: in,
|
|
||||||
ContextDir: contextDir,
|
|
||||||
ImageTag: imageTag,
|
|
||||||
}))
|
|
||||||
image = imageTag
|
|
||||||
} else {
|
|
||||||
return common.NewErrorExecutor(fmt.Errorf("unable to determine executor type for image '%s'", action.Uses))
|
|
||||||
}
|
|
||||||
|
|
||||||
ghReader, err := action.createGithubTarball(eventJSON)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.NewErrorExecutor(err)
|
return err
|
||||||
}
|
|
||||||
randSuffix := randString(6)
|
|
||||||
containerName := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-")
|
|
||||||
if len(containerName)+len(randSuffix)+1 > 30 {
|
|
||||||
containerName = containerName[:(30 - (len(randSuffix) + 1))]
|
|
||||||
}
|
|
||||||
executors = append(executors, container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
|
|
||||||
DockerExecutorInput: in,
|
|
||||||
Cmd: action.Args,
|
|
||||||
Entrypoint: action.Runs,
|
|
||||||
Image: image,
|
|
||||||
WorkingDir: "/github/workspace",
|
|
||||||
Env: env,
|
|
||||||
Name: fmt.Sprintf("%s-%s", containerName, randSuffix),
|
|
||||||
Binds: []string{
|
|
||||||
fmt.Sprintf("%s:%s", workingDir, "/github/workspace"),
|
|
||||||
fmt.Sprintf("%s:%s", tempDir, "/github/home"),
|
|
||||||
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
|
|
||||||
},
|
|
||||||
Content: map[string]io.Reader{"/github": ghReader},
|
|
||||||
}))
|
|
||||||
|
|
||||||
return common.NewPipelineExecutor(executors...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const letterBytes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
log.Debugf("Running actions %s -> %s", runner.config.EventName, workflow.Resolves)
|
||||||
|
return runner.RunActions(workflow.Resolves...)
|
||||||
func randString(slen int) string {
|
|
||||||
b := make([]byte, slen)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (action *actionDef) createGithubTarball(eventJSON string) (io.Reader, error) {
|
func (runner *runnerImpl) Close() error {
|
||||||
var buf bytes.Buffer
|
return os.RemoveAll(runner.tempDir)
|
||||||
tw := tar.NewWriter(&buf)
|
|
||||||
var files = []struct {
|
|
||||||
Name string
|
|
||||||
Mode int64
|
|
||||||
Body string
|
|
||||||
}{
|
|
||||||
{"workflow/event.json", 0644, eventJSON},
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
log.Debugf("Writing entry to tarball %s len:%d from %v", file.Name, len(eventJSON), eventJSON)
|
|
||||||
hdr := &tar.Header{
|
|
||||||
Name: file.Name,
|
|
||||||
Mode: file.Mode,
|
|
||||||
Size: int64(len(eventJSON)),
|
|
||||||
}
|
|
||||||
if err := tw.WriteHeader(hdr); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, err := tw.Write([]byte(eventJSON)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := tw.Close(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &buf, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wFile *workflowsFile) setupEnvironment(eventName string, actionName string, dryrun bool) []string {
|
|
||||||
env := make([]string, 0)
|
|
||||||
repoPath := wFile.WorkingDir
|
|
||||||
|
|
||||||
_, workflowName, _ := wFile.getWorkflow(eventName)
|
|
||||||
|
|
||||||
env = append(env, fmt.Sprintf("HOME=/github/home"))
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_ACTOR=nektos/act"))
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_EVENT_PATH=/github/workflow/event.json"))
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_WORKSPACE=/github/workspace"))
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_WORKFLOW=%s", workflowName))
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_EVENT_NAME=%s", eventName))
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_ACTION=%s", actionName))
|
|
||||||
|
|
||||||
_, rev, err := common.FindGitRevision(repoPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("unable to get git revision: %v", err)
|
|
||||||
} else {
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_SHA=%s", rev))
|
|
||||||
}
|
|
||||||
|
|
||||||
repo, err := common.FindGithubRepo(repoPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("unable to get git repo: %v", err)
|
|
||||||
} else {
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_REPOSITORY=%s", repo))
|
|
||||||
}
|
|
||||||
|
|
||||||
branch, err := common.FindGitBranch(repoPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warningf("unable to get git branch: %v", err)
|
|
||||||
} else {
|
|
||||||
env = append(env, fmt.Sprintf("GITHUB_REF=refs/heads/%s", branch))
|
|
||||||
}
|
|
||||||
|
|
||||||
action, err := wFile.getAction(actionName)
|
|
||||||
if err == nil && !dryrun {
|
|
||||||
action.applyEnvironmentSecrets(&env)
|
|
||||||
}
|
|
||||||
|
|
||||||
return env
|
|
||||||
}
|
|
||||||
|
|
||||||
func (action *actionDef) applyEnvironmentSecrets(env *[]string) {
|
|
||||||
if action != nil {
|
|
||||||
for envKey, envValue := range action.Env {
|
|
||||||
*env = append(*env, fmt.Sprintf("%s=%s", envKey, envValue))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, secret := range action.Secrets {
|
|
||||||
if secretVal, ok := os.LookupEnv(secret); ok {
|
|
||||||
*env = append(*env, fmt.Sprintf("%s=%s", 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 = append(*env, fmt.Sprintf("%s=%s", secret, secretCache[secret]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
185
actions/runner_exec.go
Normal file
185
actions/runner_exec.go
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := make(map[string]string)
|
||||||
|
for _, applier := range []environmentApplier{action, runner} {
|
||||||
|
applier.applyEnvironment(env)
|
||||||
|
}
|
||||||
|
env["GITHUB_ACTION"] = actionName
|
||||||
|
|
||||||
|
logger := newActionLogger(actionName, runner.config.Dryrun)
|
||||||
|
log.Debugf("Using '%s' for action '%s'", action.Uses, actionName)
|
||||||
|
|
||||||
|
in := container.DockerExecutorInput{
|
||||||
|
Ctx: runner.config.Ctx,
|
||||||
|
Logger: logger,
|
||||||
|
Dryrun: runner.config.Dryrun,
|
||||||
|
}
|
||||||
|
|
||||||
|
var image string
|
||||||
|
executors := make([]common.Executor, 0)
|
||||||
|
if imageRef, ok := parseImageReference(action.Uses); ok {
|
||||||
|
executors = append(executors, container.NewDockerPullExecutor(container.NewDockerPullExecutorInput{
|
||||||
|
DockerExecutorInput: in,
|
||||||
|
Image: imageRef,
|
||||||
|
}))
|
||||||
|
image = imageRef
|
||||||
|
} else if contextDir, imageTag, ok := parseImageLocal(runner.config.WorkingDir, action.Uses); ok {
|
||||||
|
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||||
|
DockerExecutorInput: in,
|
||||||
|
ContextDir: contextDir,
|
||||||
|
ImageTag: imageTag,
|
||||||
|
}))
|
||||||
|
image = imageTag
|
||||||
|
} else if cloneURL, ref, path, ok := parseImageGithub(action.Uses); ok {
|
||||||
|
cloneDir := filepath.Join(os.TempDir(), "act", action.Uses)
|
||||||
|
executors = append(executors, common.NewGitCloneExecutor(common.NewGitCloneExecutorInput{
|
||||||
|
URL: cloneURL,
|
||||||
|
Ref: ref,
|
||||||
|
Dir: cloneDir,
|
||||||
|
Logger: logger,
|
||||||
|
Dryrun: runner.config.Dryrun,
|
||||||
|
}))
|
||||||
|
|
||||||
|
contextDir := filepath.Join(cloneDir, path)
|
||||||
|
imageTag := fmt.Sprintf("%s:%s", filepath.Base(cloneURL.Path), ref)
|
||||||
|
|
||||||
|
executors = append(executors, container.NewDockerBuildExecutor(container.NewDockerBuildExecutorInput{
|
||||||
|
DockerExecutorInput: in,
|
||||||
|
ContextDir: contextDir,
|
||||||
|
ImageTag: imageTag,
|
||||||
|
}))
|
||||||
|
image = imageTag
|
||||||
|
} else {
|
||||||
|
return common.NewErrorExecutor(fmt.Errorf("unable to determine executor type for image '%s'", action.Uses))
|
||||||
|
}
|
||||||
|
|
||||||
|
ghReader, err := runner.createGithubTarball()
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorExecutor(err)
|
||||||
|
}
|
||||||
|
randSuffix := randString(6)
|
||||||
|
containerName := regexp.MustCompile("[^a-zA-Z0-9]").ReplaceAllString(actionName, "-")
|
||||||
|
if len(containerName)+len(randSuffix)+1 > 30 {
|
||||||
|
containerName = containerName[:(30 - (len(randSuffix) + 1))]
|
||||||
|
}
|
||||||
|
|
||||||
|
envList := make([]string, 0)
|
||||||
|
for k, v := range env {
|
||||||
|
envList = append(envList, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
executors = append(executors, container.NewDockerRunExecutor(container.NewDockerRunExecutorInput{
|
||||||
|
DockerExecutorInput: in,
|
||||||
|
Cmd: action.Args,
|
||||||
|
Entrypoint: action.Runs,
|
||||||
|
Image: image,
|
||||||
|
WorkingDir: "/github/workspace",
|
||||||
|
Env: envList,
|
||||||
|
Name: fmt.Sprintf("%s-%s", containerName, randSuffix),
|
||||||
|
Binds: []string{
|
||||||
|
fmt.Sprintf("%s:%s", runner.config.WorkingDir, "/github/workspace"),
|
||||||
|
fmt.Sprintf("%s:%s", runner.tempDir, "/github/home"),
|
||||||
|
fmt.Sprintf("%s:%s", "/var/run/docker.sock", "/var/run/docker.sock"),
|
||||||
|
},
|
||||||
|
Content: map[string]io.Reader{"/github": ghReader},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return common.NewPipelineExecutor(executors...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) applyEnvironment(env map[string]string) {
|
||||||
|
repoPath := runner.config.WorkingDir
|
||||||
|
|
||||||
|
_, workflowName, _ := runner.workflows.getWorkflow(runner.config.EventName)
|
||||||
|
|
||||||
|
env["HOME"] = "/github/home"
|
||||||
|
env["GITHUB_ACTOR"] = "nektos/act"
|
||||||
|
env["GITHUB_EVENT_PATH"] = "/github/workflow/event.json"
|
||||||
|
env["GITHUB_WORKSPACE"] = "/github/workspace"
|
||||||
|
env["GITHUB_WORKFLOW"] = workflowName
|
||||||
|
env["GITHUB_EVENT_NAME"] = runner.config.EventName
|
||||||
|
|
||||||
|
_, rev, err := common.FindGitRevision(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("unable to get git revision: %v", err)
|
||||||
|
} else {
|
||||||
|
env["GITHUB_SHA"] = rev
|
||||||
|
}
|
||||||
|
|
||||||
|
repo, err := common.FindGithubRepo(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("unable to get git repo: %v", err)
|
||||||
|
} else {
|
||||||
|
env["GITHUB_REPOSITORY"] = repo
|
||||||
|
}
|
||||||
|
|
||||||
|
branch, err := common.FindGitBranch(repoPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Warningf("unable to get git branch: %v", err)
|
||||||
|
} else {
|
||||||
|
env["GITHUB_REF"] = fmt.Sprintf("refs/heads/%s", branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (runner *runnerImpl) createGithubTarball() (io.Reader, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
tw := tar.NewWriter(&buf)
|
||||||
|
var files = []struct {
|
||||||
|
Name string
|
||||||
|
Mode int64
|
||||||
|
Body string
|
||||||
|
}{
|
||||||
|
{"workflow/event.json", 0644, runner.eventJSON},
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
log.Debugf("Writing entry to tarball %s len:%d", file.Name, len(runner.eventJSON))
|
||||||
|
hdr := &tar.Header{
|
||||||
|
Name: file.Name,
|
||||||
|
Mode: file.Mode,
|
||||||
|
Size: int64(len(runner.eventJSON)),
|
||||||
|
}
|
||||||
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := tw.Write([]byte(runner.eventJSON)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &buf, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const letterBytes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
|
func randString(slen int) string {
|
||||||
|
b := make([]byte, slen)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
|
@ -1,56 +0,0 @@
|
||||||
package actions
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Workflows provides capabilities to work with the workflow file
|
|
||||||
type Workflows interface {
|
|
||||||
EventGrapher
|
|
||||||
EventLister
|
|
||||||
ActionRunner
|
|
||||||
EventRunner
|
|
||||||
Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventGrapher to list the actions
|
|
||||||
type EventGrapher interface {
|
|
||||||
GraphEvent(eventName string) ([][]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventLister to list the events
|
|
||||||
type EventLister interface {
|
|
||||||
ListEvents() []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActionRunner to run an action
|
|
||||||
type ActionRunner interface {
|
|
||||||
RunAction(ctx context.Context, dryrun bool, action string, eventJSON string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventRunner to run an event
|
|
||||||
type EventRunner interface {
|
|
||||||
RunEvent(ctx context.Context, dryrun bool, event string, eventJSON string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type workflowDef struct {
|
|
||||||
On string
|
|
||||||
Resolves []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type actionDef struct {
|
|
||||||
Needs []string
|
|
||||||
Uses string
|
|
||||||
Runs []string
|
|
||||||
Args []string
|
|
||||||
Env map[string]string
|
|
||||||
Secrets []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type workflowsFile struct {
|
|
||||||
TempDir string
|
|
||||||
WorkingDir string
|
|
||||||
WorkflowPath string
|
|
||||||
Workflow map[string]workflowDef
|
|
||||||
Action map[string]actionDef
|
|
||||||
}
|
|
100
cmd/root.go
100
cmd/root.go
|
@ -3,9 +3,7 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/nektos/act/actions"
|
"github.com/nektos/act/actions"
|
||||||
"github.com/nektos/act/common"
|
"github.com/nektos/act/common"
|
||||||
|
@ -13,82 +11,80 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var verbose bool
|
|
||||||
var workflowPath string
|
|
||||||
var workingDir string
|
|
||||||
var list bool
|
|
||||||
var actionName string
|
|
||||||
var dryrun bool
|
|
||||||
var eventPath string
|
|
||||||
|
|
||||||
// Execute is the entry point to running the CLI
|
// Execute is the entry point to running the CLI
|
||||||
func Execute(ctx context.Context, version string) {
|
func Execute(ctx context.Context, version string) {
|
||||||
|
runnerConfig := &actions.RunnerConfig{Ctx: ctx}
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "act [event name to run]",
|
Use: "act [event name to run]",
|
||||||
Short: "Run Github actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
Short: "Run Github actions locally by specifying the event name (e.g. `push`) or an action name directly.",
|
||||||
Args: cobra.MaximumNArgs(1),
|
Args: cobra.MaximumNArgs(1),
|
||||||
RunE: newRunAction(ctx),
|
RunE: newRunCommand(runnerConfig),
|
||||||
|
PersistentPreRun: setupLogging,
|
||||||
Version: version,
|
Version: version,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
}
|
}
|
||||||
rootCmd.Flags().BoolVarP(&list, "list", "l", false, "list actions")
|
rootCmd.Flags().BoolP("list", "l", false, "list actions")
|
||||||
rootCmd.Flags().StringVarP(&actionName, "action", "a", "", "run action")
|
rootCmd.Flags().StringP("action", "a", "", "run action")
|
||||||
rootCmd.Flags().StringVarP(&eventPath, "event", "e", "", "path to event JSON file")
|
rootCmd.Flags().StringVarP(&runnerConfig.EventPath, "event", "e", "", "path to event JSON file")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&dryrun, "dryrun", "n", false, "dryrun mode")
|
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
rootCmd.PersistentFlags().BoolVarP(&runnerConfig.Dryrun, "dryrun", "n", false, "dryrun mode")
|
||||||
rootCmd.PersistentFlags().StringVarP(&workflowPath, "file", "f", "./.github/main.workflow", "path to workflow file")
|
rootCmd.PersistentFlags().StringVarP(&runnerConfig.WorkflowPath, "file", "f", "./.github/main.workflow", "path to workflow file")
|
||||||
rootCmd.PersistentFlags().StringVarP(&workingDir, "directory", "C", ".", "working directory")
|
rootCmd.PersistentFlags().StringVarP(&runnerConfig.WorkingDir, "directory", "C", ".", "working directory")
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRunAction(ctx context.Context) func(*cobra.Command, []string) error {
|
func setupLogging(cmd *cobra.Command, args []string) {
|
||||||
return func(cmd *cobra.Command, args []string) error {
|
verbose, _ := cmd.Flags().GetBool("verbose")
|
||||||
if verbose {
|
if verbose {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
workflows, err := actions.ParseWorkflows(workingDir, workflowPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer workflows.Close()
|
|
||||||
|
|
||||||
if list {
|
|
||||||
return listEvents(workflows)
|
|
||||||
}
|
|
||||||
|
|
||||||
eventJSON := "{}"
|
|
||||||
if eventPath != "" {
|
|
||||||
if !filepath.IsAbs(eventPath) {
|
|
||||||
eventPath = filepath.Join(workingDir, eventPath)
|
|
||||||
}
|
|
||||||
log.Debugf("Reading event.json from %s", eventPath)
|
|
||||||
eventJSONBytes, err := ioutil.ReadFile(eventPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
eventJSON = string(eventJSONBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
if actionName != "" {
|
|
||||||
return workflows.RunAction(ctx, dryrun, actionName, eventJSON)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRunCommand(runnerConfig *actions.RunnerConfig) func(*cobra.Command, []string) error {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return workflows.RunEvent(ctx, dryrun, "push", eventJSON)
|
runnerConfig.EventName = "push"
|
||||||
|
} else {
|
||||||
|
runnerConfig.EventName = args[0]
|
||||||
}
|
}
|
||||||
return workflows.RunEvent(ctx, dryrun, args[0], eventJSON)
|
|
||||||
|
// create the runner
|
||||||
|
runner, err := actions.NewRunner(runnerConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer runner.Close()
|
||||||
|
|
||||||
|
// check if we should just print the graph
|
||||||
|
list, err := cmd.Flags().GetBool("list")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if list {
|
||||||
|
return drawGraph(runner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we are running just a single action
|
||||||
|
actionName, err := cmd.Flags().GetString("action")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if actionName != "" {
|
||||||
|
return runner.RunActions(actionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the event in the RunnerRonfig
|
||||||
|
return runner.RunEvent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listEvents(workflows actions.Workflows) error {
|
func drawGraph(runner actions.Runner) error {
|
||||||
eventNames := workflows.ListEvents()
|
eventNames := runner.ListEvents()
|
||||||
for _, eventName := range eventNames {
|
for _, eventName := range eventNames {
|
||||||
graph, err := workflows.GraphEvent(eventName)
|
graph, err := runner.GraphEvent(eventName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue