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

     1  package runner
     2  
     3  import (
     4  	"archive/tar"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io/fs"
     9  	"os"
    10  	"path"
    11  	"regexp"
    12  	"sync"
    13  
    14  	"github.com/nektos/act/pkg/common"
    15  	"github.com/nektos/act/pkg/common/git"
    16  	"github.com/nektos/act/pkg/model"
    17  )
    18  
    19  func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
    20  	return newReusableWorkflowExecutor(rc, rc.Config.Workdir, rc.Run.Job().Uses)
    21  }
    22  
    23  func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
    24  	uses := rc.Run.Job().Uses
    25  
    26  	remoteReusableWorkflow := newRemoteReusableWorkflow(uses)
    27  	if remoteReusableWorkflow == nil {
    28  		return common.NewErrorExecutor(fmt.Errorf("expected format {owner}/{repo}/.github/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", uses))
    29  	}
    30  
    31  	// uses with safe filename makes the target directory look something like this {owner}-{repo}-.github-workflows-{filename}@{ref}
    32  	// instead we will just use {owner}-{repo}@{ref} as our target directory. This should also improve performance when we are using
    33  	// multiple reusable workflows from the same repository and ref since for each workflow we won't have to clone it again
    34  	filename := fmt.Sprintf("%s/%s@%s", remoteReusableWorkflow.Org, remoteReusableWorkflow.Repo, remoteReusableWorkflow.Ref)
    35  	workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(filename))
    36  
    37  	if rc.Config.ActionCache != nil {
    38  		return newActionCacheReusableWorkflowExecutor(rc, filename, remoteReusableWorkflow)
    39  	}
    40  
    41  	return common.NewPipelineExecutor(
    42  		newMutexExecutor(cloneIfRequired(rc, *remoteReusableWorkflow, workflowDir)),
    43  		newReusableWorkflowExecutor(rc, workflowDir, fmt.Sprintf("./.github/workflows/%s", remoteReusableWorkflow.Filename)),
    44  	)
    45  }
    46  
    47  func newActionCacheReusableWorkflowExecutor(rc *RunContext, filename string, remoteReusableWorkflow *remoteReusableWorkflow) common.Executor {
    48  	return func(ctx context.Context) error {
    49  		ghctx := rc.getGithubContext(ctx)
    50  		remoteReusableWorkflow.URL = ghctx.ServerURL
    51  		sha, err := rc.Config.ActionCache.Fetch(ctx, filename, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, ghctx.Token)
    52  		if err != nil {
    53  			return err
    54  		}
    55  		archive, err := rc.Config.ActionCache.GetTarArchive(ctx, filename, sha, fmt.Sprintf(".github/workflows/%s", remoteReusableWorkflow.Filename))
    56  		if err != nil {
    57  			return err
    58  		}
    59  		defer archive.Close()
    60  		treader := tar.NewReader(archive)
    61  		if _, err = treader.Next(); err != nil {
    62  			return err
    63  		}
    64  		planner, err := model.NewSingleWorkflowPlanner(remoteReusableWorkflow.Filename, treader)
    65  		if err != nil {
    66  			return err
    67  		}
    68  		plan, err := planner.PlanEvent("workflow_call")
    69  		if err != nil {
    70  			return err
    71  		}
    72  
    73  		runner, err := NewReusableWorkflowRunner(rc)
    74  		if err != nil {
    75  			return err
    76  		}
    77  
    78  		return runner.NewPlanExecutor(plan)(ctx)
    79  	}
    80  }
    81  
    82  var (
    83  	executorLock sync.Mutex
    84  )
    85  
    86  func newMutexExecutor(executor common.Executor) common.Executor {
    87  	return func(ctx context.Context) error {
    88  		executorLock.Lock()
    89  		defer executorLock.Unlock()
    90  
    91  		return executor(ctx)
    92  	}
    93  }
    94  
    95  func cloneIfRequired(rc *RunContext, remoteReusableWorkflow remoteReusableWorkflow, targetDirectory string) common.Executor {
    96  	return common.NewConditionalExecutor(
    97  		func(ctx context.Context) bool {
    98  			_, err := os.Stat(targetDirectory)
    99  			notExists := errors.Is(err, fs.ErrNotExist)
   100  			return notExists
   101  		},
   102  		func(ctx context.Context) error {
   103  			remoteReusableWorkflow.URL = rc.getGithubContext(ctx).ServerURL
   104  			return git.NewGitCloneExecutor(git.NewGitCloneExecutorInput{
   105  				URL:         remoteReusableWorkflow.CloneURL(),
   106  				Ref:         remoteReusableWorkflow.Ref,
   107  				Dir:         targetDirectory,
   108  				Token:       rc.Config.Token,
   109  				OfflineMode: rc.Config.ActionOfflineMode,
   110  			})(ctx)
   111  		},
   112  		nil,
   113  	)
   114  }
   115  
   116  func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor {
   117  	return func(ctx context.Context) error {
   118  		planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
   119  		if err != nil {
   120  			return err
   121  		}
   122  
   123  		plan, err := planner.PlanEvent("workflow_call")
   124  		if err != nil {
   125  			return err
   126  		}
   127  
   128  		runner, err := NewReusableWorkflowRunner(rc)
   129  		if err != nil {
   130  			return err
   131  		}
   132  
   133  		return runner.NewPlanExecutor(plan)(ctx)
   134  	}
   135  }
   136  
   137  func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
   138  	runner := &runnerImpl{
   139  		config:    rc.Config,
   140  		eventJSON: rc.EventJSON,
   141  		caller: &caller{
   142  			runContext: rc,
   143  		},
   144  	}
   145  
   146  	return runner.configure()
   147  }
   148  
   149  type remoteReusableWorkflow struct {
   150  	URL      string
   151  	Org      string
   152  	Repo     string
   153  	Filename string
   154  	Ref      string
   155  }
   156  
   157  func (r *remoteReusableWorkflow) CloneURL() string {
   158  	return fmt.Sprintf("%s/%s/%s", r.URL, r.Org, r.Repo)
   159  }
   160  
   161  func newRemoteReusableWorkflow(uses string) *remoteReusableWorkflow {
   162  	// GitHub docs:
   163  	// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
   164  	r := regexp.MustCompile(`^([^/]+)/([^/]+)/.github/workflows/([^@]+)@(.*)$`)
   165  	matches := r.FindStringSubmatch(uses)
   166  	if len(matches) != 5 {
   167  		return nil
   168  	}
   169  	return &remoteReusableWorkflow{
   170  		Org:      matches[1],
   171  		Repo:     matches[2],
   172  		Filename: matches[3],
   173  		Ref:      matches[4],
   174  		URL:      "https://github.com",
   175  	}
   176  }