github.com/nektos/act@v0.2.63/pkg/runner/step_action_remote.go (about)

     1  package runner
     2  
     3  import (
     4  	"archive/tar"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	gogit "github.com/go-git/go-git/v5"
    16  
    17  	"github.com/nektos/act/pkg/common"
    18  	"github.com/nektos/act/pkg/common/git"
    19  	"github.com/nektos/act/pkg/model"
    20  )
    21  
    22  type stepActionRemote struct {
    23  	Step                *model.Step
    24  	RunContext          *RunContext
    25  	compositeRunContext *RunContext
    26  	compositeSteps      *compositeSteps
    27  	readAction          readAction
    28  	runAction           runAction
    29  	action              *model.Action
    30  	env                 map[string]string
    31  	remoteAction        *remoteAction
    32  	cacheDir            string
    33  	resolvedSha         string
    34  }
    35  
    36  var (
    37  	stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
    38  )
    39  
    40  func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
    41  	return func(ctx context.Context) error {
    42  		if sar.remoteAction != nil && sar.action != nil {
    43  			// we are already good to run
    44  			return nil
    45  		}
    46  
    47  		sar.remoteAction = newRemoteAction(sar.Step.Uses)
    48  		if sar.remoteAction == nil {
    49  			return fmt.Errorf("Expected format {org}/{repo}[/path]@ref. Actual '%s' Input string was not in a correct format", sar.Step.Uses)
    50  		}
    51  
    52  		github := sar.getGithubContext(ctx)
    53  		sar.remoteAction.URL = github.ServerURL
    54  
    55  		if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
    56  			common.Logger(ctx).Debugf("Skipping local actions/checkout because workdir was already copied")
    57  			return nil
    58  		}
    59  
    60  		for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom {
    61  			if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) {
    62  				sar.remoteAction.URL = "https://github.com"
    63  				github.Token = sar.RunContext.Config.ReplaceGheActionTokenWithGithubCom
    64  			}
    65  		}
    66  		if sar.RunContext.Config.ActionCache != nil {
    67  			cache := sar.RunContext.Config.ActionCache
    68  
    69  			var err error
    70  			sar.cacheDir = fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo)
    71  			repoURL := sar.remoteAction.URL + "/" + sar.cacheDir
    72  			repoRef := sar.remoteAction.Ref
    73  			sar.resolvedSha, err = cache.Fetch(ctx, sar.cacheDir, repoURL, repoRef, github.Token)
    74  			if err != nil {
    75  				return fmt.Errorf("failed to fetch \"%s\" version \"%s\": %w", repoURL, repoRef, err)
    76  			}
    77  
    78  			remoteReader := func(ctx context.Context) actionYamlReader {
    79  				return func(filename string) (io.Reader, io.Closer, error) {
    80  					spath := path.Join(sar.remoteAction.Path, filename)
    81  					for i := 0; i < maxSymlinkDepth; i++ {
    82  						tars, err := cache.GetTarArchive(ctx, sar.cacheDir, sar.resolvedSha, spath)
    83  						if err != nil {
    84  							return nil, nil, os.ErrNotExist
    85  						}
    86  						treader := tar.NewReader(tars)
    87  						header, err := treader.Next()
    88  						if err != nil {
    89  							return nil, nil, os.ErrNotExist
    90  						}
    91  						if header.FileInfo().Mode()&os.ModeSymlink == os.ModeSymlink {
    92  							spath, err = symlinkJoin(spath, header.Linkname, ".")
    93  							if err != nil {
    94  								return nil, nil, err
    95  							}
    96  						} else {
    97  							return treader, tars, nil
    98  						}
    99  					}
   100  					return nil, nil, fmt.Errorf("max depth %d of symlinks exceeded while reading %s", maxSymlinkDepth, spath)
   101  				}
   102  			}
   103  
   104  			actionModel, err := sar.readAction(ctx, sar.Step, sar.resolvedSha, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
   105  			sar.action = actionModel
   106  			return err
   107  		}
   108  
   109  		actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
   110  		gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
   111  			URL:         sar.remoteAction.CloneURL(),
   112  			Ref:         sar.remoteAction.Ref,
   113  			Dir:         actionDir,
   114  			Token:       github.Token,
   115  			OfflineMode: sar.RunContext.Config.ActionOfflineMode,
   116  		})
   117  		var ntErr common.Executor
   118  		if err := gitClone(ctx); err != nil {
   119  			if errors.Is(err, git.ErrShortRef) {
   120  				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",
   121  					sar.Step.Uses, sar.remoteAction.Ref, err.(*git.Error).Commit())
   122  			} else if errors.Is(err, gogit.ErrForceNeeded) { // TODO: figure out if it will be easy to shadow/alias go-git err's
   123  				ntErr = common.NewInfoExecutor("Non-terminating error while running 'git clone': %v", err)
   124  			} else {
   125  				return err
   126  			}
   127  		}
   128  
   129  		remoteReader := func(ctx context.Context) actionYamlReader {
   130  			return func(filename string) (io.Reader, io.Closer, error) {
   131  				f, err := os.Open(filepath.Join(actionDir, sar.remoteAction.Path, filename))
   132  				return f, f, err
   133  			}
   134  		}
   135  
   136  		return common.NewPipelineExecutor(
   137  			ntErr,
   138  			func(ctx context.Context) error {
   139  				actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
   140  				sar.action = actionModel
   141  				return err
   142  			},
   143  		)(ctx)
   144  	}
   145  }
   146  
   147  func (sar *stepActionRemote) pre() common.Executor {
   148  	sar.env = map[string]string{}
   149  
   150  	return common.NewPipelineExecutor(
   151  		sar.prepareActionExecutor(),
   152  		runStepExecutor(sar, stepStagePre, runPreStep(sar)).If(hasPreStep(sar)).If(shouldRunPreStep(sar)))
   153  }
   154  
   155  func (sar *stepActionRemote) main() common.Executor {
   156  	return common.NewPipelineExecutor(
   157  		sar.prepareActionExecutor(),
   158  		runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
   159  			github := sar.getGithubContext(ctx)
   160  			if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
   161  				if sar.RunContext.Config.BindWorkdir {
   162  					common.Logger(ctx).Debugf("Skipping local actions/checkout because you bound your workspace")
   163  					return nil
   164  				}
   165  				eval := sar.RunContext.NewExpressionEvaluator(ctx)
   166  				copyToPath := path.Join(sar.RunContext.JobContainer.ToContainerPath(sar.RunContext.Config.Workdir), eval.Interpolate(ctx, sar.Step.With["path"]))
   167  				return sar.RunContext.JobContainer.CopyDir(copyToPath, sar.RunContext.Config.Workdir+string(filepath.Separator)+".", sar.RunContext.Config.UseGitIgnore)(ctx)
   168  			}
   169  
   170  			actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
   171  
   172  			return sar.runAction(sar, actionDir, sar.remoteAction)(ctx)
   173  		}),
   174  	)
   175  }
   176  
   177  func (sar *stepActionRemote) post() common.Executor {
   178  	return runStepExecutor(sar, stepStagePost, runPostStep(sar)).If(hasPostStep(sar)).If(shouldRunPostStep(sar))
   179  }
   180  
   181  func (sar *stepActionRemote) getRunContext() *RunContext {
   182  	return sar.RunContext
   183  }
   184  
   185  func (sar *stepActionRemote) getGithubContext(ctx context.Context) *model.GithubContext {
   186  	ghc := sar.getRunContext().getGithubContext(ctx)
   187  
   188  	// extend github context if we already have an initialized remoteAction
   189  	remoteAction := sar.remoteAction
   190  	if remoteAction != nil {
   191  		ghc.ActionRepository = fmt.Sprintf("%s/%s", remoteAction.Org, remoteAction.Repo)
   192  		ghc.ActionRef = remoteAction.Ref
   193  	}
   194  
   195  	return ghc
   196  }
   197  
   198  func (sar *stepActionRemote) getStepModel() *model.Step {
   199  	return sar.Step
   200  }
   201  
   202  func (sar *stepActionRemote) getEnv() *map[string]string {
   203  	return &sar.env
   204  }
   205  
   206  func (sar *stepActionRemote) getIfExpression(ctx context.Context, stage stepStage) string {
   207  	switch stage {
   208  	case stepStagePre:
   209  		github := sar.getGithubContext(ctx)
   210  		if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
   211  			// skip local checkout pre step
   212  			return "false"
   213  		}
   214  		return sar.action.Runs.PreIf
   215  	case stepStageMain:
   216  		return sar.Step.If.Value
   217  	case stepStagePost:
   218  		return sar.action.Runs.PostIf
   219  	}
   220  	return ""
   221  }
   222  
   223  func (sar *stepActionRemote) getActionModel() *model.Action {
   224  	return sar.action
   225  }
   226  
   227  func (sar *stepActionRemote) getCompositeRunContext(ctx context.Context) *RunContext {
   228  	if sar.compositeRunContext == nil {
   229  		actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
   230  		actionLocation := path.Join(actionDir, sar.remoteAction.Path)
   231  		_, containerActionDir := getContainerActionPaths(sar.getStepModel(), actionLocation, sar.RunContext)
   232  
   233  		sar.compositeRunContext = newCompositeRunContext(ctx, sar.RunContext, sar, containerActionDir)
   234  		sar.compositeSteps = sar.compositeRunContext.compositeExecutor(sar.action)
   235  	} else {
   236  		// Re-evaluate environment here. For remote actions the environment
   237  		// need to be re-created for every stage (pre, main, post) as there
   238  		// might be required context changes (inputs/outputs) while the action
   239  		// stages are executed. (e.g. the output of another action is the
   240  		// input for this action during the main stage, but the env
   241  		// was already created during the pre stage)
   242  		env := evaluateCompositeInputAndEnv(ctx, sar.RunContext, sar)
   243  		sar.compositeRunContext.Env = env
   244  		sar.compositeRunContext.ExtraPath = sar.RunContext.ExtraPath
   245  	}
   246  	return sar.compositeRunContext
   247  }
   248  
   249  func (sar *stepActionRemote) getCompositeSteps() *compositeSteps {
   250  	return sar.compositeSteps
   251  }
   252  
   253  type remoteAction struct {
   254  	URL  string
   255  	Org  string
   256  	Repo string
   257  	Path string
   258  	Ref  string
   259  }
   260  
   261  func (ra *remoteAction) CloneURL() string {
   262  	return fmt.Sprintf("%s/%s/%s", ra.URL, ra.Org, ra.Repo)
   263  }
   264  
   265  func (ra *remoteAction) IsCheckout() bool {
   266  	if ra.Org == "actions" && ra.Repo == "checkout" {
   267  		return true
   268  	}
   269  	return false
   270  }
   271  
   272  func newRemoteAction(action string) *remoteAction {
   273  	// GitHub's document[^] describes:
   274  	// > We strongly recommend that you include the version of
   275  	// > the action you are using by specifying a Git ref, SHA, or Docker tag number.
   276  	// Actually, the workflow stops if there is the uses directive that hasn't @ref.
   277  	// [^]: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions
   278  	r := regexp.MustCompile(`^([^/@]+)/([^/@]+)(/([^@]*))?(@(.*))?$`)
   279  	matches := r.FindStringSubmatch(action)
   280  	if len(matches) < 7 || matches[6] == "" {
   281  		return nil
   282  	}
   283  	return &remoteAction{
   284  		Org:  matches[1],
   285  		Repo: matches[2],
   286  		Path: matches[4],
   287  		Ref:  matches[6],
   288  		URL:  "https://github.com",
   289  	}
   290  }
   291  
   292  func safeFilename(s string) string {
   293  	return strings.NewReplacer(
   294  		`<`, "-",
   295  		`>`, "-",
   296  		`:`, "-",
   297  		`"`, "-",
   298  		`/`, "-",
   299  		`\`, "-",
   300  		`|`, "-",
   301  		`?`, "-",
   302  		`*`, "-",
   303  	).Replace(s)
   304  }