2022-12-15 10:45:22 -06:00
package runner
import (
2024-01-19 17:49:35 -06:00
"archive/tar"
2023-01-19 14:49:11 -06:00
"context"
"errors"
2022-12-15 10:45:22 -06:00
"fmt"
2023-01-19 14:49:11 -06:00
"io/fs"
"os"
2022-12-15 10:45:22 -06:00
"path"
2023-01-19 14:49:11 -06:00
"regexp"
"sync"
2022-12-15 10:45:22 -06:00
"github.com/nektos/act/pkg/common"
2023-01-19 14:49:11 -06:00
"github.com/nektos/act/pkg/common/git"
2022-12-15 10:45:22 -06:00
"github.com/nektos/act/pkg/model"
)
func newLocalReusableWorkflowExecutor ( rc * RunContext ) common . Executor {
2023-01-19 14:49:11 -06:00
return newReusableWorkflowExecutor ( rc , rc . Config . Workdir , rc . Run . Job ( ) . Uses )
2022-12-15 10:45:22 -06:00
}
func newRemoteReusableWorkflowExecutor ( rc * RunContext ) common . Executor {
2023-01-19 14:49:11 -06:00
uses := rc . Run . Job ( ) . Uses
2022-12-15 10:45:22 -06:00
2023-01-19 14:49:11 -06:00
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 ) )
2022-12-15 10:45:22 -06:00
}
2023-01-19 14:49:11 -06:00
2023-05-03 10:46:28 -05:00
// 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 ) )
2023-01-19 14:49:11 -06:00
2024-01-19 17:49:35 -06:00
if rc . Config . ActionCache != nil {
return newActionCacheReusableWorkflowExecutor ( rc , filename , remoteReusableWorkflow )
}
2023-01-19 14:49:11 -06:00
return common . NewPipelineExecutor (
newMutexExecutor ( cloneIfRequired ( rc , * remoteReusableWorkflow , workflowDir ) ) ,
newReusableWorkflowExecutor ( rc , workflowDir , fmt . Sprintf ( "./.github/workflows/%s" , remoteReusableWorkflow . Filename ) ) ,
)
}
2024-01-19 17:49:35 -06:00
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 )
}
}
2023-01-19 14:49:11 -06:00
var (
executorLock sync . Mutex
)
2022-12-15 10:45:22 -06:00
2023-01-19 14:49:11 -06:00
func newMutexExecutor ( executor common . Executor ) common . Executor {
return func ( ctx context . Context ) error {
executorLock . Lock ( )
defer executorLock . Unlock ( )
2022-12-15 10:45:22 -06:00
2023-01-19 14:49:11 -06:00
return executor ( ctx )
2022-12-15 10:45:22 -06:00
}
2023-01-19 14:49:11 -06:00
}
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
} ,
2023-05-03 10:46:28 -05:00
func ( ctx context . Context ) error {
remoteReusableWorkflow . URL = rc . getGithubContext ( ctx ) . ServerURL
return git . NewGitCloneExecutor ( git . NewGitCloneExecutorInput {
2024-01-19 18:20:15 -06:00
URL : remoteReusableWorkflow . CloneURL ( ) ,
Ref : remoteReusableWorkflow . Ref ,
Dir : targetDirectory ,
Token : rc . Config . Token ,
OfflineMode : rc . Config . ActionOfflineMode ,
2023-05-03 10:46:28 -05:00
} ) ( ctx )
} ,
2023-01-19 14:49:11 -06:00
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
}
2022-12-15 10:45:22 -06:00
2023-02-16 10:41:59 -06:00
plan , err := planner . PlanEvent ( "workflow_call" )
if err != nil {
return err
}
2023-01-19 14:49:11 -06:00
runner , err := NewReusableWorkflowRunner ( rc )
if err != nil {
return err
}
return runner . NewPlanExecutor ( plan ) ( ctx )
}
2022-12-15 10:45:22 -06:00
}
func NewReusableWorkflowRunner ( rc * RunContext ) ( Runner , error ) {
runner := & runnerImpl {
config : rc . Config ,
eventJSON : rc . EventJSON ,
caller : & caller {
runContext : rc ,
} ,
}
return runner . configure ( )
}
2023-01-19 14:49:11 -06:00
type remoteReusableWorkflow struct {
URL string
Org string
Repo string
Filename string
Ref string
}
func ( r * remoteReusableWorkflow ) CloneURL ( ) string {
2023-05-03 10:46:28 -05:00
return fmt . Sprintf ( "%s/%s/%s" , r . URL , r . Org , r . Repo )
2023-01-19 14:49:11 -06:00
}
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 ] ,
2023-05-03 10:46:28 -05:00
URL : "https://github.com" ,
2023-01-19 14:49:11 -06:00
}
}