2022-03-22 16:13:00 -05:00
package runner
import (
2024-01-19 17:49:35 -06:00
"archive/tar"
2022-03-22 16:13:00 -05:00
"context"
2022-06-10 16:16:42 -05:00
"errors"
2022-03-22 16:13:00 -05:00
"fmt"
"io"
"os"
2022-05-24 08:36:06 -05:00
"path"
2022-03-22 16:13:00 -05:00
"path/filepath"
"regexp"
"strings"
2023-03-03 02:38:33 -06:00
gogit "github.com/go-git/go-git/v5"
2022-03-22 16:13:00 -05:00
"github.com/nektos/act/pkg/common"
2022-06-10 16:16:42 -05:00
"github.com/nektos/act/pkg/common/git"
2022-03-22 16:13:00 -05:00
"github.com/nektos/act/pkg/model"
)
type stepActionRemote struct {
2022-05-24 08:36:06 -05:00
Step * model . Step
RunContext * RunContext
compositeRunContext * RunContext
compositeSteps * compositeSteps
readAction readAction
runAction runAction
action * model . Action
env map [ string ] string
remoteAction * remoteAction
2024-01-19 17:49:35 -06:00
cacheDir string
resolvedSha string
2022-03-22 16:13:00 -05:00
}
var (
2022-06-10 16:16:42 -05:00
stepActionRemoteNewCloneExecutor = git . NewGitCloneExecutor
2022-03-22 16:13:00 -05:00
)
2022-06-08 10:36:08 -05:00
func ( sar * stepActionRemote ) prepareActionExecutor ( ) common . Executor {
2022-06-20 16:58:51 -05:00
return func ( ctx context . Context ) error {
if sar . remoteAction != nil && sar . action != nil {
// we are already good to run
return nil
}
2022-06-08 10:36:08 -05:00
2022-06-20 16:58:51 -05:00
sar . remoteAction = newRemoteAction ( sar . Step . Uses )
if sar . remoteAction == nil {
return fmt . Errorf ( "Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format" , sar . Step . Uses )
}
2022-05-24 08:36:06 -05:00
2022-10-06 16:58:16 -05:00
github := sar . getGithubContext ( ctx )
2023-05-03 10:46:28 -05:00
sar . remoteAction . URL = github . ServerURL
2022-06-20 16:58:51 -05:00
if sar . remoteAction . IsCheckout ( ) && isLocalCheckout ( github , sar . Step ) && ! sar . RunContext . Config . NoSkipCheckout {
common . Logger ( ctx ) . Debugf ( "Skipping local actions/checkout because workdir was already copied" )
return nil
}
2022-05-24 08:36:06 -05:00
2022-06-21 08:52:21 -05:00
for _ , action := range sar . RunContext . Config . ReplaceGheActionWithGithubCom {
if strings . EqualFold ( fmt . Sprintf ( "%s/%s" , sar . remoteAction . Org , sar . remoteAction . Repo ) , action ) {
2023-05-03 10:46:28 -05:00
sar . remoteAction . URL = "https://github.com"
2022-06-21 08:52:21 -05:00
github . Token = sar . RunContext . Config . ReplaceGheActionTokenWithGithubCom
}
}
2024-01-19 17:49:35 -06:00
if sar . RunContext . Config . ActionCache != nil {
cache := sar . RunContext . Config . ActionCache
var err error
sar . cacheDir = fmt . Sprintf ( "%s/%s" , sar . remoteAction . Org , sar . remoteAction . Repo )
sar . resolvedSha , err = cache . Fetch ( ctx , sar . cacheDir , sar . remoteAction . URL + "/" + sar . cacheDir , sar . remoteAction . Ref , github . Token )
if err != nil {
return err
}
remoteReader := func ( ctx context . Context ) actionYamlReader {
return func ( filename string ) ( io . Reader , io . Closer , error ) {
spath := filename
for i := 0 ; i < maxSymlinkDepth ; i ++ {
tars , err := cache . GetTarArchive ( ctx , sar . cacheDir , sar . resolvedSha , spath )
if err != nil {
return nil , nil , os . ErrNotExist
}
treader := tar . NewReader ( tars )
header , err := treader . Next ( )
if err != nil {
return nil , nil , os . ErrNotExist
}
if header . FileInfo ( ) . Mode ( ) & os . ModeSymlink == os . ModeSymlink {
spath , err = symlinkJoin ( spath , header . Linkname , "." )
if err != nil {
return nil , nil , err
}
} else {
return treader , tars , nil
}
}
return nil , nil , fmt . Errorf ( "max depth %d of symlinks exceeded while reading %s" , maxSymlinkDepth , spath )
}
}
actionModel , err := sar . readAction ( ctx , sar . Step , sar . resolvedSha , sar . remoteAction . Path , remoteReader ( ctx ) , os . WriteFile )
sar . action = actionModel
return err
}
2022-06-21 08:52:21 -05:00
2023-03-03 02:38:33 -06:00
actionDir := fmt . Sprintf ( "%s/%s" , sar . RunContext . ActionCacheDir ( ) , safeFilename ( sar . Step . Uses ) )
2022-06-20 16:58:51 -05:00
gitClone := stepActionRemoteNewCloneExecutor ( git . NewGitCloneExecutorInput {
2024-01-19 18:20:15 -06:00
URL : sar . remoteAction . CloneURL ( ) ,
Ref : sar . remoteAction . Ref ,
Dir : actionDir ,
Token : github . Token ,
OfflineMode : sar . RunContext . Config . ActionOfflineMode ,
2022-06-20 16:58:51 -05:00
} )
var ntErr common . Executor
if err := gitClone ( ctx ) ; err != nil {
if errors . Is ( err , git . ErrShortRef ) {
return 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" ,
sar . Step . Uses , sar . remoteAction . Ref , err . ( * git . Error ) . Commit ( ) )
} else if errors . Is ( err , gogit . ErrForceNeeded ) { // TODO: figure out if it will be easy to shadow/alias go-git err's
ntErr = common . NewInfoExecutor ( "Non-terminating error while running 'git clone': %v" , err )
} else {
return err
2022-05-24 08:36:06 -05:00
}
2022-06-20 16:58:51 -05:00
}
2022-05-24 08:36:06 -05:00
2022-06-20 16:58:51 -05:00
remoteReader := func ( ctx context . Context ) actionYamlReader {
return func ( filename string ) ( io . Reader , io . Closer , error ) {
f , err := os . Open ( filepath . Join ( actionDir , sar . remoteAction . Path , filename ) )
return f , f , err
2022-05-24 08:36:06 -05:00
}
2022-06-20 16:58:51 -05:00
}
2022-03-22 16:13:00 -05:00
2022-06-20 16:58:51 -05:00
return common . NewPipelineExecutor (
ntErr ,
func ( ctx context . Context ) error {
2022-10-06 16:58:16 -05:00
actionModel , err := sar . readAction ( ctx , sar . Step , actionDir , sar . remoteAction . Path , remoteReader ( ctx ) , os . WriteFile )
2022-06-20 16:58:51 -05:00
sar . action = actionModel
return err
} ,
) ( ctx )
}
2022-06-08 10:36:08 -05:00
}
func ( sar * stepActionRemote ) pre ( ) common . Executor {
sar . env = map [ string ] string { }
return common . NewPipelineExecutor (
sar . prepareActionExecutor ( ) ,
2022-05-24 08:36:06 -05:00
runStepExecutor ( sar , stepStagePre , runPreStep ( sar ) ) . If ( hasPreStep ( sar ) ) . If ( shouldRunPreStep ( sar ) ) )
}
func ( sar * stepActionRemote ) main ( ) common . Executor {
2022-06-08 10:36:08 -05:00
return common . NewPipelineExecutor (
sar . prepareActionExecutor ( ) ,
runStepExecutor ( sar , stepStageMain , func ( ctx context . Context ) error {
2022-10-06 16:58:16 -05:00
github := sar . getGithubContext ( ctx )
2022-06-08 10:36:08 -05:00
if sar . remoteAction . IsCheckout ( ) && isLocalCheckout ( github , sar . Step ) && ! sar . RunContext . Config . NoSkipCheckout {
2022-06-20 17:14:14 -05:00
if sar . RunContext . Config . BindWorkdir {
common . Logger ( ctx ) . Debugf ( "Skipping local actions/checkout because you bound your workspace" )
return nil
}
eval := sar . RunContext . NewExpressionEvaluator ( ctx )
2022-11-16 15:29:45 -06:00
copyToPath := path . Join ( sar . RunContext . JobContainer . ToContainerPath ( sar . RunContext . Config . Workdir ) , eval . Interpolate ( ctx , sar . Step . With [ "path" ] ) )
2022-06-20 17:14:14 -05:00
return sar . RunContext . JobContainer . CopyDir ( copyToPath , sar . RunContext . Config . Workdir + string ( filepath . Separator ) + "." , sar . RunContext . Config . UseGitIgnore ) ( ctx )
2022-06-08 10:36:08 -05:00
}
2022-03-22 16:13:00 -05:00
2023-03-03 02:38:33 -06:00
actionDir := fmt . Sprintf ( "%s/%s" , sar . RunContext . ActionCacheDir ( ) , safeFilename ( sar . Step . Uses ) )
2022-03-22 16:13:00 -05:00
2022-10-06 16:58:16 -05:00
return sar . runAction ( sar , actionDir , sar . remoteAction ) ( ctx )
2022-06-08 10:36:08 -05:00
} ) ,
)
2022-03-22 16:13:00 -05:00
}
func ( sar * stepActionRemote ) post ( ) common . Executor {
2022-05-24 08:36:06 -05:00
return runStepExecutor ( sar , stepStagePost , runPostStep ( sar ) ) . If ( hasPostStep ( sar ) ) . If ( shouldRunPostStep ( sar ) )
2022-03-22 16:13:00 -05:00
}
func ( sar * stepActionRemote ) getRunContext ( ) * RunContext {
return sar . RunContext
}
2022-10-06 16:58:16 -05:00
func ( sar * stepActionRemote ) getGithubContext ( ctx context . Context ) * model . GithubContext {
ghc := sar . getRunContext ( ) . getGithubContext ( ctx )
// extend github context if we already have an initialized remoteAction
remoteAction := sar . remoteAction
if remoteAction != nil {
ghc . ActionRepository = fmt . Sprintf ( "%s/%s" , remoteAction . Org , remoteAction . Repo )
ghc . ActionRef = remoteAction . Ref
}
return ghc
}
2022-03-22 16:13:00 -05:00
func ( sar * stepActionRemote ) getStepModel ( ) * model . Step {
return sar . Step
}
func ( sar * stepActionRemote ) getEnv ( ) * map [ string ] string {
return & sar . env
}
2022-06-17 10:55:21 -05:00
func ( sar * stepActionRemote ) getIfExpression ( ctx context . Context , stage stepStage ) string {
2022-05-24 08:36:06 -05:00
switch stage {
case stepStagePre :
2022-10-06 16:58:16 -05:00
github := sar . getGithubContext ( ctx )
2022-05-24 08:36:06 -05:00
if sar . remoteAction . IsCheckout ( ) && isLocalCheckout ( github , sar . Step ) && ! sar . RunContext . Config . NoSkipCheckout {
// skip local checkout pre step
return "false"
}
return sar . action . Runs . PreIf
case stepStageMain :
return sar . Step . If . Value
case stepStagePost :
return sar . action . Runs . PostIf
}
return ""
}
2022-03-22 16:13:00 -05:00
func ( sar * stepActionRemote ) getActionModel ( ) * model . Action {
return sar . action
}
2022-06-17 10:55:21 -05:00
func ( sar * stepActionRemote ) getCompositeRunContext ( ctx context . Context ) * RunContext {
2022-05-24 08:36:06 -05:00
if sar . compositeRunContext == nil {
2023-03-03 02:38:33 -06:00
actionDir := fmt . Sprintf ( "%s/%s" , sar . RunContext . ActionCacheDir ( ) , safeFilename ( sar . Step . Uses ) )
2022-05-24 08:36:06 -05:00
actionLocation := path . Join ( actionDir , sar . remoteAction . Path )
_ , containerActionDir := getContainerActionPaths ( sar . getStepModel ( ) , actionLocation , sar . RunContext )
2022-06-17 10:55:21 -05:00
sar . compositeRunContext = newCompositeRunContext ( ctx , sar . RunContext , sar , containerActionDir )
2022-05-24 08:36:06 -05:00
sar . compositeSteps = sar . compositeRunContext . compositeExecutor ( sar . action )
2022-10-12 11:19:32 -05:00
} else {
// Re-evaluate environment here. For remote actions the environment
// need to be re-created for every stage (pre, main, post) as there
// might be required context changes (inputs/outputs) while the action
// stages are executed. (e.g. the output of another action is the
// input for this action during the main stage, but the env
// was already created during the pre stage)
env := evaluateCompositeInputAndEnv ( ctx , sar . RunContext , sar )
sar . compositeRunContext . Env = env
2023-01-10 15:55:05 -06:00
sar . compositeRunContext . ExtraPath = sar . RunContext . ExtraPath
2022-05-24 08:36:06 -05:00
}
return sar . compositeRunContext
}
func ( sar * stepActionRemote ) getCompositeSteps ( ) * compositeSteps {
return sar . compositeSteps
}
2022-03-22 16:13:00 -05:00
type remoteAction struct {
URL string
Org string
Repo string
Path string
Ref string
}
func ( ra * remoteAction ) CloneURL ( ) string {
2023-05-03 10:46:28 -05:00
return fmt . Sprintf ( "%s/%s/%s" , ra . URL , ra . Org , ra . Repo )
2022-03-22 16:13:00 -05:00
}
func ( ra * remoteAction ) IsCheckout ( ) bool {
if ra . Org == "actions" && ra . Repo == "checkout" {
return true
}
return false
}
func newRemoteAction ( action string ) * remoteAction {
// 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
r := regexp . MustCompile ( ` ^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$ ` )
matches := r . FindStringSubmatch ( action )
if len ( matches ) < 7 || matches [ 6 ] == "" {
return nil
}
return & remoteAction {
Org : matches [ 1 ] ,
Repo : matches [ 2 ] ,
Path : matches [ 4 ] ,
Ref : matches [ 6 ] ,
2023-05-03 10:46:28 -05:00
URL : "https://github.com" ,
2022-03-22 16:13:00 -05:00
}
}
2023-03-03 02:38:33 -06:00
func safeFilename ( s string ) string {
return strings . NewReplacer (
` < ` , "-" ,
` > ` , "-" ,
` : ` , "-" ,
` " ` , "-" ,
` / ` , "-" ,
` \ ` , "-" ,
` | ` , "-" ,
` ? ` , "-" ,
` * ` , "-" ,
) . Replace ( s )
}