summaryrefslogtreecommitdiffstats
path: root/pkg/runner/step_action_remote.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/runner/step_action_remote.go')
-rw-r--r--pkg/runner/step_action_remote.go358
1 files changed, 358 insertions, 0 deletions
diff --git a/pkg/runner/step_action_remote.go b/pkg/runner/step_action_remote.go
new file mode 100644
index 0000000..ed4d94e
--- /dev/null
+++ b/pkg/runner/step_action_remote.go
@@ -0,0 +1,358 @@
+package runner
+
+import (
+ "archive/tar"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ gogit "github.com/go-git/go-git/v5"
+
+ "github.com/nektos/act/pkg/common"
+ "github.com/nektos/act/pkg/common/git"
+ "github.com/nektos/act/pkg/model"
+)
+
+type stepActionRemote struct {
+ Step *model.Step
+ RunContext *RunContext
+ compositeRunContext *RunContext
+ compositeSteps *compositeSteps
+ readAction readAction
+ runAction runAction
+ action *model.Action
+ env map[string]string
+ remoteAction *remoteAction
+ cacheDir string
+ resolvedSha string
+}
+
+var stepActionRemoteNewCloneExecutor = git.NewGitCloneExecutor
+
+func (sar *stepActionRemote) prepareActionExecutor() common.Executor {
+ return func(ctx context.Context) error {
+ if sar.remoteAction != nil && sar.action != nil {
+ // we are already good to run
+ return nil
+ }
+
+ // For gitea:
+ // Since actions can specify the download source via a url prefix.
+ // The prefix may contain some sensitive information that needs to be stored in secrets,
+ // so we need to interpolate the expression value for uses first.
+ sar.Step.Uses = sar.RunContext.NewExpressionEvaluator(ctx).Interpolate(ctx, sar.Step.Uses)
+
+ 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)
+ }
+
+ github := sar.getGithubContext(ctx)
+ 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
+ }
+
+ for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom {
+ if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) {
+ sar.remoteAction.URL = "https://github.com"
+ github.Token = sar.RunContext.Config.ReplaceGheActionTokenWithGithubCom
+ }
+ }
+ 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)
+ repoURL := sar.remoteAction.URL + "/" + sar.cacheDir
+ repoRef := sar.remoteAction.Ref
+ sar.resolvedSha, err = cache.Fetch(ctx, sar.cacheDir, repoURL, repoRef, github.Token)
+ if err != nil {
+ return fmt.Errorf("failed to fetch \"%s\" version \"%s\": %w", repoURL, repoRef, err)
+ }
+
+ remoteReader := func(ctx context.Context) actionYamlReader {
+ return func(filename string) (io.Reader, io.Closer, error) {
+ spath := path.Join(sar.remoteAction.Path, 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
+ }
+
+ actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
+ gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
+ URL: sar.remoteAction.CloneURL(sar.RunContext.Config.DefaultActionInstance),
+ Ref: sar.remoteAction.Ref,
+ Dir: actionDir,
+ Token: "", /*
+ Shouldn't provide token when cloning actions,
+ the token comes from the instance which triggered the task,
+ however, it might be not the same instance which provides actions.
+ For GitHub, they are the same, always github.com.
+ But for Gitea, tasks triggered by a.com can clone actions from b.com.
+ */
+ OfflineMode: sar.RunContext.Config.ActionOfflineMode,
+
+ InsecureSkipTLS: sar.cloneSkipTLS(), // For Gitea
+ })
+ 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
+ }
+ }
+
+ 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
+ }
+ }
+
+ return common.NewPipelineExecutor(
+ ntErr,
+ func(ctx context.Context) error {
+ actionModel, err := sar.readAction(ctx, sar.Step, actionDir, sar.remoteAction.Path, remoteReader(ctx), os.WriteFile)
+ sar.action = actionModel
+ return err
+ },
+ )(ctx)
+ }
+}
+
+func (sar *stepActionRemote) pre() common.Executor {
+ sar.env = map[string]string{}
+
+ return common.NewPipelineExecutor(
+ sar.prepareActionExecutor(),
+ runStepExecutor(sar, stepStagePre, runPreStep(sar)).If(hasPreStep(sar)).If(shouldRunPreStep(sar)))
+}
+
+func (sar *stepActionRemote) main() common.Executor {
+ return common.NewPipelineExecutor(
+ sar.prepareActionExecutor(),
+ runStepExecutor(sar, stepStageMain, func(ctx context.Context) error {
+ github := sar.getGithubContext(ctx)
+ if sar.remoteAction.IsCheckout() && isLocalCheckout(github, sar.Step) && !sar.RunContext.Config.NoSkipCheckout {
+ 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)
+ copyToPath := path.Join(sar.RunContext.JobContainer.ToContainerPath(sar.RunContext.Config.Workdir), eval.Interpolate(ctx, sar.Step.With["path"]))
+ return sar.RunContext.JobContainer.CopyDir(copyToPath, sar.RunContext.Config.Workdir+string(filepath.Separator)+".", sar.RunContext.Config.UseGitIgnore)(ctx)
+ }
+
+ actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
+
+ return sar.runAction(sar, actionDir, sar.remoteAction)(ctx)
+ }),
+ )
+}
+
+func (sar *stepActionRemote) post() common.Executor {
+ return runStepExecutor(sar, stepStagePost, runPostStep(sar)).If(hasPostStep(sar)).If(shouldRunPostStep(sar))
+}
+
+func (sar *stepActionRemote) getRunContext() *RunContext {
+ return sar.RunContext
+}
+
+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
+}
+
+func (sar *stepActionRemote) getStepModel() *model.Step {
+ return sar.Step
+}
+
+func (sar *stepActionRemote) getEnv() *map[string]string {
+ return &sar.env
+}
+
+func (sar *stepActionRemote) getIfExpression(ctx context.Context, stage stepStage) string {
+ switch stage {
+ case stepStagePre:
+ github := sar.getGithubContext(ctx)
+ 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 ""
+}
+
+func (sar *stepActionRemote) getActionModel() *model.Action {
+ return sar.action
+}
+
+func (sar *stepActionRemote) getCompositeRunContext(ctx context.Context) *RunContext {
+ if sar.compositeRunContext == nil {
+ actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), safeFilename(sar.Step.Uses))
+ actionLocation := path.Join(actionDir, sar.remoteAction.Path)
+ _, containerActionDir := getContainerActionPaths(sar.getStepModel(), actionLocation, sar.RunContext)
+
+ sar.compositeRunContext = newCompositeRunContext(ctx, sar.RunContext, sar, containerActionDir)
+ sar.compositeSteps = sar.compositeRunContext.compositeExecutor(sar.action)
+ } 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
+ sar.compositeRunContext.ExtraPath = sar.RunContext.ExtraPath
+ }
+ return sar.compositeRunContext
+}
+
+func (sar *stepActionRemote) getCompositeSteps() *compositeSteps {
+ return sar.compositeSteps
+}
+
+// For Gitea
+// cloneSkipTLS returns true if the runner can clone an action from the Gitea instance
+func (sar *stepActionRemote) cloneSkipTLS() bool {
+ if !sar.RunContext.Config.InsecureSkipTLS {
+ // Return false if the Gitea instance is not an insecure instance
+ return false
+ }
+ if sar.remoteAction.URL == "" {
+ // Empty URL means the default action instance should be used
+ // Return true if the URL of the Gitea instance is the same as the URL of the default action instance
+ return sar.RunContext.Config.DefaultActionInstance == sar.RunContext.Config.GitHubInstance
+ }
+ // Return true if the URL of the remote action is the same as the URL of the Gitea instance
+ return sar.remoteAction.URL == sar.RunContext.Config.GitHubInstance
+}
+
+type remoteAction struct {
+ URL string
+ Org string
+ Repo string
+ Path string
+ Ref string
+}
+
+func (ra *remoteAction) CloneURL(u string) string {
+ if ra.URL == "" {
+ if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
+ u = "https://" + u
+ }
+ } else {
+ u = ra.URL
+ }
+
+ return fmt.Sprintf("%s/%s/%s", u, ra.Org, ra.Repo)
+}
+
+func (ra *remoteAction) IsCheckout() bool {
+ if ra.Org == "actions" && ra.Repo == "checkout" {
+ return true
+ }
+ return false
+}
+
+func newRemoteAction(action string) *remoteAction {
+ // support http(s)://host/owner/repo@v3
+ for _, schema := range []string{"https://", "http://"} {
+ if strings.HasPrefix(action, schema) {
+ splits := strings.SplitN(strings.TrimPrefix(action, schema), "/", 2)
+ if len(splits) != 2 {
+ return nil
+ }
+ ret := parseAction(splits[1])
+ if ret == nil {
+ return nil
+ }
+ ret.URL = schema + splits[0]
+ return ret
+ }
+ }
+
+ return parseAction(action)
+}
+
+func parseAction(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],
+ URL: "",
+ }
+}
+
+func safeFilename(s string) string {
+ return strings.NewReplacer(
+ `<`, "-",
+ `>`, "-",
+ `:`, "-",
+ `"`, "-",
+ `/`, "-",
+ `\`, "-",
+ `|`, "-",
+ `?`, "-",
+ `*`, "-",
+ ).Replace(s)
+}