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"
"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"
2021-01-19 08:30:17 -06:00
"github.com/AlecAivazis/survey/v2"
2021-01-12 00:39:43 -06:00
"github.com/andreaskoch/go-fswatch"
2020-03-06 14:30:24 -06:00
"github.com/joho/godotenv"
2021-04-05 10:51:13 -05:00
"github.com/mitchellh/go-homedir"
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"
2021-01-19 08:30:17 -06:00
"github.com/nektos/act/pkg/model"
"github.com/nektos/act/pkg/runner"
2019-01-12 22:45:25 -06:00
)
// 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 {
2021-01-18 13:42:55 -06:00
Use : "act [event name to run]\nIf no event name passed, will default to \"on: push\"" ,
2021-03-30 14:26:25 -05:00
Short : "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly." ,
2019-01-17 02:15:35 -06:00
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" )
2020-10-12 12:26:22 -05:00
rootCmd . Flags ( ) . BoolP ( "graph" , "g" , false , "draw workflows" )
2020-02-04 18:38:41 -06:00
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)" )
2021-01-14 23:26:01 -06:00
rootCmd . Flags ( ) . StringArrayVarP ( & input . envs , "env" , "" , [ ] string { } , "env to make available to actions with optional value (e.g. --e myenv=foo or -s myenv)" )
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" )
2021-03-30 14:26:25 -05:00
rootCmd . Flags ( ) . BoolVarP ( & input . forcePull , "pull" , "p" , false , "pull docker image(s) even if already present" )
2021-01-18 13:42:55 -06:00
rootCmd . Flags ( ) . BoolVarP ( & input . autodetectEvent , "detect-event" , "" , false , "Use first event type from workflow as event that triggered the workflow" )
2020-02-17 23:51:49 -06:00
rootCmd . Flags ( ) . StringVarP ( & input . eventPath , "eventpath" , "e" , "" , "path to event JSON file" )
2020-09-02 09:56:44 -05:00
rootCmd . Flags ( ) . StringVar ( & input . defaultBranch , "defaultbranch" , "" , "the name of the main branch" )
2020-08-01 15:21:49 -05:00
rootCmd . Flags ( ) . BoolVar ( & input . privileged , "privileged" , false , "use privileged mode" )
2021-02-27 10:31:25 -06:00
rootCmd . Flags ( ) . StringVar ( & input . usernsMode , "userns" , "" , "user namespace to use" )
2021-05-03 09:37:20 -05:00
rootCmd . Flags ( ) . BoolVar ( & input . useGitIgnore , "use-gitignore" , true , "Controls whether paths specified in .gitignore should be copied into container" )
2020-05-12 02:14:56 -05:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . actor , "actor" , "a" , "nektos/act" , "user that triggered the event" )
2020-05-26 22:29:50 -05:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . workflowsPath , "workflows" , "W" , "./.github/workflows/" , "path to workflow file(s)" )
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" )
2021-01-19 08:31:46 -06:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . secretfile , "secret-file" , "" , ".secrets" , "file with list of secrets to read from (e.g. --secret-file .secrets)" )
2021-01-12 00:28:45 -06:00
rootCmd . PersistentFlags ( ) . BoolVarP ( & input . insecureSecrets , "insecure-secrets" , "" , false , "NOT RECOMMENDED! Doesn't hide secrets while printing logs." )
2020-04-17 12:04:40 -05:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . envfile , "env-file" , "" , ".env" , "environment file to read and use as env in the containers" )
2021-05-02 10:15:13 -05:00
rootCmd . PersistentFlags ( ) . StringVarP ( & input . containerArchitecture , "container-architecture" , "" , "" , "Architecture which should be used to run containers, e.g.: linux/amd64. If not specified, will use host default architecture. Requires Docker server API Version 1.41+. Ignored on earlier Docker server platforms." )
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 )
}
}
2021-04-05 10:51:13 -05:00
func configLocations ( ) [ ] string {
home , err := homedir . Dir ( )
if err != nil {
log . Fatal ( err )
}
// reference: https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html
var actrcXdg string
if xdg , ok := os . LookupEnv ( "XDG_CONFIG_HOME" ) ; ok && xdg != "" {
actrcXdg = filepath . Join ( xdg , ".actrc" )
} else {
actrcXdg = filepath . Join ( home , ".config" , ".actrc" )
}
return [ ] string {
filepath . Join ( home , ".actrc" ) ,
actrcXdg ,
2021-01-19 08:30:17 -06:00
filepath . Join ( "." , ".actrc" ) ,
}
2021-04-05 10:51:13 -05:00
}
func args ( ) [ ] string {
actrc := configLocations ( )
args := make ( [ ] string , 0 )
2021-01-19 08:30:17 -06:00
for _ , f := range actrc {
args = append ( args , readArgsFile ( f ) ... )
2020-02-25 00:11:33 -06:00
}
2021-04-05 10:51:13 -05:00
2020-02-25 00:11:33 -06:00
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
}
2021-01-12 00:39:43 -06:00
defer func ( ) {
err := f . Close ( )
if err != nil {
log . Errorf ( "Failed to close args file: %v" , err )
}
} ( )
2020-02-25 00:11:33 -06:00
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
}
2021-01-12 00:39:43 -06:00
func setupLogging ( cmd * cobra . Command , _ [ ] string ) {
2019-01-17 02:15:35 -06:00
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 )
2021-01-14 23:26:01 -06:00
if input . envs != nil {
for _ , envVar := range input . envs {
e := strings . SplitN ( envVar , ` = ` , 2 )
if len ( e ) == 2 {
envs [ e [ 0 ] ] = e [ 1 ]
} else {
envs [ e [ 0 ] ] = ""
}
}
}
2020-04-17 12:04:40 -05:00
_ = 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
2021-01-18 13:42:55 -06:00
events := planner . GetEvents ( )
if input . autodetectEvent && len ( events ) > 0 {
2021-01-29 08:40: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 ]
2021-01-18 13:42:55 -06:00
} else {
if len ( args ) > 0 {
eventName = args [ 0 ]
} else if plan := planner . PlanEvent ( "push" ) ; plan != nil {
eventName = "push"
}
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-10-12 12:26:22 -05:00
// check if we should just list the workflows
2020-02-04 18:38:41 -06:00
if list , err := cmd . Flags ( ) . GetBool ( "list" ) ; err != nil {
return err
2020-10-12 12:26:22 -05:00
} else if list {
return printList ( plan )
}
// check if we should just print the graph
if list , err := cmd . Flags ( ) . GetBool ( "graph" ) ; err != nil {
return err
2020-02-04 18:38:41 -06:00
} else if list {
return drawGraph ( plan )
}
2019-02-15 10:34:19 -06:00
2020-09-02 09:56:44 -05:00
// check to see if the main branch was defined
defaultbranch , err := cmd . Flags ( ) . GetString ( "defaultbranch" )
if err != nil {
return err
}
2021-04-05 10:51:13 -05:00
// Check if platforms flag is set, if not, run default image survey
2021-01-29 08:40:13 -06:00
if len ( input . platforms ) == 0 {
2021-04-05 10:51:13 -05:00
cfgFound := false
cfgLocations := configLocations ( )
for _ , v := range cfgLocations {
_ , err := os . Stat ( v )
if os . IsExist ( err ) {
cfgFound = true
2021-01-29 08:40:13 -06:00
}
2021-04-05 10:51:13 -05:00
}
if ! cfgFound && len ( cfgLocations ) > 0 {
if err := defaultImageSurvey ( cfgLocations [ 0 ] ) ; err != nil {
2021-01-29 08:40:13 -06:00
log . Fatal ( err )
}
2021-04-05 10:51:13 -05:00
input . platforms = readArgsFile ( cfgLocations [ 0 ] )
2021-01-29 08:40:13 -06:00
}
}
2020-02-04 18:38:41 -06:00
// run the plan
2020-02-07 00:17:58 -06:00
config := & runner . Config {
2021-03-28 23:08:40 -05:00
Actor : input . actor ,
EventName : eventName ,
EventPath : input . EventPath ( ) ,
DefaultBranch : defaultbranch ,
ForcePull : input . forcePull ,
ReuseContainers : input . reuseContainers ,
Workdir : input . Workdir ( ) ,
BindWorkdir : input . bindWorkdir ,
LogOutput : ! input . noOutput ,
Env : envs ,
Secrets : secrets ,
InsecureSecrets : input . insecureSecrets ,
Platforms : input . newPlatforms ( ) ,
Privileged : input . privileged ,
UsernsMode : input . usernsMode ,
ContainerArchitecture : input . containerArchitecture ,
2021-05-03 09:37:20 -05:00
UseGitIgnore : input . useGitIgnore ,
2020-02-07 00:17:58 -06:00
}
2021-01-12 00:39:43 -06:00
r , err := runner . New ( config )
2020-02-07 00:17:58 -06:00
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 {
2021-01-12 00:39:43 -06:00
return watchAndRun ( ctx , r . NewPlanExecutor ( plan ) )
2020-02-07 00:17:58 -06:00
}
2021-01-12 00:39:43 -06:00
return r . NewPlanExecutor ( plan ) ( ctx )
2019-02-09 20:39:09 -06:00
}
}
2021-04-05 10:51:13 -05:00
func defaultImageSurvey ( actrc string ) error {
var answer string
confirmation := & survey . Select {
Message : "Please choose the default image you want to use with act:\n\n - Large size image: +20GB Docker image, includes almost all tools used on GitHub Actions (IMPORTANT: currently only ubuntu-18.04 platform is available)\n - Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with all actions\n - Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions\n\nDefault image and other options can be changed manually in ~/.actrc (please refer to https://github.com/nektos/act#configuration for additional information about file structure)" ,
Help : "If you want to know why act asks you that, please go to https://github.com/nektos/act/issues/107" ,
Default : "Medium" ,
Options : [ ] string { "Large" , "Medium" , "Micro" } ,
}
err := survey . AskOne ( confirmation , & answer )
if err != nil {
return err
}
var option string
switch answer {
case "Large" :
option = "-P ubuntu-18.04=nektos/act-environments-ubuntu:18.04"
case "Medium" :
option = "-P ubuntu-latest=catthehacker/ubuntu:act-latest\n-P ubuntu-20.04=catthehacker/ubuntu:act-20.04\n-P ubuntu-18.04=catthehacker/ubuntu:act-18.04\nubuntu-16.04=catthehacker/ubuntu:act-16.04"
case "Micro" :
option = "-P ubuntu-latest=node:12.20.1-buster-slim\n-P ubuntu-20.04=node:12.20.1-buster-slim\n-P ubuntu-18.04=node:12.20.1-buster-slim\n-P ubuntu-16.04=node:12.20.1-stretch-slim"
}
f , err := os . Create ( actrc )
if err != nil {
return err
}
_ , err = f . WriteString ( option )
if err != nil {
_ = f . Close ( )
return err
}
err = f . Close ( )
if err != nil {
return err
}
return nil
}
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
}