177 lines
5.1 KiB
Go
177 lines
5.1 KiB
Go
package runner
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"sync"
|
|
|
|
"github.com/nektos/act/pkg/common"
|
|
"github.com/nektos/act/pkg/common/git"
|
|
"github.com/nektos/act/pkg/model"
|
|
)
|
|
|
|
func newLocalReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
|
return newReusableWorkflowExecutor(rc, rc.Config.Workdir, rc.Run.Job().Uses)
|
|
}
|
|
|
|
func newRemoteReusableWorkflowExecutor(rc *RunContext) common.Executor {
|
|
uses := rc.Run.Job().Uses
|
|
|
|
remoteReusableWorkflow := newRemoteReusableWorkflow(uses)
|
|
if remoteReusableWorkflow == nil {
|
|
return common.NewErrorExecutor(fmt.Errorf("expected format {owner}/{repo}/.github/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format", uses))
|
|
}
|
|
|
|
// uses with safe filename makes the target directory look something like this {owner}-{repo}-.github-workflows-{filename}@{ref}
|
|
// instead we will just use {owner}-{repo}@{ref} as our target directory. This should also improve performance when we are using
|
|
// multiple reusable workflows from the same repository and ref since for each workflow we won't have to clone it again
|
|
filename := fmt.Sprintf("%s/%s@%s", remoteReusableWorkflow.Org, remoteReusableWorkflow.Repo, remoteReusableWorkflow.Ref)
|
|
workflowDir := fmt.Sprintf("%s/%s", rc.ActionCacheDir(), safeFilename(filename))
|
|
|
|
if rc.Config.ActionCache != nil {
|
|
return newActionCacheReusableWorkflowExecutor(rc, filename, remoteReusableWorkflow)
|
|
}
|
|
|
|
return common.NewPipelineExecutor(
|
|
newMutexExecutor(cloneIfRequired(rc, *remoteReusableWorkflow, workflowDir)),
|
|
newReusableWorkflowExecutor(rc, workflowDir, fmt.Sprintf("./.github/workflows/%s", remoteReusableWorkflow.Filename)),
|
|
)
|
|
}
|
|
|
|
func newActionCacheReusableWorkflowExecutor(rc *RunContext, filename string, remoteReusableWorkflow *remoteReusableWorkflow) common.Executor {
|
|
return func(ctx context.Context) error {
|
|
ghctx := rc.getGithubContext(ctx)
|
|
remoteReusableWorkflow.URL = ghctx.ServerURL
|
|
sha, err := rc.Config.ActionCache.Fetch(ctx, filename, remoteReusableWorkflow.CloneURL(), remoteReusableWorkflow.Ref, ghctx.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
archive, err := rc.Config.ActionCache.GetTarArchive(ctx, filename, sha, fmt.Sprintf(".github/workflows/%s", remoteReusableWorkflow.Filename))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer archive.Close()
|
|
treader := tar.NewReader(archive)
|
|
if _, err = treader.Next(); err != nil {
|
|
return err
|
|
}
|
|
planner, err := model.NewSingleWorkflowPlanner(remoteReusableWorkflow.Filename, treader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
plan, err := planner.PlanEvent("workflow_call")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
runner, err := NewReusableWorkflowRunner(rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return runner.NewPlanExecutor(plan)(ctx)
|
|
}
|
|
}
|
|
|
|
var (
|
|
executorLock sync.Mutex
|
|
)
|
|
|
|
func newMutexExecutor(executor common.Executor) common.Executor {
|
|
return func(ctx context.Context) error {
|
|
executorLock.Lock()
|
|
defer executorLock.Unlock()
|
|
|
|
return executor(ctx)
|
|
}
|
|
}
|
|
|
|
func cloneIfRequired(rc *RunContext, remoteReusableWorkflow remoteReusableWorkflow, targetDirectory string) common.Executor {
|
|
return common.NewConditionalExecutor(
|
|
func(ctx context.Context) bool {
|
|
_, err := os.Stat(targetDirectory)
|
|
notExists := errors.Is(err, fs.ErrNotExist)
|
|
return notExists
|
|
},
|
|
func(ctx context.Context) error {
|
|
remoteReusableWorkflow.URL = rc.getGithubContext(ctx).ServerURL
|
|
return git.NewGitCloneExecutor(git.NewGitCloneExecutorInput{
|
|
URL: remoteReusableWorkflow.CloneURL(),
|
|
Ref: remoteReusableWorkflow.Ref,
|
|
Dir: targetDirectory,
|
|
Token: rc.Config.Token,
|
|
OfflineMode: rc.Config.ActionOfflineMode,
|
|
})(ctx)
|
|
},
|
|
nil,
|
|
)
|
|
}
|
|
|
|
func newReusableWorkflowExecutor(rc *RunContext, directory string, workflow string) common.Executor {
|
|
return func(ctx context.Context) error {
|
|
planner, err := model.NewWorkflowPlanner(path.Join(directory, workflow), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
plan, err := planner.PlanEvent("workflow_call")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
runner, err := NewReusableWorkflowRunner(rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return runner.NewPlanExecutor(plan)(ctx)
|
|
}
|
|
}
|
|
|
|
func NewReusableWorkflowRunner(rc *RunContext) (Runner, error) {
|
|
runner := &runnerImpl{
|
|
config: rc.Config,
|
|
eventJSON: rc.EventJSON,
|
|
caller: &caller{
|
|
runContext: rc,
|
|
},
|
|
}
|
|
|
|
return runner.configure()
|
|
}
|
|
|
|
type remoteReusableWorkflow struct {
|
|
URL string
|
|
Org string
|
|
Repo string
|
|
Filename string
|
|
Ref string
|
|
}
|
|
|
|
func (r *remoteReusableWorkflow) CloneURL() string {
|
|
return fmt.Sprintf("%s/%s/%s", r.URL, r.Org, r.Repo)
|
|
}
|
|
|
|
func newRemoteReusableWorkflow(uses string) *remoteReusableWorkflow {
|
|
// GitHub docs:
|
|
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
|
|
r := regexp.MustCompile(`^([^/]+)/([^/]+)/.github/workflows/([^@]+)@(.*)$`)
|
|
matches := r.FindStringSubmatch(uses)
|
|
if len(matches) != 5 {
|
|
return nil
|
|
}
|
|
return &remoteReusableWorkflow{
|
|
Org: matches[1],
|
|
Repo: matches[2],
|
|
Filename: matches[3],
|
|
Ref: matches[4],
|
|
URL: "https://github.com",
|
|
}
|
|
}
|