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"
2021-09-27 12:33:14 -05:00
"embed"
2020-02-23 17:02:01 -06:00
"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
func ( sc * StepContext ) Executor ( ) common . Executor {
rc := sc . RunContext
step := sc . Step
switch step . Type ( ) {
case model . StepTypeRun :
return common . NewPipelineExecutor (
sc . setupShellCommand ( ) ,
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 )
2020-02-23 18:36:44 -06:00
return common . NewPipelineExecutor (
2021-08-03 12:39:56 -05:00
sc . setupAction ( actionDir , "" , true ) ,
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 ( )
if remoteAction . IsCheckout ( ) && github . isLocalCheckout ( 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-05-07 22:29:03 -05:00
if err := gitClone ( context . TODO ( ) ) ; 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
}
2020-02-24 00:34:48 -06:00
return common . NewPipelineExecutor (
2021-05-18 01:14:49 -05:00
ntErr ,
2021-08-03 12:39:56 -05:00
sc . setupAction ( actionDir , remoteAction . Path , false ) ,
sc . runAction ( actionDir , remoteAction . Path , 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 )
}
}
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-11-03 13:47:47 -05:00
// nolint:gocyclo
2020-02-23 17:02:01 -06:00
func ( sc * StepContext ) setupShellCommand ( ) common . Executor {
rc := sc . RunContext
step := sc . Step
return func ( ctx context . Context ) error {
var script strings . Builder
2020-11-02 15:56:20 -06:00
var err error
2020-02-23 17:02:01 -06:00
2020-08-28 13:52:25 -05:00
if step . WorkingDirectory == "" {
step . WorkingDirectory = rc . Run . Job ( ) . Defaults . Run . WorkingDirectory
}
if step . WorkingDirectory == "" {
step . WorkingDirectory = rc . Run . Workflow . Defaults . Run . WorkingDirectory
}
2021-06-10 10:28:23 -05:00
step . WorkingDirectory = rc . ExprEval . Interpolate ( step . WorkingDirectory )
2020-03-14 02:00:37 -05:00
2020-02-23 17:02:01 -06:00
run := rc . ExprEval . Interpolate ( step . Run )
2021-06-10 10:28:23 -05:00
step . Shell = rc . ExprEval . Interpolate ( step . Shell )
2020-02-23 17:02:01 -06:00
if _ , err = script . WriteString ( run ) ; err != nil {
return err
}
scriptName := fmt . Sprintf ( "workflow/%s" , step . ID )
2021-03-29 12:06:51 -05:00
2021-05-04 16:50:35 -05: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
2021-03-29 12:06:51 -05:00
runPrepend := ""
runAppend := ""
scriptExt := ""
switch step . Shell {
case "bash" , "sh" :
scriptExt = ".sh"
case "pwsh" , "powershell" :
scriptExt = ".ps1"
runPrepend = "$ErrorActionPreference = 'stop'"
runAppend = "if ((Test-Path -LiteralPath variable:/LASTEXITCODE)) { exit $LASTEXITCODE }"
case "cmd" :
scriptExt = ".cmd"
runPrepend = "@echo off"
case "python" :
scriptExt = ".py"
}
scriptName += scriptExt
run = runPrepend + "\n" + run + "\n" + runAppend
2020-02-23 17:02:01 -06:00
log . Debugf ( "Wrote command '%s' to '%s'" , run , scriptName )
2021-08-10 14:40:20 -05:00
scriptPath := fmt . Sprintf ( "%s/%s" , rc . Config . ContainerWorkdir ( ) , scriptName )
2020-08-28 13:52:25 -05:00
if step . Shell == "" {
step . Shell = rc . Run . Job ( ) . Defaults . Run . Shell
}
if step . Shell == "" {
step . Shell = rc . Run . Workflow . Defaults . Run . Shell
}
2021-11-03 13:47:47 -05:00
if rc . Run . Job ( ) . Container ( ) != nil {
if rc . Run . Job ( ) . Container ( ) . Image != "" && step . Shell == "" {
step . Shell = "sh"
}
}
2021-03-29 12:06:51 -05:00
scCmd := step . ShellCommand ( )
2021-08-10 14:40:20 -05:00
var finalCMD [ ] string
2021-03-29 12:06:51 -05:00
if step . Shell == "pwsh" || step . Shell == "powershell" {
2021-08-10 14:40:20 -05:00
finalCMD = strings . SplitN ( scCmd , " " , 3 )
2021-03-29 12:06:51 -05:00
} else {
2021-08-10 14:40:20 -05:00
finalCMD = strings . Fields ( scCmd )
2021-03-29 12:06:51 -05:00
}
2021-08-10 14:40:20 -05:00
for k , v := range finalCMD {
if strings . Contains ( v , ` { 0} ` ) {
finalCMD [ k ] = strings . Replace ( v , ` { 0} ` , scriptPath , 1 )
}
}
sc . Cmd = finalCMD
2021-05-04 16:50:35 -05:00
return rc . JobContainer . Copy ( rc . Config . ContainerWorkdir ( ) , & container . FileEntry {
2020-02-23 17:02:01 -06:00
Name : scriptName ,
2020-08-08 15:31:26 -05:00
Mode : 0755 ,
2020-02-23 17:02:01 -06:00
Body : script . String ( ) ,
} ) ( 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 00:34:48 -06:00
stepEE := sc . NewExpressionEvaluator ( )
for i , v := range cmd {
cmd [ i ] = stepEE . Interpolate ( v )
}
for i , v := range entrypoint {
entrypoint [ i ] = stepEE . Interpolate ( 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-02-23 11:47:06 -06:00
cmd , err := shellquote . Split ( sc . RunContext . NewExpressionEvaluator ( ) . Interpolate ( step . With [ "args" ] ) )
if err != nil {
return err
}
2020-02-24 00:34:48 -06:00
entrypoint := strings . Fields ( 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
}
}
2021-03-30 14:26:25 -05:00
//go:embed res/trampoline.js
2021-09-27 12:33:14 -05:00
var trampoline embed . FS
2021-03-30 14:26:25 -05:00
2021-08-03 12:39:56 -05:00
func ( sc * StepContext ) setupAction ( actionDir string , actionPath string , localAction bool ) common . Executor {
2020-02-23 17:02:01 -06:00
return func ( ctx context . Context ) error {
2021-08-03 12:39:56 -05:00
var readFile func ( filename string ) ( io . Reader , io . Closer , error )
if localAction {
_ , cpath := sc . getContainerActionPaths ( sc . Step , path . Join ( actionDir , actionPath ) , sc . RunContext )
readFile = 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
}
} else {
readFile = func ( filename string ) ( io . Reader , io . Closer , error ) {
f , err := os . Open ( filepath . Join ( actionDir , actionPath , filename ) )
return f , f , err
}
}
reader , closer , err := readFile ( "action.yml" )
2020-02-23 17:02:01 -06:00
if os . IsNotExist ( err ) {
2021-08-03 12:39:56 -05:00
reader , closer , err = readFile ( "action.yaml" )
2020-02-23 17:02:01 -06:00
if err != nil {
2021-08-03 12:39:56 -05:00
if _ , closer , err2 := readFile ( "Dockerfile" ) ; err2 == nil {
closer . Close ( )
2021-03-30 14:26:25 -05:00
sc . Action = & model . Action {
Name : "(Synthetic)" ,
Runs : model . ActionRuns {
Using : "docker" ,
Image : "Dockerfile" ,
} ,
}
log . Debugf ( "Using synthetic action %v for Dockerfile" , sc . Action )
return nil
}
if sc . Step . With != nil {
if val , ok := sc . Step . With [ "args" ] ; ok {
2021-09-27 12:33:14 -05:00
var b [ ] byte
if b , err = trampoline . ReadFile ( "res/trampoline.js" ) ; err != nil {
return err
}
err2 := ioutil . WriteFile ( filepath . Join ( actionDir , actionPath , "trampoline.js" ) , b , 0400 )
2021-03-30 14:26:25 -05:00
if err2 != nil {
return err
}
sc . Action = & model . Action {
Name : "(Synthetic)" ,
Inputs : map [ string ] model . Input {
"cwd" : {
Description : "(Actual working directory)" ,
Required : false ,
Default : filepath . Join ( actionDir , actionPath ) ,
} ,
"command" : {
Description : "(Actual program)" ,
Required : false ,
Default : val ,
} ,
} ,
Runs : model . ActionRuns {
Using : "node12" ,
Main : "trampoline.js" ,
} ,
}
log . Debugf ( "Using synthetic action %v" , sc . Action )
return nil
}
}
2020-02-23 17:02:01 -06:00
return err
}
} else if err != nil {
return err
}
2021-08-03 12:39:56 -05:00
defer closer . Close ( )
2020-02-23 17:02:01 -06:00
2021-08-03 12:39:56 -05:00
sc . Action , err = model . ReadAction ( reader )
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-05-04 16:50:35 -05:00
// nolint: gocyclo
2021-08-03 12:39:56 -05:00
func ( sc * StepContext ) runAction ( actionDir string , actionPath 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 {
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
2020-04-19 13:29:34 -05:00
sc . Env = mergeMaps ( sc . Env , action . Runs . Env )
2021-11-12 17:21:18 -06:00
ee := sc . NewExpressionEvaluator ( )
for k , v := range sc . Env {
sc . Env [ k ] = ee . Interpolate ( v )
}
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-06-10 10:28:23 -05:00
sc . Env [ "GITHUB_ACTION_PATH" ] = 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 {
case model . ActionRunsUsingNode12 :
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 ,
model . ActionRunsUsingComposite ,
} , action . Runs . Using ) )
}
}
}
2021-03-28 23:08:40 -05:00
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-05-06 08:30:12 -05:00
if ! correctArchExists {
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 )
}
}
cmd , err := shellquote . Split ( step . With [ "args" ] )
if err != nil {
return err
}
if len ( cmd ) == 0 {
cmd = action . Runs . Args
}
entrypoint := strings . Fields ( step . With [ "entrypoint" ] )
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
}
for outputName , output := range action . Outputs {
re := regexp . MustCompile ( ` \$ {{ steps \ . ( [ a - zA - Z_ ] [ a - zA - Z0 -9 _ - ] + ) \ .outputs \ . ( [ a - zA - Z_ ] [ a - zA - Z0 -9 _ - ] + ) }} ` )
matches := re . FindStringSubmatch ( output . Value )
if len ( matches ) > 2 {
if sc . RunContext . OutputMappings == nil {
sc . RunContext . OutputMappings = make ( map [ MappableOutput ] MappableOutput )
2021-04-02 15:40:44 -05:00
}
2021-05-06 08:30:12 -05:00
k := MappableOutput { StepID : matches [ 1 ] , OutputName : matches [ 2 ] }
v := MappableOutput { StepID : step . ID , OutputName : outputName }
sc . RunContext . OutputMappings [ k ] = v
}
}
2021-04-02 15:40:44 -05:00
2021-05-06 08:30:12 -05:00
executors := make ( [ ] common . Executor , 0 , len ( action . Runs . Steps ) )
stepID := 0
for _ , compositeStep := range action . Runs . Steps {
stepClone := compositeStep
// Take a copy of the run context structure (rc is a pointer)
// Then take the address of the new structure
rcCloneStr := * rc
rcClone := & rcCloneStr
if stepClone . ID == "" {
stepClone . ID = fmt . Sprintf ( "composite-%d" , stepID )
stepID ++
}
rcClone . CurrentStep = stepClone . ID
2021-04-02 15:40:44 -05:00
2021-05-06 08:30:12 -05:00
if err := compositeStep . Validate ( ) ; err != nil {
return err
}
2021-04-02 15:40:44 -05:00
2021-05-06 08:30:12 -05:00
// Setup the outputs for the composite steps
if _ , ok := rcClone . StepResults [ stepClone . ID ] ; ! ok {
rcClone . StepResults [ stepClone . ID ] = & stepResult {
Success : true ,
Outputs : make ( map [ string ] string ) ,
}
}
2021-04-02 15:40:44 -05:00
2021-06-06 09:53:18 -05:00
env := stepClone . Environment ( )
2021-05-06 08:30:12 -05:00
stepContext := StepContext {
RunContext : rcClone ,
2021-06-10 10:28:23 -05:00
Step : step ,
2021-06-06 09:53:18 -05:00
Env : mergeMaps ( sc . Env , env ) ,
2021-07-21 08:50:43 -05:00
Action : action ,
2021-05-06 08:30:12 -05:00
}
2021-04-02 15:40:44 -05:00
2021-06-10 10:28:23 -05:00
// Required to set github.action_path
if rcClone . Config . Env == nil {
// Workaround to get test working
rcClone . Config . Env = make ( map [ string ] string )
2021-05-06 08:30:12 -05:00
}
2021-06-10 10:28:23 -05:00
rcClone . Config . Env [ "GITHUB_ACTION_PATH" ] = sc . Env [ "GITHUB_ACTION_PATH" ]
ev := stepContext . NewExpressionEvaluator ( )
// Required to interpolate inputs and github.action_path into the env map
stepContext . interpolateEnv ( ev )
// Required to interpolate inputs, env and github.action_path into run steps
ev = stepContext . NewExpressionEvaluator ( )
stepClone . Run = ev . Interpolate ( stepClone . Run )
stepClone . Shell = ev . Interpolate ( stepClone . Shell )
stepClone . WorkingDirectory = ev . Interpolate ( stepClone . WorkingDirectory )
stepContext . Step = & stepClone
2021-05-06 08:30:12 -05:00
executors = append ( executors , stepContext . Executor ( ) )
}
return common . NewPipelineExecutor ( executors ... ) ( ctx )
}
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
}