1
0
Fork 0
mirror of https://code.forgejo.org/forgejo/runner.git synced 2025-08-06 17:40:58 +00:00

initial load of yaml working

This commit is contained in:
Casey Lee 2020-02-04 16:38:41 -08:00
parent 113ebda3ff
commit fbab49c68d
28 changed files with 522 additions and 393 deletions

8
.github/workflows/basic.yml vendored Normal file
View file

@ -0,0 +1,8 @@
name: basic
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo hello world!

View file

@ -10,7 +10,7 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils" "github.com/docker/docker/pkg/fileutils"
"github.com/nektos/act/common" "github.com/nektos/act/pkg/common"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )

View file

@ -6,7 +6,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/nektos/act/common" "github.com/nektos/act/pkg/common"
) )
// NewDockerPullExecutorInput the input for the NewDockerPullExecutor function // NewDockerPullExecutorInput the input for the NewDockerPullExecutor function

View file

@ -9,7 +9,7 @@ import (
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/nektos/act/common" "github.com/nektos/act/pkg/common"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )

View file

@ -3,11 +3,12 @@ package container
import ( import (
"bytes" "bytes"
"context" "context"
"github.com/nektos/act/common"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"io/ioutil" "io/ioutil"
"testing" "testing"
"github.com/nektos/act/pkg/common"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
) )
type rawFormatter struct{} type rawFormatter struct{}

196
act/model/planner.go Normal file
View file

@ -0,0 +1,196 @@
package model
import (
"io/ioutil"
"math"
"os"
"path/filepath"
"sort"
log "github.com/sirupsen/logrus"
)
// WorkflowPlanner contains methods for creating plans
type WorkflowPlanner interface {
PlanEvent(eventName string) *Plan
PlanJob(jobName string) *Plan
GetEvents() []string
}
// Plan contains a list of stages to run in series
type Plan struct {
Stages []*Stage
}
// Stage contains a list of runs to execute in parallel
type Stage struct {
Runs []*Run
}
// Run represents a job from a workflow that needs to be run
type Run struct {
Workflow *Workflow
JobID string
}
// NewWorkflowPlanner will load all workflows from a directory
func NewWorkflowPlanner(dirname string) (WorkflowPlanner, error) {
log.Debugf("Loading workflows from '%s'", dirname)
files, err := ioutil.ReadDir(dirname)
if err != nil {
return nil, err
}
wp := new(workflowPlanner)
for _, file := range files {
ext := filepath.Ext(file.Name())
if ext == ".yml" || ext == ".yaml" {
f, err := os.Open(filepath.Join(dirname, file.Name()))
if err != nil {
return nil, err
}
workflow, err := ReadWorkflow(f)
if err != nil {
f.Close()
return nil, err
}
wp.workflows = append(wp.workflows, workflow)
f.Close()
}
}
return wp, nil
}
type workflowPlanner struct {
workflows []*Workflow
}
// PlanEvent builds a new list of runs to execute in parallel for an event name
func (wp *workflowPlanner) PlanEvent(eventName string) *Plan {
plan := new(Plan)
for _, w := range wp.workflows {
if w.On == eventName {
plan.mergeStages(createStages(w, w.GetJobIDs()...))
}
}
return plan
}
// PlanJob builds a new run to execute in parallel for a job name
func (wp *workflowPlanner) PlanJob(jobName string) *Plan {
plan := new(Plan)
for _, w := range wp.workflows {
plan.mergeStages(createStages(w, jobName))
}
return plan
}
// GetEvents gets all the events in the workflows file
func (wp *workflowPlanner) GetEvents() []string {
events := make([]string, 0)
for _, w := range wp.workflows {
found := false
for _, e := range events {
if e == w.On {
found = true
break
}
}
if !found {
events = append(events, w.On)
}
}
// sort the list based on depth of dependencies
sort.Slice(events, func(i, j int) bool {
return events[i] < events[j]
})
return events
}
// GetJobIDs will get all the job names in the stage
func (s *Stage) GetJobIDs() []string {
names := make([]string, 0)
for _, r := range s.Runs {
names = append(names, r.JobID)
}
return names
}
// Merge stages with existing stages in plan
func (p *Plan) mergeStages(stages []*Stage) {
newStages := make([]*Stage, int(math.Max(float64(len(p.Stages)), float64(len(stages)))))
for i := 0; i < len(newStages); i++ {
newStages[i] = new(Stage)
if i >= len(p.Stages) {
newStages[i].Runs = append(stages[i].Runs)
} else if i >= len(stages) {
newStages[i].Runs = append(p.Stages[i].Runs)
} else {
newStages[i].Runs = append(p.Stages[i].Runs, stages[i].Runs...)
}
}
p.Stages = newStages
}
func createStages(w *Workflow, jobIDs ...string) []*Stage {
// first, build a list of all the necessary jobs to run, and their dependencies
jobDependencies := make(map[string][]string)
for len(jobIDs) > 0 {
newJobIDs := make([]string, 0)
for _, jID := range jobIDs {
// make sure we haven't visited this job yet
if _, ok := jobDependencies[jID]; !ok {
if job := w.GetJob(jID); job != nil {
jobDependencies[jID] = job.Needs
newJobIDs = append(newJobIDs, job.Needs...)
}
}
}
jobIDs = newJobIDs
}
// next, build an execution graph
stages := make([]*Stage, 0)
for len(jobDependencies) > 0 {
stage := new(Stage)
for jID, jDeps := range jobDependencies {
// make sure all deps are in the graph already
if listInStages(jDeps, stages...) {
stage.Runs = append(stage.Runs, &Run{
Workflow: w,
JobID: jID,
})
delete(jobDependencies, jID)
}
}
if len(stage.Runs) == 0 {
log.Fatalf("Unable to build dependency graph!")
}
stages = append(stages, stage)
}
return stages
}
// return true iff all strings in srcList exist in at least one of the stages
func listInStages(srcList []string, stages ...*Stage) bool {
for _, src := range srcList {
found := false
for _, stage := range stages {
for _, search := range stage.GetJobIDs() {
if src == search {
found = true
}
}
}
if !found {
return false
}
}
return true
}

67
act/model/workflow.go Normal file
View file

@ -0,0 +1,67 @@
package model
import (
"io"
"gopkg.in/yaml.v2"
)
// Workflow is the structure of the files in .github/workflows
type Workflow struct {
Name string `yaml:"name"`
On string `yaml:"on"`
Env map[string]string `yaml:"env"`
Jobs map[string]*Job `yaml:"jobs"`
}
// Job is the structure of one job in a workflow
type Job struct {
Name string `yaml:"name"`
Needs []string `yaml:"needs"`
RunsOn string `yaml:"runs-on"`
Env map[string]string `yaml:"env"`
If string `yaml:"if"`
Steps []*Step `yaml:"steps"`
TimeoutMinutes int64 `yaml:"timeout-minutes"`
}
// Step is the structure of one step in a job
type Step struct {
ID string `yaml:"id"`
If string `yaml:"if"`
Name string `yaml:"name"`
Uses string `yaml:"uses"`
Run string `yaml:"run"`
WorkingDirectory string `yaml:"working-directory"`
Shell string `yaml:"shell"`
Env map[string]string `yaml:"env"`
With map[string]string `yaml:"with"`
ContinueOnError bool `yaml:"continue-on-error"`
TimeoutMinutes int64 `yaml:"timeout-minutes"`
}
// ReadWorkflow returns a list of jobs for a given workflow file reader
func ReadWorkflow(in io.Reader) (*Workflow, error) {
w := new(Workflow)
err := yaml.NewDecoder(in).Decode(w)
return w, err
}
// GetJob will get a job by name in the workflow
func (w *Workflow) GetJob(jobID string) *Job {
for id, j := range w.Jobs {
if jobID == id {
return j
}
}
return nil
}
// GetJobIDs will get all the job names in the workflow
func (w *Workflow) GetJobIDs() []string {
ids := make([]string, 0)
for id := range w.Jobs {
ids = append(ids, id)
}
return ids
}

5
act/runner/api.go Normal file
View file

@ -0,0 +1,5 @@
package runner
type environmentApplier interface {
applyEnvironment(map[string]string)
}

88
act/runner/runner.go Normal file
View file

@ -0,0 +1,88 @@
package runner
import (
"io"
"io/ioutil"
"os"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus"
)
// Runner provides capabilities to run GitHub actions
type Runner interface {
PlanRunner
io.Closer
}
// PlanRunner to run a specific actions
type PlanRunner interface {
RunPlan(plan *model.Plan) error
}
// Config contains the config for a new runner
type Config struct {
Dryrun bool // don't start any of the containers
EventName string // name of event to run
EventPath string // path to JSON file to use for event.json in containers
ReuseContainers bool // reuse containers to maintain state
ForcePull bool // force pulling of the image, if already present
}
type runnerImpl struct {
config *Config
tempDir string
eventJSON string
}
// NewRunner Creates a new Runner
func NewRunner(runnerConfig *Config) (Runner, error) {
runner := &runnerImpl{
config: runnerConfig,
}
init := common.NewPipelineExecutor(
runner.setupTempDir,
runner.setupEvent,
)
return runner, init()
}
func (runner *runnerImpl) setupTempDir() error {
var err error
runner.tempDir, err = ioutil.TempDir("", "act-")
return err
}
func (runner *runnerImpl) setupEvent() error {
runner.eventJSON = "{}"
if 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) RunPlan(plan *model.Plan) error {
pipeline := make([]common.Executor, 0)
for _, stage := range plan.Stages {
stageExecutor := make([]common.Executor, 0)
for _, run := range stage.Runs {
stageExecutor = append(stageExecutor, runner.newRunExecutor(run))
}
pipeline = append(pipeline, common.NewParallelExecutor(stageExecutor...))
}
executor := common.NewPipelineExecutor(pipeline...)
return executor()
}
func (runner *runnerImpl) Close() error {
return os.RemoveAll(runner.tempDir)
}

View file

@ -1,21 +1,20 @@
package actions package runner
import ( import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"github.com/actions/workflow-parser/model" "github.com/nektos/act/pkg/common"
"github.com/nektos/act/common" "github.com/nektos/act/pkg/container"
"github.com/nektos/act/container" "github.com/nektos/act/pkg/model"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor { func (runner *runnerImpl) newRunExecutor(run *model.Run) common.Executor {
action := runner.workflowConfig.GetAction(actionName) action := runner.workflowConfig.GetAction(actionName)
if action == nil { if action == nil {
return common.NewErrorExecutor(fmt.Errorf("Unable to find action named '%s'", actionName)) return common.NewErrorExecutor(fmt.Errorf("Unable to find action named '%s'", actionName))
@ -35,7 +34,8 @@ func (runner *runnerImpl) newActionExecutor(actionName string) common.Executor {
return common.NewPipelineExecutor(executors...) return common.NewPipelineExecutor(executors...)
} }
func (runner *runnerImpl) addImageExecutor(action *model.Action, executors *[]common.Executor) (string, error) { /*
func (runner *runnerImpl) addImageExecutor(action *Action, executors *[]common.Executor) (string, error) {
var image string var image string
logger := newActionLogger(action.Identifier, runner.config.Dryrun) logger := newActionLogger(action.Identifier, runner.config.Dryrun)
log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier) log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier)
@ -111,8 +111,9 @@ func (runner *runnerImpl) addImageExecutor(action *model.Action, executors *[]co
return image, nil return image, nil
} }
*/
func (runner *runnerImpl) addRunExecutor(action *model.Action, image string, executors *[]common.Executor) error { func (runner *runnerImpl) addRunExecutor(action *Action, image string, executors *[]common.Executor) error {
logger := newActionLogger(action.Identifier, runner.config.Dryrun) logger := newActionLogger(action.Identifier, runner.config.Dryrun)
log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier) log.Debugf("Using '%s' for action '%s'", action.Uses, action.Identifier)

View file

@ -1,4 +1,4 @@
package actions package runner
import ( import (
"context" "context"

View file

@ -5,17 +5,18 @@ import (
"log" "log"
"os" "os"
"github.com/actions/workflow-parser/model"
"github.com/howeyc/gopass" "github.com/howeyc/gopass"
) )
var secretCache map[string]string var secretCache map[string]string
type actionEnvironmentApplier struct { type actionEnvironmentApplier struct {
*model.Action *Action
} }
func newActionEnvironmentApplier(action *model.Action) environmentApplier { type Action struct{}
func newActionEnvironmentApplier(action *Action) environmentApplier {
return &actionEnvironmentApplier{action} return &actionEnvironmentApplier{action}
} }

View file

@ -1,51 +0,0 @@
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
ReuseContainers bool // reuse containers to maintain state
ForcePull bool // force pulling of the image, if already present
}
type environmentApplier interface {
applyEnvironment(map[string]string)
}

View file

@ -1,64 +0,0 @@
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
}

View file

@ -1,160 +0,0 @@
package actions
import (
"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
workflowConfig *model.Configuration
tempDir string
eventJSON 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.workflowConfig, err = parser.Parse(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")
events := make([]string, 0)
for _, w := range runner.workflowConfig.Workflows {
events = append(events, w.On)
}
// sort the list based on depth of dependencies
sort.Slice(events, func(i, j int) bool {
return events[i] < events[j]
})
return events
}
// GraphEvent builds an execution path
func (runner *runnerImpl) GraphEvent(eventName string) ([][]string, error) {
log.Debugf("Listing actions for event '%s'", eventName)
resolves := runner.resolveEvent(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 := newExecutionGraph(runner.workflowConfig, actionNames...)
pipeline := make([]common.Executor, 0)
for _, actions := range graph {
stage := make([]common.Executor, 0)
for _, actionName := range actions {
stage = append(stage, runner.newActionExecutor(actionName))
}
pipeline = append(pipeline, common.NewParallelExecutor(stage...))
}
executor := common.NewPipelineExecutor(pipeline...)
return executor()
}
// RunEvent runs the actions for a single event
func (runner *runnerImpl) RunEvent() error {
log.Debugf("Running event '%s'", runner.config.EventName)
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(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
}

40
cmd/graph.go Normal file
View file

@ -0,0 +1,40 @@
package cmd
import (
"fmt"
"os"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/model"
)
func drawGraph(plan *model.Plan) error {
drawings := make([]*common.Drawing, 0)
jobPen := common.NewPen(common.StyleSingleLine, 96)
arrowPen := common.NewPen(common.StyleNoLine, 97)
for i, stage := range plan.Stages {
if i > 0 {
drawings = append(drawings, arrowPen.DrawArrow())
}
ids := make([]string, 0)
for _, r := range stage.Runs {
ids = append(ids, fmt.Sprintf("%s/%s", r.Workflow.Name, r.JobID))
}
drawings = append(drawings, jobPen.DrawBoxes(ids...))
}
maxWidth := 0
for _, d := range drawings {
if d.GetWidth() > maxWidth {
maxWidth = d.GetWidth()
}
}
for _, d := range drawings {
d.Draw(os.Stdout, maxWidth)
}
return nil
}

40
cmd/input.go Normal file
View file

@ -0,0 +1,40 @@
package cmd
import (
"log"
"path/filepath"
)
// Input contains the input for the root command
type Input struct {
workingDir string
workflowsPath string
eventPath string
reuseContainers bool
dryrun bool
forcePull bool
}
func (i *Input) resolve(path string) string {
basedir, err := filepath.Abs(i.workingDir)
if err != nil {
log.Fatal(err)
}
if path == "" {
return path
}
if !filepath.IsAbs(path) {
path = filepath.Join(basedir, path)
}
return path
}
// WorkflowsPath returns path to workflows
func (i *Input) WorkflowsPath() string {
return i.resolve(i.workflowsPath)
}
// EventPath returns the path to events file
func (i *Input) EventPath() string {
return i.resolve(i.eventPath)
}

View file

@ -2,13 +2,11 @@ package cmd
import ( import (
"context" "context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
fswatch "github.com/andreaskoch/go-fswatch" fswatch "github.com/andreaskoch/go-fswatch"
"github.com/nektos/act/actions" "github.com/nektos/act/pkg/model"
"github.com/nektos/act/common"
gitignore "github.com/sabhiram/go-gitignore" gitignore "github.com/sabhiram/go-gitignore"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -16,26 +14,26 @@ import (
// 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} input := new(Input)
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: newRunCommand(runnerConfig), RunE: newRunCommand(ctx, input),
PersistentPreRun: setupLogging, PersistentPreRun: setupLogging,
Version: version, Version: version,
SilenceUsage: true, SilenceUsage: true,
} }
rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change") rootCmd.Flags().BoolP("watch", "w", false, "watch the contents of the local repo and run when files change")
rootCmd.Flags().BoolP("list", "l", false, "list actions") rootCmd.Flags().BoolP("list", "l", false, "list workflows")
rootCmd.Flags().StringP("action", "a", "", "run action") rootCmd.Flags().StringP("job", "j", "", "run job")
rootCmd.Flags().BoolVarP(&runnerConfig.ReuseContainers, "reuse", "r", false, "reuse action containers to maintain state") rootCmd.Flags().BoolVarP(&input.reuseContainers, "reuse", "r", false, "reuse action containers to maintain state")
rootCmd.Flags().StringVarP(&runnerConfig.EventPath, "event", "e", "", "path to event JSON file") rootCmd.Flags().BoolVarP(&input.forcePull, "pull", "p", false, "pull docker image(s) if already present")
rootCmd.Flags().BoolVarP(&runnerConfig.ForcePull, "pull", "p", false, "pull docker image(s) if already present") rootCmd.Flags().StringVarP(&input.eventPath, "event", "e", "", "path to event JSON file")
rootCmd.PersistentFlags().StringVarP(&input.workflowsPath, "workflows", "W", "./.github/workflows/", "path to workflow files")
rootCmd.PersistentFlags().StringVarP(&input.workingDir, "directory", "C", ".", "working directory")
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output") rootCmd.PersistentFlags().BoolP("verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().BoolVarP(&runnerConfig.Dryrun, "dryrun", "n", false, "dryrun mode") rootCmd.PersistentFlags().BoolVarP(&input.dryrun, "dryrun", "n", false, "dryrun mode")
rootCmd.PersistentFlags().StringVarP(&runnerConfig.WorkflowPath, "file", "f", "./.github/main.workflow", "path to workflow file")
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)
} }
@ -49,67 +47,63 @@ func setupLogging(cmd *cobra.Command, args []string) {
} }
} }
func newRunCommand(runnerConfig *actions.RunnerConfig) func(*cobra.Command, []string) error { func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
if len(args) > 0 { planner, err := model.NewWorkflowPlanner(input.WorkflowsPath())
runnerConfig.EventName = args[0]
}
watch, err := cmd.Flags().GetBool("watch")
if err != nil { if err != nil {
return err return err
} }
if watch {
return watchAndRun(runnerConfig.Ctx, func() error { // Determine the event name
return parseAndRun(cmd, runnerConfig) var eventName string
}) if len(args) > 0 {
eventName = args[0]
} else {
// set default event type if we only have a single workflow in the file.
// this way user dont have to specify the event.
if events := planner.GetEvents(); len(events) == 1 {
log.Debugf("Using detected workflow event: %s", events[0])
eventName = events[0]
}
} }
return parseAndRun(cmd, runnerConfig)
}
}
func parseAndRun(cmd *cobra.Command, runnerConfig *actions.RunnerConfig) error { // build the plan for this run
// create the runner var plan *model.Plan
runner, err := actions.NewRunner(runnerConfig) if jobID, err := cmd.Flags().GetString("job"); err != nil {
if err != nil { return err
return err } else if jobID != "" {
} log.Debugf("Planning job: %s", jobID)
defer runner.Close() plan = planner.PlanJob(jobID)
} else {
// set default event type if we only have a single workflow in the file. log.Debugf("Planning event: %s", eventName)
// this way user dont have to specify the event. plan = planner.PlanEvent(eventName)
if runnerConfig.EventName == "" {
if events := runner.ListEvents(); len(events) == 1 {
log.Debugf("Using detected workflow event: %s", events[0])
runnerConfig.EventName = events[0]
} }
}
// fall back to default event name if we could not detect one. // check if we should just print the graph
if runnerConfig.EventName == "" { if list, err := cmd.Flags().GetBool("list"); err != nil {
runnerConfig.EventName = "push" return err
} } else if list {
return drawGraph(plan)
}
// check if we should just print the graph // run the plan
list, err := cmd.Flags().GetBool("list") // runner, err := runner.New(config)
if err != nil { // if err != nil {
return err // return err
} // }
if list { // defer runner.Close()
return drawGraph(runner)
}
// check if we are running just a single action // if watch, err := cmd.Flags().GetBool("watch"); err != nil {
actionName, err := cmd.Flags().GetString("action") // return err
if err != nil { // } else if watch {
return err // return watchAndRun(ctx, func() error {
} // return runner.RunPlan(plan)
if actionName != "" { // })
return runner.RunActions(actionName) // }
}
// run the event in the RunnerRonfig // return runner.RunPlan(plan)
return runner.RunEvent() return nil
}
} }
func watchAndRun(ctx context.Context, fn func() error) error { func watchAndRun(ctx context.Context, fn func() error) error {
@ -155,40 +149,3 @@ func watchAndRun(ctx context.Context, fn func() error) error {
folderWatcher.Stop() folderWatcher.Stop()
return err return err
} }
func drawGraph(runner actions.Runner) error {
eventNames := runner.ListEvents()
for _, eventName := range eventNames {
graph, err := runner.GraphEvent(eventName)
if err != nil {
return err
}
drawings := make([]*common.Drawing, 0)
eventPen := common.NewPen(common.StyleDoubleLine, 91 /*34*/)
drawings = append(drawings, eventPen.DrawBoxes(fmt.Sprintf("EVENT: %s", eventName)))
actionPen := common.NewPen(common.StyleSingleLine, 96)
arrowPen := common.NewPen(common.StyleNoLine, 97)
drawings = append(drawings, arrowPen.DrawArrow())
for i, stage := range graph {
if i > 0 {
drawings = append(drawings, arrowPen.DrawArrow())
}
drawings = append(drawings, actionPen.DrawBoxes(stage...))
}
maxWidth := 0
for _, d := range drawings {
if d.GetWidth() > maxWidth {
maxWidth = d.GetWidth()
}
}
for _, d := range drawings {
d.Draw(os.Stdout, maxWidth)
}
}
return nil
}