2019-01-12 22:45:25 -06:00
package cmd
import (
2020-02-25 00:11:33 -06:00
"bufio"
2019-01-12 22:45:25 -06:00
"context"
2020-02-25 00:11:33 -06:00
"fmt"
2019-01-12 22:45:25 -06:00
"os"
2019-02-09 20:39:09 -06:00
"path/filepath"
2020-03-02 10:11:46 -06:00
"regexp"
2020-02-25 00:11:33 -06:00
"strings"
2019-01-12 22:45:25 -06:00
2020-02-07 00:17:58 -06:00
"github.com/nektos/act/pkg/common"
2019-02-09 20:39:09 -06:00
fswatch "github.com/andreaskoch/go-fswatch"
2020-03-06 14:30:24 -06:00
"github.com/joho/godotenv"
2020-02-04 18:38:41 -06:00
"github.com/nektos/act/pkg/model"
2020-02-07 00:17:58 -06:00
"github.com/nektos/act/pkg/runner"
2019-02-09 20:39:09 -06:00
gitignore "github.com/sabhiram/go-gitignore"
2019-01-12 22:45:25 -06:00
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// Execute is the entry point to running the CLI
func Execute ( ctx context . Context , version string ) {
2020-02-04 18:38:41 -06:00
input := new ( Input )
2019-01-12 22:45:25 -06:00
var rootCmd = & cobra . Command {
2019-01-17 02:15:35 -06:00
Use : "act [event name to run]" ,
Short : "Run Github actions locally by specifying the event name (e.g. `push`) or an action name directly." ,
Args : cobra . MaximumNArgs ( 1 ) ,
2020-02-04 18:38:41 -06:00
RunE : newRunCommand ( ctx , input ) ,
2019-01-17 02:15:35 -06:00
PersistentPreRun : setupLogging ,
Version : version ,
SilenceUsage : true ,
2019-01-12 22:45:25 -06:00
}
2019-02-09 20:39:09 -06:00
rootCmd . Flags ( ) . BoolP ( "watch" , "w" , false , "watch the contents of the local repo and run when files change" )
2020-02-04 18:38:41 -06:00
rootCmd . Flags ( ) . BoolP ( "list" , "l" , false , "list workflows" )
rootCmd . Flags ( ) . StringP ( "job" , "j" , "" , "run job" )
2020-02-17 23:51:49 -06:00
rootCmd . Flags ( ) . StringArrayVarP ( & input . secrets , "secret" , "s" , [ ] string { } , "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)" )
2020-02-19 21:16:40 -06:00
rootCmd . Flags ( ) . StringArrayVarP ( & input . platforms , "platform" , "P" , [ ] string { } , "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)" )
2020-02-04 18:38:41 -06:00
rootCmd . Flags ( ) . BoolVarP ( & input . reuseContainers , "reuse" , "r" , false , "reuse action containers to maintain state" )
2020-02-24 19:48:21 -06:00
rootCmd . Flags ( ) . BoolVarP ( & input . bindWorkdir , "bind" , "b" , false , "bind working directory to container, rather than copy" )
2020-02-04 18:38:41 -06:00
rootCmd . Flags ( ) . BoolVarP ( & input . forcePull , "pull" , "p" , false , "pull docker image(s) if already present" )
2020-02-17 23:51:49 -06:00
rootCmd . Flags ( ) . StringVarP ( & input . eventPath , "eventpath" , "e" , "" , "path to event JSON file" )
2020-05-12 02:14:56 -05:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . actor , "actor" , "a" , "nektos/act" , "user that triggered the event" )
2020-02-04 18:38:41 -06:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . workflowsPath , "workflows" , "W" , "./.github/workflows/" , "path to workflow files" )
2020-02-07 00:17:58 -06:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . workdir , "directory" , "C" , "." , "working directory" )
2019-01-17 02:15:35 -06:00
rootCmd . PersistentFlags ( ) . BoolP ( "verbose" , "v" , false , "verbose output" )
2020-02-20 10:57:18 -06:00
rootCmd . PersistentFlags ( ) . BoolVarP ( & input . noOutput , "quiet" , "q" , false , "disable logging of output from steps" )
2020-02-04 18:38:41 -06:00
rootCmd . PersistentFlags ( ) . BoolVarP ( & input . dryrun , "dryrun" , "n" , false , "dryrun mode" )
2020-04-17 12:04:40 -05:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . secretfile , "secret-file" , "" , "" , "file with list of secrets to read from" )
rootCmd . PersistentFlags ( ) . StringVarP ( & input . envfile , "env-file" , "" , ".env" , "environment file to read and use as env in the containers" )
2020-02-25 00:11:33 -06:00
rootCmd . SetArgs ( args ( ) )
2020-03-06 14:30:24 -06:00
2019-01-12 22:45:25 -06:00
if err := rootCmd . Execute ( ) ; err != nil {
os . Exit ( 1 )
}
}
2020-02-25 00:11:33 -06:00
func args ( ) [ ] string {
args := make ( [ ] string , 0 )
for _ , dir := range [ ] string {
os . Getenv ( "HOME" ) ,
"." ,
} {
args = append ( args , readArgsFile ( fmt . Sprintf ( "%s/.actrc" , dir ) ) ... )
}
args = append ( args , os . Args [ 1 : ] ... )
return args
}
func readArgsFile ( file string ) [ ] string {
args := make ( [ ] string , 0 )
f , err := os . Open ( file )
if err != nil {
return args
}
defer f . Close ( )
scanner := bufio . NewScanner ( f )
for scanner . Scan ( ) {
arg := scanner . Text ( )
if strings . HasPrefix ( arg , "-" ) {
2020-03-02 10:11:46 -06:00
args = append ( args , regexp . MustCompile ( ` \s ` ) . Split ( arg , 2 ) ... )
2020-02-25 00:11:33 -06:00
}
}
return args
}
2019-01-17 02:15:35 -06:00
func setupLogging ( cmd * cobra . Command , args [ ] string ) {
verbose , _ := cmd . Flags ( ) . GetBool ( "verbose" )
if verbose {
log . SetLevel ( log . DebugLevel )
}
}
2020-04-17 12:04:40 -05:00
func readEnvs ( path string , envs map [ string ] string ) bool {
if _ , err := os . Stat ( path ) ; err == nil {
env , err := godotenv . Read ( path )
if err != nil {
log . Fatalf ( "Error loading from %s: %v" , path , err )
}
for k , v := range env {
envs [ k ] = v
}
return true
}
return false
}
2020-02-04 18:38:41 -06:00
func newRunCommand ( ctx context . Context , input * Input ) func ( * cobra . Command , [ ] string ) error {
2019-01-12 22:45:25 -06:00
return func ( cmd * cobra . Command , args [ ] string ) error {
2020-04-17 12:04:40 -05:00
log . Debugf ( "Loading environment from %s" , input . Envfile ( ) )
envs := make ( map [ string ] string )
_ = readEnvs ( input . Envfile ( ) , envs )
log . Debugf ( "Loading secrets from %s" , input . Secretfile ( ) )
secrets := newSecrets ( input . secrets )
_ = readEnvs ( input . Secretfile ( ) , secrets )
2020-03-06 14:30:24 -06:00
2020-02-04 18:38:41 -06:00
planner , err := model . NewWorkflowPlanner ( input . WorkflowsPath ( ) )
2019-01-17 02:15:35 -06:00
if err != nil {
return err
}
2020-02-04 18:38:41 -06:00
// Determine the event name
var eventName string
if len ( args ) > 0 {
eventName = args [ 0 ]
2020-03-16 00:29:13 -05:00
} else if plan := planner . PlanEvent ( "push" ) ; plan != nil {
eventName = "push"
2020-02-17 12:11:16 -06:00
} else if events := planner . GetEvents ( ) ; len ( events ) > 0 {
2020-02-17 00:04:13 -06:00
// set default event type to first event
2020-02-04 18:38:41 -06:00
// this way user dont have to specify the event.
2020-02-10 18:53:14 -06:00
log . Debugf ( "Using detected workflow event: %s" , events [ 0 ] )
eventName = events [ 0 ]
2019-01-12 22:45:25 -06:00
}
2019-02-09 20:39:09 -06:00
2020-02-04 18:38:41 -06:00
// build the plan for this run
var plan * model . Plan
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 )
2019-02-15 10:34:19 -06:00
}
2020-02-04 18:38:41 -06:00
// check if we should just print the graph
if list , err := cmd . Flags ( ) . GetBool ( "list" ) ; err != nil {
return err
} else if list {
return drawGraph ( plan )
}
2019-02-15 10:34:19 -06:00
2020-02-04 18:38:41 -06:00
// run the plan
2020-02-07 00:17:58 -06:00
config := & runner . Config {
2020-05-12 02:14:56 -05:00
Actor : input . actor ,
2020-02-07 00:17:58 -06:00
EventName : eventName ,
EventPath : input . EventPath ( ) ,
ForcePull : input . forcePull ,
ReuseContainers : input . reuseContainers ,
Workdir : input . Workdir ( ) ,
2020-02-24 19:48:21 -06:00
BindWorkdir : input . bindWorkdir ,
2020-02-20 10:57:18 -06:00
LogOutput : ! input . noOutput ,
2020-04-17 12:04:40 -05:00
Env : envs ,
Secrets : secrets ,
2020-02-19 21:16:40 -06:00
Platforms : input . newPlatforms ( ) ,
2020-02-07 00:17:58 -06:00
}
runner , err := runner . New ( config )
if err != nil {
return err
}
ctx = common . WithDryrun ( ctx , input . dryrun )
if watch , err := cmd . Flags ( ) . GetBool ( "watch" ) ; err != nil {
return err
} else if watch {
return watchAndRun ( ctx , runner . NewPlanExecutor ( plan ) )
}
return runner . NewPlanExecutor ( plan ) ( ctx )
2019-02-09 20:39:09 -06:00
}
}
2020-02-07 00:17:58 -06:00
func watchAndRun ( ctx context . Context , fn common . Executor ) error {
2019-02-09 20:39:09 -06:00
recurse := true
checkIntervalInSeconds := 2
dir , err := os . Getwd ( )
if err != nil {
return err
}
var ignore * gitignore . GitIgnore
if _ , err := os . Stat ( filepath . Join ( dir , ".gitignore" ) ) ; ! os . IsNotExist ( err ) {
ignore , _ = gitignore . CompileIgnoreFile ( filepath . Join ( dir , ".gitignore" ) )
} else {
ignore = & gitignore . GitIgnore { }
}
folderWatcher := fswatch . NewFolderWatcher (
dir ,
recurse ,
ignore . MatchesPath ,
checkIntervalInSeconds ,
)
folderWatcher . Start ( )
go func ( ) {
for folderWatcher . IsRunning ( ) {
2020-02-07 00:17:58 -06:00
if err = fn ( ctx ) ; err != nil {
2019-02-12 22:32:54 -06:00
break
}
log . Debugf ( "Watching %s for changes" , dir )
2019-02-09 20:39:09 -06:00
for changes := range folderWatcher . ChangeDetails ( ) {
log . Debugf ( "%s" , changes . String ( ) )
2020-02-07 00:17:58 -06:00
if err = fn ( ctx ) ; err != nil {
2019-02-12 22:32:54 -06:00
break
}
2019-02-09 20:39:09 -06:00
log . Debugf ( "Watching %s for changes" , dir )
}
}
} ( )
<- ctx . Done ( )
folderWatcher . Stop ( )
2019-02-12 22:32:54 -06:00
return err
2019-01-12 22:45:25 -06:00
}