act/pkg/runner/step_action_remote.go
Markus Wolf f0b1845802
fix: re-evaluate env for remote composite actions (#1385)
The env for remote composite actions need to be re-evaluated
at every stage (pre, main, post) as it is created during the
pre stage but there might be changes used as input to this
actions main stage (e.g. outputs for another action).

This is not required for local actions as their env is created
for the main stage (there is no pre stage).
Post stages do not need an updated env since they cannot recieve
inputs from other actions.
2022-10-12 16:19:32 +00:00

244 lines
8 KiB
Go

package runner
import (
"context"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/nektos/act/pkg/common"
"github.com/nektos/act/pkg/common/git"
"github.com/nektos/act/pkg/model"
gogit "github.com/go-git/go-git/v5"
)
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
}
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
}
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)
}
sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance
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
}
sar.remoteAction.URL = sar.RunContext.Config.GitHubInstance
for _, action := range sar.RunContext.Config.ReplaceGheActionWithGithubCom {
if strings.EqualFold(fmt.Sprintf("%s/%s", sar.remoteAction.Org, sar.remoteAction.Repo), action) {
sar.remoteAction.URL = "github.com"
github.Token = sar.RunContext.Config.ReplaceGheActionTokenWithGithubCom
}
}
actionDir := fmt.Sprintf("%s/%s", sar.RunContext.ActionCacheDir(), strings.ReplaceAll(sar.Step.Uses, "/", "-"))
gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{
URL: sar.remoteAction.CloneURL(),
Ref: sar.remoteAction.Ref,
Dir: actionDir,
Token: github.Token,
})
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.Config.ContainerWorkdir(), 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(), strings.ReplaceAll(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(), strings.ReplaceAll(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
}
return sar.compositeRunContext
}
func (sar *stepActionRemote) getCompositeSteps() *compositeSteps {
return sar.compositeSteps
}
type remoteAction struct {
URL string
Org string
Repo string
Path string
Ref string
}
func (ra *remoteAction) CloneURL() string {
return fmt.Sprintf("https://%s/%s/%s", ra.URL, 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 {
// 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: "github.com",
}
}