2020-02-23 17:02:01 -06:00
package runner
import (
2021-08-03 12:39:56 -05:00
"archive/tar"
2020-02-23 17:02:01 -06:00
"context"
"fmt"
2021-09-27 12:33:14 -05:00
"io"
2021-03-30 14:26:25 -05:00
"io/ioutil"
2020-02-23 18:36:44 -06:00
"os"
2020-06-23 13:57:24 -05:00
"path"
2020-02-23 18:36:44 -06:00
"path/filepath"
2020-02-23 17:02:01 -06:00
"regexp"
2020-02-24 12:56:49 -06:00
"runtime"
2020-02-23 17:02:01 -06:00
"strings"
2021-02-23 11:47:06 -06:00
"github.com/kballard/go-shellquote"
2021-05-07 22:29:03 -05:00
"github.com/pkg/errors"
2021-03-28 23:08:40 -05:00
log "github.com/sirupsen/logrus"
2020-09-29 15:39:45 -05:00
2020-02-23 17:02:01 -06:00
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/container"
"github.com/nektos/act/pkg/model"
)
// StepContext contains info about current job
type StepContext struct {
RunContext * RunContext
Step * model . Step
Env map [ string ] string
Cmd [ ] string
2020-02-23 18:36:44 -06:00
Action * model . Action
2021-07-01 10:20:20 -05:00
Needs * model . Job
2020-02-23 17:02:01 -06:00
}
func ( sc * StepContext ) execJobContainer ( ) common . Executor {
return func ( ctx context . Context ) error {
2021-08-10 14:40:20 -05:00
return sc . RunContext . execJobContainer ( sc . Cmd , sc . Env , "" , sc . Step . WorkingDirectory ) ( ctx )
2020-02-23 17:02:01 -06:00
}
}
2021-01-23 10:07:28 -06:00
type formatError string
func ( e formatError ) Error ( ) string {
return fmt . Sprintf ( "Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format." , string ( e ) )
}
2020-02-23 17:02:01 -06:00
// Executor for a step context
2021-12-22 11:29:43 -06:00
func ( sc * StepContext ) Executor ( ctx context . Context ) common . Executor {
2020-02-23 17:02:01 -06:00
rc := sc . RunContext
step := sc . Step
switch step . Type ( ) {
case model . StepTypeRun :
return common . NewPipelineExecutor (
2021-12-22 00:37:16 -06:00
sc . setupShellCommandExecutor ( ) ,
2020-02-23 17:02:01 -06:00
sc . execJobContainer ( ) ,
)
case model . StepTypeUsesDockerURL :
return common . NewPipelineExecutor (
sc . runUsesContainer ( ) ,
)
2020-02-23 18:36:44 -06:00
case model . StepTypeUsesActionLocal :
2020-02-24 00:34:48 -06:00
actionDir := filepath . Join ( rc . Config . Workdir , step . Uses )
2022-02-08 14:17:59 -06:00
localReader := func ( ctx context . Context ) actionyamlReader {
_ , cpath := sc . getContainerActionPaths ( sc . Step , path . Join ( actionDir , "" ) , sc . RunContext )
return func ( filename string ) ( io . Reader , io . Closer , error ) {
tars , err := sc . RunContext . JobContainer . GetContainerArchive ( ctx , path . Join ( cpath , filename ) )
if err != nil {
return nil , nil , os . ErrNotExist
}
treader := tar . NewReader ( tars )
if _ , err := treader . Next ( ) ; err != nil {
return nil , nil , os . ErrNotExist
}
return treader , tars , nil
}
}
2020-02-23 18:36:44 -06:00
return common . NewPipelineExecutor (
2022-02-08 14:17:59 -06:00
sc . setupAction ( actionDir , "" , localReader ) ,
2021-12-22 13:19:50 -06:00
sc . runAction ( actionDir , "" , "" , "" , true ) ,
2020-02-24 00:34:48 -06:00
)
case model . StepTypeUsesActionRemote :
remoteAction := newRemoteAction ( step . Uses )
2021-01-23 10:07:28 -06:00
if remoteAction == nil {
return common . NewErrorExecutor ( formatError ( step . Uses ) )
2021-01-21 08:00:33 -06:00
}
2021-05-05 11:42:34 -05:00
remoteAction . URL = rc . Config . GitHubInstance
github := rc . getGithubContext ( )
2021-12-22 13:52:09 -06:00
if remoteAction . IsCheckout ( ) && isLocalCheckout ( github , step ) {
2020-02-24 00:34:48 -06:00
return func ( ctx context . Context ) error {
2021-05-10 10:12:57 -05:00
common . Logger ( ctx ) . Debugf ( "Skipping local actions/checkout because workdir was already copied" )
2020-02-24 00:34:48 -06:00
return nil
}
}
2020-02-24 18:38:49 -06:00
actionDir := fmt . Sprintf ( "%s/%s" , rc . ActionCacheDir ( ) , strings . ReplaceAll ( step . Uses , "/" , "-" ) )
2021-05-07 22:29:03 -05:00
gitClone := common . NewGitCloneExecutor ( common . NewGitCloneExecutorInput {
URL : remoteAction . CloneURL ( ) ,
Ref : remoteAction . Ref ,
Dir : actionDir ,
Token : github . Token ,
} )
2021-05-18 01:14:49 -05:00
var ntErr common . Executor
2021-12-22 11:29:43 -06:00
if err := gitClone ( ctx ) ; err != nil {
2021-05-18 01:14:49 -05:00
if err . Error ( ) == "short SHA references are not supported" {
err = errors . Cause ( err )
return common . NewErrorExecutor ( fmt . Errorf ( "Unable to resolve action `%s`, the provided ref `%s` is the shortened version of a commit SHA, which is not supported. Please use the full commit SHA `%s` instead" , step . Uses , remoteAction . Ref , err . Error ( ) ) )
} else if err . Error ( ) != "some refs were not updated" {
return common . NewErrorExecutor ( err )
} else {
ntErr = common . NewInfoExecutor ( "Non-terminating error while running 'git clone': %v" , err )
}
2021-05-07 22:29:03 -05:00
}
2022-02-08 14:17:59 -06:00
remoteReader := func ( ctx context . Context ) actionyamlReader {
return func ( filename string ) ( io . Reader , io . Closer , error ) {
f , err := os . Open ( filepath . Join ( actionDir , remoteAction . Path , filename ) )
return f , f , err
}
}
2020-02-24 00:34:48 -06:00
return common . NewPipelineExecutor (
2021-05-18 01:14:49 -05:00
ntErr ,
2022-02-08 14:17:59 -06:00
sc . setupAction ( actionDir , remoteAction . Path , remoteReader ) ,
2021-12-22 13:19:50 -06:00
sc . runAction ( actionDir , remoteAction . Path , remoteAction . Repo , remoteAction . Ref , false ) ,
2020-02-23 18:36:44 -06:00
)
2021-04-01 13:36:41 -05:00
case model . StepTypeInvalid :
return common . NewErrorExecutor ( fmt . Errorf ( "Invalid run/uses syntax for job:%s step:%+v" , rc . Run , step ) )
2020-02-23 17:02:01 -06:00
}
return common . NewErrorExecutor ( fmt . Errorf ( "Unable to determine how to run job:%s step:%+v" , rc . Run , step ) )
}
2021-01-12 18:02:54 -06:00
func ( sc * StepContext ) mergeEnv ( ) map [ string ] string {
2020-02-23 17:02:01 -06:00
rc := sc . RunContext
job := rc . Run . Job ( )
2021-01-12 18:02:54 -06:00
var env map [ string ] string
c := job . Container ( )
if c != nil {
2021-09-27 20:18:59 -05:00
env = mergeMaps ( rc . GetEnv ( ) , c . Env )
2021-01-12 18:02:54 -06:00
} else {
2021-09-27 20:18:59 -05:00
env = rc . GetEnv ( )
2021-01-12 18:02:54 -06:00
}
2021-06-10 18:12:05 -05:00
if env [ "PATH" ] == "" {
env [ "PATH" ] = ` /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin `
}
if rc . ExtraPath != nil && len ( rc . ExtraPath ) > 0 {
p := env [ "PATH" ]
2021-05-05 18:11:43 -05:00
env [ "PATH" ] = strings . Join ( rc . ExtraPath , ` : ` )
2021-06-10 18:12:05 -05:00
env [ "PATH" ] += ` : ` + p
2021-01-12 18:02:54 -06:00
}
sc . Env = rc . withGithubEnv ( env )
return env
}
2020-11-02 15:56:20 -06:00
2021-01-12 18:02:54 -06:00
func ( sc * StepContext ) interpolateEnv ( exprEval ExpressionEvaluator ) {
for k , v := range sc . Env {
sc . Env [ k ] = exprEval . Interpolate ( v )
}
}
2021-12-08 14:57:42 -06:00
func ( sc * StepContext ) isEnabled ( ctx context . Context ) ( bool , error ) {
runStep , err := EvalBool ( sc . NewExpressionEvaluator ( ) , sc . Step . If . Value )
if err != nil {
2022-02-25 12:39:50 -06:00
return false , fmt . Errorf ( " \u274C Error in if-expression: \"if: %s\" (%s)" , sc . Step . If . Value , err )
2021-12-08 14:57:42 -06:00
}
return runStep , nil
}
2021-01-12 18:02:54 -06:00
func ( sc * StepContext ) setupEnv ( ctx context . Context ) ( ExpressionEvaluator , error ) {
rc := sc . RunContext
sc . Env = sc . mergeEnv ( )
if sc . Env != nil {
2021-09-27 14:01:14 -05:00
err := rc . JobContainer . UpdateFromImageEnv ( & sc . Env ) ( ctx )
if err != nil {
return nil , err
}
err = rc . JobContainer . UpdateFromEnv ( sc . Env [ "GITHUB_ENV" ] , & sc . Env ) ( ctx )
2021-01-12 18:02:54 -06:00
if err != nil {
return nil , err
2020-02-23 17:02:01 -06:00
}
2021-05-06 08:30:12 -05:00
err = rc . JobContainer . UpdateFromPath ( & sc . Env ) ( ctx )
if err != nil {
return nil , err
}
2020-02-23 17:02:01 -06:00
}
2021-09-27 20:18:59 -05:00
sc . Env = mergeMaps ( sc . Env , sc . Step . GetEnv ( ) ) // step env should not be overwritten
2021-01-12 18:02:54 -06:00
evaluator := sc . NewExpressionEvaluator ( )
sc . interpolateEnv ( evaluator )
2021-03-28 23:45:07 -05:00
common . Logger ( ctx ) . Debugf ( "setupEnv => %v" , sc . Env )
2021-01-12 18:02:54 -06:00
return evaluator , nil
2020-02-23 17:02:01 -06:00
}
2021-12-22 00:37:16 -06:00
func ( sc * StepContext ) setupWorkingDirectory ( ) {
2020-02-23 17:02:01 -06:00
rc := sc . RunContext
step := sc . Step
2021-12-22 00:37:16 -06:00
if step . WorkingDirectory == "" {
step . WorkingDirectory = rc . Run . Job ( ) . Defaults . Run . WorkingDirectory
}
2020-03-14 02:00:37 -05:00
2021-12-22 00:37:16 -06:00
// jobs can receive context values, so we interpolate
step . WorkingDirectory = rc . ExprEval . Interpolate ( step . WorkingDirectory )
2020-02-23 17:02:01 -06:00
2021-12-22 00:37:16 -06:00
// but top level keys in workflow file like `defaults` or `env` can't
if step . WorkingDirectory == "" {
step . WorkingDirectory = rc . Run . Workflow . Defaults . Run . WorkingDirectory
}
}
2021-03-29 12:06:51 -05:00
2021-12-22 00:37:16 -06:00
func ( sc * StepContext ) setupShell ( ) {
rc := sc . RunContext
step := sc . Step
2021-03-29 12:06:51 -05:00
2021-12-22 00:37:16 -06:00
if step . Shell == "" {
step . Shell = rc . Run . Job ( ) . Defaults . Run . Shell
}
2020-08-28 13:52:25 -05:00
2021-12-22 00:37:16 -06:00
step . Shell = rc . ExprEval . Interpolate ( step . Shell )
2021-08-10 14:40:20 -05:00
2021-12-22 00:37:16 -06:00
if step . Shell == "" {
step . Shell = rc . Run . Workflow . Defaults . Run . Shell
}
2021-03-29 12:06:51 -05:00
2021-12-22 00:37:16 -06:00
// current GitHub Runner behaviour is that default is `sh`,
// but if it's not container it validates with `which` command
// if `bash` is available, and provides `bash` if it is
// for now I'm going to leave below logic, will address it in different PR
// https://github.com/actions/runner/blob/9a829995e02d2db64efb939dc2f283002595d4d9/src/Runner.Worker/Handlers/ScriptHandler.cs#L87-L91
if rc . Run . Job ( ) . Container ( ) != nil {
if rc . Run . Job ( ) . Container ( ) . Image != "" && step . Shell == "" {
step . Shell = "sh"
2021-08-10 14:40:20 -05:00
}
2021-12-22 00:37:16 -06:00
}
}
2021-12-22 13:19:50 -06:00
func getScriptName ( rc * RunContext , step * model . Step ) string {
scriptName := step . ID
for rcs := rc ; rcs . Parent != nil ; rcs = rcs . Parent {
scriptName = fmt . Sprintf ( "%s-composite-%s" , rcs . Parent . CurrentStep , scriptName )
}
return fmt . Sprintf ( "workflow/%s" , scriptName )
}
2021-12-22 00:37:16 -06:00
// TODO: Currently we just ignore top level keys, BUT we should return proper error on them
// BUTx2 I leave this for when we rewrite act to use actionlint for workflow validation
// so we return proper errors before any execution or spawning containers
// it will error anyway with:
// OCI runtime exec failed: exec failed: container_linux.go:380: starting container process caused: exec: "${{": executable file not found in $PATH: unknown
func ( sc * StepContext ) setupShellCommand ( ) ( name , script string , err error ) {
sc . setupShell ( )
sc . setupWorkingDirectory ( )
step := sc . Step
script = sc . RunContext . ExprEval . Interpolate ( step . Run )
scCmd := step . ShellCommand ( )
2021-12-22 13:19:50 -06:00
name = getScriptName ( sc . RunContext , step )
2021-12-22 00:37:16 -06:00
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L47-L64
// Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L19-L27
runPrepend := ""
runAppend := ""
switch step . Shell {
case "bash" , "sh" :
name += ".sh"
case "pwsh" , "powershell" :
name += ".ps1"
runPrepend = "$ErrorActionPreference = 'stop'"
runAppend = "if ((Test-Path -LiteralPath variable:/LASTEXITCODE)) { exit $LASTEXITCODE }"
case "cmd" :
name += ".cmd"
runPrepend = "@echo off"
case "python" :
name += ".py"
}
script = fmt . Sprintf ( "%s\n%s\n%s" , runPrepend , script , runAppend )
2021-08-10 14:40:20 -05:00
2021-12-22 00:37:16 -06:00
log . Debugf ( "Wrote command \n%s\n to '%s'" , script , name )
scriptPath := fmt . Sprintf ( "%s/%s" , ActPath , name )
sc . Cmd , err = shellquote . Split ( strings . Replace ( scCmd , ` { 0} ` , scriptPath , 1 ) )
return name , script , err
}
func ( sc * StepContext ) setupShellCommandExecutor ( ) common . Executor {
rc := sc . RunContext
return func ( ctx context . Context ) error {
scriptName , script , err := sc . setupShellCommand ( )
if err != nil {
return err
}
2021-08-10 14:40:20 -05:00
2021-12-22 00:37:16 -06:00
return rc . JobContainer . Copy ( ActPath , & container . FileEntry {
2020-02-23 17:02:01 -06:00
Name : scriptName ,
2020-08-08 15:31:26 -05:00
Mode : 0755 ,
2021-12-22 00:37:16 -06:00
Body : script ,
2020-02-23 17:02:01 -06:00
} ) ( ctx )
}
}
func ( sc * StepContext ) newStepContainer ( ctx context . Context , image string , cmd [ ] string , entrypoint [ ] string ) container . Container {
rc := sc . RunContext
step := sc . Step
rawLogger := common . Logger ( ctx ) . WithField ( "raw_output" , true )
2020-02-24 14:48:12 -06:00
logWriter := common . NewLineWriter ( rc . commandHandler ( ctx ) , func ( s string ) bool {
2020-02-23 17:02:01 -06:00
if rc . Config . LogOutput {
2020-06-24 09:05:05 -05:00
rawLogger . Infof ( "%s" , s )
2020-02-23 17:02:01 -06:00
} else {
2020-06-24 09:05:05 -05:00
rawLogger . Debugf ( "%s" , s )
2020-02-23 17:02:01 -06:00
}
2020-02-24 14:48:12 -06:00
return true
2020-02-23 17:02:01 -06:00
} )
envList := make ( [ ] string , 0 )
for k , v := range sc . Env {
envList = append ( envList , fmt . Sprintf ( "%s=%s" , k , v ) )
}
2020-02-24 12:56:49 -06:00
2020-02-25 10:52:05 -06:00
envList = append ( envList , fmt . Sprintf ( "%s=%s" , "RUNNER_TOOL_CACHE" , "/opt/hostedtoolcache" ) )
2020-04-23 01:57:36 -05:00
envList = append ( envList , fmt . Sprintf ( "%s=%s" , "RUNNER_OS" , "Linux" ) )
envList = append ( envList , fmt . Sprintf ( "%s=%s" , "RUNNER_TEMP" , "/tmp" ) )
2020-02-24 12:56:49 -06:00
2021-05-04 16:50:35 -05:00
binds , mounts := rc . GetBindsAndMounts ( )
2020-02-24 19:48:21 -06:00
2020-02-23 17:02:01 -06:00
stepContainer := container . NewContainer ( & container . NewContainerInput {
2021-05-04 16:50:35 -05:00
Cmd : cmd ,
Entrypoint : entrypoint ,
WorkingDir : rc . Config . ContainerWorkdir ( ) ,
Image : image ,
2021-05-05 11:37:17 -05:00
Username : rc . Config . Secrets [ "DOCKER_USERNAME" ] ,
Password : rc . Config . Secrets [ "DOCKER_PASSWORD" ] ,
2021-05-04 16:50:35 -05:00
Name : createContainerName ( rc . jobContainerName ( ) , step . ID ) ,
Env : envList ,
Mounts : mounts ,
2020-03-09 19:43:24 -05:00
NetworkMode : fmt . Sprintf ( "container:%s" , rc . jobContainerName ( ) ) ,
Binds : binds ,
Stdout : logWriter ,
Stderr : logWriter ,
2020-08-01 15:21:49 -05:00
Privileged : rc . Config . Privileged ,
2021-02-27 10:31:25 -06:00
UsernsMode : rc . Config . UsernsMode ,
2021-03-28 23:08:40 -05:00
Platform : rc . Config . ContainerArchitecture ,
2020-02-23 17:02:01 -06:00
} )
return stepContainer
}
2021-08-10 14:40:20 -05:00
2020-02-23 17:02:01 -06:00
func ( sc * StepContext ) runUsesContainer ( ) common . Executor {
rc := sc . RunContext
step := sc . Step
return func ( ctx context . Context ) error {
image := strings . TrimPrefix ( step . Uses , "docker://" )
2021-12-22 13:19:50 -06:00
eval := sc . RunContext . NewExpressionEvaluator ( )
cmd , err := shellquote . Split ( eval . Interpolate ( step . With [ "args" ] ) )
2021-02-23 11:47:06 -06:00
if err != nil {
return err
}
2021-12-22 13:19:50 -06:00
entrypoint := strings . Fields ( eval . Interpolate ( step . With [ "entrypoint" ] ) )
2020-02-23 17:02:01 -06:00
stepContainer := sc . newStepContainer ( ctx , image , cmd , entrypoint )
return common . NewPipelineExecutor (
stepContainer . Pull ( rc . Config . ForcePull ) ,
stepContainer . Remove ( ) . IfBool ( ! rc . Config . ReuseContainers ) ,
2021-06-04 11:06:59 -05:00
stepContainer . Create ( rc . Config . ContainerCapAdd , rc . Config . ContainerCapDrop ) ,
2020-02-23 17:02:01 -06:00
stepContainer . Start ( true ) ,
) . Finally (
stepContainer . Remove ( ) . IfBool ( ! rc . Config . ReuseContainers ) ,
2021-10-24 11:50:43 -05:00
) . Finally ( stepContainer . Close ( ) ) ( ctx )
2020-02-23 17:02:01 -06:00
}
}
2022-02-08 14:17:59 -06:00
func ( sc * StepContext ) setupAction ( actionDir string , actionPath string , reader func ( context . Context ) actionyamlReader ) common . Executor {
2020-02-23 17:02:01 -06:00
return func ( ctx context . Context ) error {
2022-02-08 14:17:59 -06:00
action , err := sc . readAction ( sc . Step , actionDir , actionPath , reader ( ctx ) , ioutil . WriteFile )
sc . Action = action
2021-08-03 12:39:56 -05:00
log . Debugf ( "Read action %v from '%s'" , sc . Action , "Unknown" )
2020-02-23 18:36:44 -06:00
return err
}
}
2020-02-23 17:02:01 -06:00
2020-10-09 00:30:50 -05:00
func getOsSafeRelativePath ( s , prefix string ) string {
actionName := strings . TrimPrefix ( s , prefix )
if runtime . GOOS == "windows" {
actionName = strings . ReplaceAll ( actionName , "\\" , "/" )
}
actionName = strings . TrimPrefix ( actionName , "/" )
return actionName
}
func ( sc * StepContext ) getContainerActionPaths ( step * model . Step , actionDir string , rc * RunContext ) ( string , string ) {
actionName := ""
containerActionDir := "."
2021-06-10 10:28:23 -05:00
if step . Type ( ) != model . StepTypeUsesActionRemote {
2020-10-09 00:30:50 -05:00
actionName = getOsSafeRelativePath ( actionDir , rc . Config . Workdir )
2021-06-10 10:28:23 -05:00
containerActionDir = rc . Config . ContainerWorkdir ( ) + "/" + actionName
actionName = "./" + actionName
2020-10-09 00:30:50 -05:00
} else if step . Type ( ) == model . StepTypeUsesActionRemote {
actionName = getOsSafeRelativePath ( actionDir , rc . ActionCacheDir ( ) )
2021-05-24 12:09:03 -05:00
containerActionDir = ActPath + "/actions/" + actionName
2020-10-09 00:30:50 -05:00
}
if actionName == "" {
actionName = filepath . Base ( actionDir )
if runtime . GOOS == "windows" {
actionName = strings . ReplaceAll ( actionName , "\\" , "/" )
}
}
return actionName , containerActionDir
}
2021-12-22 13:19:50 -06:00
func ( sc * StepContext ) runAction ( actionDir string , actionPath string , actionRepository string , actionRef string , localAction bool ) common . Executor {
2020-02-23 18:36:44 -06:00
rc := sc . RunContext
step := sc . Step
return func ( ctx context . Context ) error {
2021-12-22 13:19:50 -06:00
// Backup the parent composite action path and restore it on continue
parentActionPath := rc . ActionPath
parentActionRepository := rc . ActionRepository
parentActionRef := rc . ActionRef
defer func ( ) {
rc . ActionPath = parentActionPath
rc . ActionRef = parentActionRef
rc . ActionRepository = parentActionRepository
} ( )
rc . ActionRef = actionRef
rc . ActionRepository = actionRepository
2020-02-23 18:36:44 -06:00
action := sc . Action
log . Debugf ( "About to run action %v" , action )
2021-05-06 08:30:12 -05:00
sc . populateEnvsFromInput ( action , rc )
2021-05-04 16:50:35 -05:00
actionLocation := ""
if actionPath != "" {
actionLocation = path . Join ( actionDir , actionPath )
} else {
actionLocation = actionDir
}
actionName , containerActionDir := sc . getContainerActionPaths ( step , actionLocation , rc )
2020-03-06 12:17:20 -06:00
2021-08-11 00:21:52 -05:00
log . Debugf ( "type=%v actionDir=%s actionPath=%s workdir=%s actionCacheDir=%s actionName=%s containerActionDir=%s" , step . Type ( ) , actionDir , actionPath , rc . Config . Workdir , rc . ActionCacheDir ( ) , actionName , containerActionDir )
2020-02-24 18:38:49 -06:00
2021-05-03 16:57:46 -05:00
maybeCopyToActionDir := func ( ) error {
2021-12-22 13:19:50 -06:00
rc . ActionPath = containerActionDir
2021-05-03 16:57:46 -05:00
if step . Type ( ) != model . StepTypeUsesActionRemote {
2021-06-10 10:28:23 -05:00
return nil
2021-05-03 16:57:46 -05:00
}
2021-08-11 00:21:52 -05:00
if err := removeGitIgnore ( actionDir ) ; err != nil {
2021-05-03 16:57:46 -05:00
return err
}
2021-08-11 00:21:52 -05:00
var containerActionDirCopy string
containerActionDirCopy = strings . TrimSuffix ( containerActionDir , actionPath )
log . Debug ( containerActionDirCopy )
if ! strings . HasSuffix ( containerActionDirCopy , ` / ` ) {
containerActionDirCopy += ` / `
}
return rc . JobContainer . CopyDir ( containerActionDirCopy , actionDir + "/" , rc . Config . UseGitIgnore ) ( ctx )
2021-05-03 16:57:46 -05:00
}
2020-02-23 17:02:01 -06:00
switch action . Runs . Using {
2021-12-22 13:34:18 -06:00
case model . ActionRunsUsingNode12 , model . ActionRunsUsingNode16 :
2021-08-11 00:21:52 -05:00
if err := maybeCopyToActionDir ( ) ; err != nil {
2021-05-03 16:57:46 -05:00
return err
2020-02-24 18:38:49 -06:00
}
2021-05-04 16:50:35 -05:00
containerArgs := [ ] string { "node" , path . Join ( containerActionDir , action . Runs . Main ) }
2020-09-29 15:39:45 -05:00
log . Debugf ( "executing remote job container: %s" , containerArgs )
2021-08-10 14:40:20 -05:00
return rc . execJobContainer ( containerArgs , sc . Env , "" , "" ) ( ctx )
2020-02-23 17:02:01 -06:00
case model . ActionRunsUsingDocker :
2021-08-03 12:39:56 -05:00
return sc . execAsDocker ( ctx , action , actionName , containerActionDir , actionLocation , rc , step , localAction )
2021-05-06 08:30:12 -05:00
case model . ActionRunsUsingComposite :
return sc . execAsComposite ( ctx , step , actionDir , rc , containerActionDir , actionName , actionPath , action , maybeCopyToActionDir )
default :
return fmt . Errorf ( fmt . Sprintf ( "The runs.using key must be one of: %v, got %s" , [ ] string {
model . ActionRunsUsingDocker ,
model . ActionRunsUsingNode12 ,
2021-12-22 13:34:18 -06:00
model . ActionRunsUsingNode16 ,
2021-05-06 08:30:12 -05:00
model . ActionRunsUsingComposite ,
} , action . Runs . Using ) )
}
}
}
2021-03-28 23:08:40 -05:00
2021-12-22 13:19:50 -06:00
func ( sc * StepContext ) evalDockerArgs ( action * model . Action , cmd * [ ] string ) {
rc := sc . RunContext
step := sc . Step
oldInputs := rc . Inputs
defer func ( ) {
rc . Inputs = oldInputs
} ( )
inputs := make ( map [ string ] interface { } )
eval := sc . RunContext . NewExpressionEvaluator ( )
// Set Defaults
for k , input := range action . Inputs {
inputs [ k ] = eval . Interpolate ( input . Default )
}
if step . With != nil {
for k , v := range step . With {
inputs [ k ] = eval . Interpolate ( v )
}
}
rc . Inputs = inputs
stepEE := sc . NewExpressionEvaluator ( )
for i , v := range * cmd {
( * cmd ) [ i ] = stepEE . Interpolate ( v )
}
sc . Env = mergeMaps ( sc . Env , action . Runs . Env )
ee := sc . NewExpressionEvaluator ( )
for k , v := range sc . Env {
sc . Env [ k ] = ee . Interpolate ( v )
}
}
2021-11-13 13:43:31 -06:00
// TODO: break out parts of function to reduce complexicity
// nolint:gocyclo
2021-08-03 12:39:56 -05:00
func ( sc * StepContext ) execAsDocker ( ctx context . Context , action * model . Action , actionName string , containerLocation string , actionLocation string , rc * RunContext , step * model . Step , localAction bool ) error {
2021-05-06 08:30:12 -05:00
var prepImage common . Executor
var image string
if strings . HasPrefix ( action . Runs . Image , "docker://" ) {
image = strings . TrimPrefix ( action . Runs . Image , "docker://" )
} else {
2021-08-09 13:16:31 -05:00
// "-dockeraction" enshures that "./", "./test " won't get converted to "act-:latest", "act-test-:latest" which are invalid docker image names
image = fmt . Sprintf ( "%s-dockeraction:%s" , regexp . MustCompile ( "[^a-zA-Z0-9]" ) . ReplaceAllString ( actionName , "-" ) , "latest" )
2021-05-06 08:30:12 -05:00
image = fmt . Sprintf ( "act-%s" , strings . TrimLeft ( image , "-" ) )
image = strings . ToLower ( image )
2021-08-03 12:39:56 -05:00
basedir := actionLocation
if localAction {
basedir = containerLocation
}
contextDir := filepath . Join ( basedir , action . Runs . Main )
2021-05-02 10:15:13 -05:00
2021-05-06 08:30:12 -05:00
anyArchExists , err := container . ImageExistsLocally ( ctx , image , "any" )
if err != nil {
return err
}
2021-03-28 23:08:40 -05:00
2021-05-06 08:30:12 -05:00
correctArchExists , err := container . ImageExistsLocally ( ctx , image , rc . Config . ContainerArchitecture )
if err != nil {
return err
}
2020-02-23 18:36:44 -06:00
2021-05-06 08:30:12 -05:00
if anyArchExists && ! correctArchExists {
wasRemoved , err := container . RemoveImage ( ctx , image , true , true )
2021-02-23 11:47:06 -06:00
if err != nil {
return err
}
2021-05-06 08:30:12 -05:00
if ! wasRemoved {
return fmt . Errorf ( "failed to remove image '%s'" , image )
2020-02-23 18:36:44 -06:00
}
2021-05-06 08:30:12 -05:00
}
2021-04-02 15:40:44 -05:00
2021-11-24 09:51:37 -06:00
if ! correctArchExists || rc . Config . ForceRebuild {
2021-05-06 08:30:12 -05:00
log . Debugf ( "image '%s' for architecture '%s' will be built from context '%s" , image , rc . Config . ContainerArchitecture , contextDir )
2021-11-12 14:48:10 -06:00
var actionContainer container . Container
2021-08-03 12:39:56 -05:00
if localAction {
actionContainer = sc . RunContext . JobContainer
}
2021-05-06 08:30:12 -05:00
prepImage = container . NewDockerBuildExecutor ( container . NewDockerBuildExecutorInput {
ContextDir : contextDir ,
ImageTag : image ,
2021-08-03 12:39:56 -05:00
Container : actionContainer ,
2021-05-06 08:30:12 -05:00
Platform : rc . Config . ContainerArchitecture ,
} )
} else {
log . Debugf ( "image '%s' for architecture '%s' already exists" , image , rc . Config . ContainerArchitecture )
}
}
2021-12-22 13:19:50 -06:00
eval := sc . NewExpressionEvaluator ( )
cmd , err := shellquote . Split ( eval . Interpolate ( step . With [ "args" ] ) )
2021-05-06 08:30:12 -05:00
if err != nil {
return err
}
if len ( cmd ) == 0 {
cmd = action . Runs . Args
2021-12-22 13:19:50 -06:00
sc . evalDockerArgs ( action , & cmd )
2021-05-06 08:30:12 -05:00
}
2021-12-22 13:19:50 -06:00
entrypoint := strings . Fields ( eval . Interpolate ( step . With [ "entrypoint" ] ) )
2021-05-06 08:30:12 -05:00
if len ( entrypoint ) == 0 {
2021-11-13 13:43:31 -06:00
if action . Runs . Entrypoint != "" {
entrypoint , err = shellquote . Split ( action . Runs . Entrypoint )
if err != nil {
return err
}
} else {
entrypoint = nil
}
2021-05-06 08:30:12 -05:00
}
stepContainer := sc . newStepContainer ( ctx , image , cmd , entrypoint )
return common . NewPipelineExecutor (
prepImage ,
stepContainer . Pull ( rc . Config . ForcePull ) ,
stepContainer . Remove ( ) . IfBool ( ! rc . Config . ReuseContainers ) ,
2021-06-04 11:06:59 -05:00
stepContainer . Create ( rc . Config . ContainerCapAdd , rc . Config . ContainerCapDrop ) ,
2021-05-06 08:30:12 -05:00
stepContainer . Start ( true ) ,
) . Finally (
stepContainer . Remove ( ) . IfBool ( ! rc . Config . ReuseContainers ) ,
2021-10-24 11:50:43 -05:00
) . Finally ( stepContainer . Close ( ) ) ( ctx )
2021-05-06 08:30:12 -05:00
}
func ( sc * StepContext ) execAsComposite ( ctx context . Context , step * model . Step , _ string , rc * RunContext , containerActionDir string , actionName string , _ string , action * model . Action , maybeCopyToActionDir func ( ) error ) error {
err := maybeCopyToActionDir ( )
if err != nil {
return err
}
2021-12-22 13:19:50 -06:00
// Disable some features of composite actions, only for feature parity with github
2021-05-06 08:30:12 -05:00
for _ , compositeStep := range action . Runs . Steps {
2021-12-22 13:19:50 -06:00
if err := compositeStep . Validate ( rc . Config . CompositeRestrictions ) ; err != nil {
2021-05-06 08:30:12 -05:00
return err
}
2021-12-22 13:19:50 -06:00
}
inputs := make ( map [ string ] interface { } )
eval := sc . RunContext . NewExpressionEvaluator ( )
// Set Defaults
for k , input := range action . Inputs {
inputs [ k ] = eval . Interpolate ( input . Default )
}
if step . With != nil {
for k , v := range step . With {
inputs [ k ] = eval . Interpolate ( v )
2021-05-06 08:30:12 -05:00
}
2021-12-22 13:19:50 -06:00
}
// Doesn't work with the command processor has a pointer to the original rc
// compositerc := rc.Clone()
// Workaround start
backup := * rc
defer func ( ) { * rc = backup } ( )
* rc = * rc . Clone ( )
scriptName := backup . CurrentStep
for rcs := & backup ; rcs . Parent != nil ; rcs = rcs . Parent {
scriptName = fmt . Sprintf ( "%s-composite-%s" , rcs . Parent . CurrentStep , scriptName )
}
compositerc := rc
compositerc . Parent = & RunContext {
CurrentStep : scriptName ,
}
// Workaround end
compositerc . Composite = action
envToEvaluate := mergeMaps ( compositerc . Env , step . Environment ( ) )
compositerc . Env = make ( map [ string ] string )
// origEnvMap: is used to pass env changes back to parent runcontext
origEnvMap := make ( map [ string ] string )
for k , v := range envToEvaluate {
ev := eval . Interpolate ( v )
origEnvMap [ k ] = ev
compositerc . Env [ k ] = ev
}
compositerc . Inputs = inputs
compositerc . ExprEval = compositerc . NewExpressionEvaluator ( )
err = compositerc . CompositeExecutor ( ) ( ctx )
2021-04-02 15:40:44 -05:00
2021-12-22 13:19:50 -06:00
// Map outputs to parent rc
eval = ( & StepContext {
Env : compositerc . Env ,
RunContext : compositerc ,
} ) . NewExpressionEvaluator ( )
for outputName , output := range action . Outputs {
backup . setOutput ( ctx , map [ string ] string {
"name" : outputName ,
} , eval . Interpolate ( output . Value ) )
}
2022-03-02 02:29:34 -06:00
backup . Masks = append ( backup . Masks , compositerc . Masks ... )
2021-12-22 13:19:50 -06:00
// Test if evaluated parent env was altered by this composite step
// Known Issues:
// - you try to set an env variable to the same value as a scoped step env, will be discared
for k , v := range compositerc . Env {
if ov , ok := origEnvMap [ k ] ; ! ok || ov != v {
backup . Env [ k ] = v
2021-05-06 08:30:12 -05:00
}
}
2022-01-21 10:08:30 -06:00
return err
2021-05-06 08:30:12 -05:00
}
func ( sc * StepContext ) populateEnvsFromInput ( action * model . Action , rc * RunContext ) {
for inputID , input := range action . Inputs {
envKey := regexp . MustCompile ( "[^A-Z0-9-]" ) . ReplaceAllString ( strings . ToUpper ( inputID ) , "_" )
envKey = fmt . Sprintf ( "INPUT_%s" , envKey )
if _ , ok := sc . Env [ envKey ] ; ! ok {
sc . Env [ envKey ] = rc . ExprEval . Interpolate ( input . Default )
2020-02-23 17:02:01 -06:00
}
}
}
type remoteAction struct {
2021-05-05 11:42:34 -05:00
URL string
2020-02-23 17:02:01 -06:00
Org string
Repo string
Path string
Ref string
}
func ( ra * remoteAction ) CloneURL ( ) string {
2021-05-05 11:42:34 -05:00
return fmt . Sprintf ( "https://%s/%s/%s" , ra . URL , ra . Org , ra . Repo )
2020-02-23 17:02:01 -06:00
}
2020-03-09 19:45:42 -05:00
func ( ra * remoteAction ) IsCheckout ( ) bool {
if ra . Org == "actions" && ra . Repo == "checkout" {
return true
}
return false
}
2020-02-23 17:02:01 -06:00
func newRemoteAction ( action string ) * remoteAction {
2021-01-23 10:07:28 -06:00
// GitHub's document[^] describes:
// > We strongly recommend that you include the version of
// > the action you are using by specifying a Git ref, SHA, or Docker tag number.
// Actually, the workflow stops if there is the uses directive that hasn't @ref.
// [^]: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
2020-02-23 17:02:01 -06:00
r := regexp . MustCompile ( ` ^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$ ` )
matches := r . FindStringSubmatch ( action )
2021-01-23 10:07:28 -06:00
if len ( matches ) < 7 || matches [ 6 ] == "" {
return nil
2020-02-23 17:02:01 -06:00
}
2021-01-23 10:07:28 -06:00
return & remoteAction {
Org : matches [ 1 ] ,
Repo : matches [ 2 ] ,
Path : matches [ 4 ] ,
Ref : matches [ 6 ] ,
2021-05-05 11:42:34 -05:00
URL : "github.com" ,
2020-02-23 17:02:01 -06:00
}
}
2020-06-23 13:57:24 -05:00
// https://github.com/nektos/act/issues/228#issuecomment-629709055
// files in .gitignore are not copied in a Docker container
// this causes issues with actions that ignore other important resources
// such as `node_modules` for example
func removeGitIgnore ( directory string ) error {
gitIgnorePath := path . Join ( directory , ".gitignore" )
if _ , err := os . Stat ( gitIgnorePath ) ; err == nil {
// .gitignore exists
log . Debugf ( "Removing %s before docker cp" , gitIgnorePath )
err := os . Remove ( gitIgnorePath )
if err != nil {
return err
}
}
return nil
}