f0b1845802
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.
244 lines
8 KiB
Go
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",
|
|
}
|
|
}
|