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 }