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:
parent
113ebda3ff
commit
fbab49c68d
28 changed files with 522 additions and 393 deletions
8
.github/workflows/basic.yml
vendored
Normal file
8
.github/workflows/basic.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
name: basic
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo hello world!
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
196
act/model/planner.go
Normal 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
67
act/model/workflow.go
Normal 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
5
act/runner/api.go
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
package runner
|
||||||
|
|
||||||
|
type environmentApplier interface {
|
||||||
|
applyEnvironment(map[string]string)
|
||||||
|
}
|
88
act/runner/runner.go
Normal file
88
act/runner/runner.go
Normal 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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package actions
|
package runner
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
40
cmd/graph.go
Normal 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
40
cmd/input.go
Normal 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)
|
||||||
|
}
|
149
cmd/root.go
149
cmd/root.go
|
@ -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 {
|
||||||
|
planner, err := model.NewWorkflowPlanner(input.WorkflowsPath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the event name
|
||||||
|
var eventName string
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
runnerConfig.EventName = args[0]
|
eventName = args[0]
|
||||||
}
|
} else {
|
||||||
|
|
||||||
watch, err := cmd.Flags().GetBool("watch")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if watch {
|
|
||||||
return watchAndRun(runnerConfig.Ctx, func() error {
|
|
||||||
return parseAndRun(cmd, runnerConfig)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return parseAndRun(cmd, runnerConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAndRun(cmd *cobra.Command, runnerConfig *actions.RunnerConfig) error {
|
|
||||||
// create the runner
|
|
||||||
runner, err := actions.NewRunner(runnerConfig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer runner.Close()
|
|
||||||
|
|
||||||
// set default event type if we only have a single workflow in the file.
|
// set default event type if we only have a single workflow in the file.
|
||||||
// this way user dont have to specify the event.
|
// this way user dont have to specify the event.
|
||||||
if runnerConfig.EventName == "" {
|
if events := planner.GetEvents(); len(events) == 1 {
|
||||||
if events := runner.ListEvents(); len(events) == 1 {
|
|
||||||
log.Debugf("Using detected workflow event: %s", events[0])
|
log.Debugf("Using detected workflow event: %s", events[0])
|
||||||
runnerConfig.EventName = events[0]
|
eventName = events[0]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fall back to default event name if we could not detect one.
|
// build the plan for this run
|
||||||
if runnerConfig.EventName == "" {
|
var plan *model.Plan
|
||||||
runnerConfig.EventName = "push"
|
if jobID, err := cmd.Flags().GetString("job"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if jobID != "" {
|
||||||
|
log.Debugf("Planning job: %s", jobID)
|
||||||
|
plan = planner.PlanJob(jobID)
|
||||||
|
} else {
|
||||||
|
log.Debugf("Planning event: %s", eventName)
|
||||||
|
plan = planner.PlanEvent(eventName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we should just print the graph
|
// check if we should just print the graph
|
||||||
list, err := cmd.Flags().GetBool("list")
|
if list, err := cmd.Flags().GetBool("list"); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
} else if list {
|
||||||
if list {
|
return drawGraph(plan)
|
||||||
return drawGraph(runner)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we are running just a single action
|
// run the plan
|
||||||
actionName, err := cmd.Flags().GetString("action")
|
// runner, err := runner.New(config)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
if actionName != "" {
|
// defer runner.Close()
|
||||||
return runner.RunActions(actionName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// run the event in the RunnerRonfig
|
// if watch, err := cmd.Flags().GetBool("watch"); err != nil {
|
||||||
return runner.RunEvent()
|
// return err
|
||||||
|
// } else if watch {
|
||||||
|
// return watchAndRun(ctx, func() error {
|
||||||
|
// return runner.RunPlan(plan)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return runner.RunPlan(plan)
|
||||||
|
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
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue