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 }