2022-12-15 10:45:22 -06:00
package runner
import (
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"
2023-03-29 00:59:22 -05:00
"strings"
2023-01-19 14:49:11 -06:00
"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-06-12 22:46:26 -05:00
if ! rc . Config . NoSkipCheckout {
fullPath := rc . Run . Job ( ) . Uses
fileName := path . Base ( fullPath )
workflowDir := strings . TrimSuffix ( fullPath , path . Join ( "/" , fileName ) )
workflowDir = strings . TrimPrefix ( workflowDir , "./" )
return common . NewPipelineExecutor (
newReusableWorkflowExecutor ( rc , workflowDir , fileName ) ,
)
}
2023-03-29 00:59:22 -05:00
// ./.gitea/workflows/wf.yml -> .gitea/workflows/wf.yml
trimmedUses := strings . TrimPrefix ( rc . Run . Job ( ) . Uses , "./" )
// uses string format is {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}
uses := fmt . Sprintf ( "%s/%s@%s" , rc . Config . PresetGitHubContext . Repository , trimmedUses , rc . Config . PresetGitHubContext . Sha )
2023-06-16 00:12:43 -05:00
remoteReusableWorkflow := newRemoteReusableWorkflowWithPlat ( rc . Config . GitHubInstance , uses )
2023-03-29 00:59:22 -05:00
if remoteReusableWorkflow == nil {
return common . NewErrorExecutor ( fmt . Errorf ( "expected format {owner}/{repo}/.{git_platform}/workflows/{filename}@{ref}. Actual '%s' Input string was not in a correct format" , uses ) )
}
workflowDir := fmt . Sprintf ( "%s/%s" , rc . ActionCacheDir ( ) , safeFilename ( uses ) )
2023-04-06 01:16:20 -05:00
// If the repository is private, we need a token to clone it
token := rc . Config . GetToken ( )
2023-03-29 00:59:22 -05:00
return common . NewPipelineExecutor (
2023-04-06 01:16:20 -05:00
newMutexExecutor ( cloneIfRequired ( rc , * remoteReusableWorkflow , workflowDir , token ) ) ,
2023-03-29 00:59:22 -05:00
newReusableWorkflowExecutor ( rc , workflowDir , remoteReusableWorkflow . FilePath ( ) ) ,
)
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-06-16 00:12:43 -05:00
remoteReusableWorkflow := newRemoteReusableWorkflowWithPlat ( rc . Config . GitHubInstance , uses )
2023-01-19 14:49:11 -06:00
if remoteReusableWorkflow == nil {
2023-03-29 00:59:22 -05:00
return common . NewErrorExecutor ( fmt . Errorf ( "expected format {owner}/{repo}/.{git_platform}/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
2023-04-06 01:16:20 -05:00
// FIXME: if the reusable workflow is from a private repository, we need to provide a token to access the repository.
token := ""
2023-01-19 14:49:11 -06:00
return common . NewPipelineExecutor (
2023-04-06 01:16:20 -05:00
newMutexExecutor ( cloneIfRequired ( rc , * remoteReusableWorkflow , workflowDir , token ) ) ,
2023-03-29 00:59:22 -05:00
newReusableWorkflowExecutor ( rc , workflowDir , remoteReusableWorkflow . FilePath ( ) ) ,
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
}
2023-04-06 01:16:20 -05:00
func cloneIfRequired ( rc * RunContext , remoteReusableWorkflow remoteReusableWorkflow , targetDirectory , token string ) common . Executor {
2023-01-19 14:49:11 -06:00
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 {
2023-06-16 00:12:43 -05:00
// Do not change the remoteReusableWorkflow.URL, because:
// 1. Gitea doesn't support specifying GithubContext.ServerURL by the GITHUB_SERVER_URL env
// 2. Gitea has already full URL with rc.Config.GitHubInstance when calling newRemoteReusableWorkflowWithPlat
// remoteReusableWorkflow.URL = rc.getGithubContext(ctx).ServerURL
2023-05-03 10:46:28 -05:00
return git . NewGitCloneExecutor ( git . NewGitCloneExecutorInput {
URL : remoteReusableWorkflow . CloneURL ( ) ,
Ref : remoteReusableWorkflow . Ref ,
Dir : targetDirectory ,
2023-06-15 21:57:31 -05:00
Token : token ,
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
2023-03-29 00:59:22 -05:00
GitPlatform string
2023-01-19 14:49:11 -06:00
}
func ( r * remoteReusableWorkflow ) CloneURL ( ) string {
2023-03-29 00:59:22 -05:00
// In Gitea, r.URL always has the protocol prefix, we don't need to add extra prefix in this case.
if strings . HasPrefix ( r . URL , "http://" ) || strings . HasPrefix ( r . URL , "https://" ) {
return fmt . Sprintf ( "%s/%s/%s" , r . URL , r . Org , r . Repo )
}
2023-01-19 14:49:11 -06:00
return fmt . Sprintf ( "https://%s/%s/%s" , r . URL , r . Org , r . Repo )
}
2023-03-29 00:59:22 -05:00
func ( r * remoteReusableWorkflow ) FilePath ( ) string {
return fmt . Sprintf ( "./.%s/workflows/%s" , r . GitPlatform , r . Filename )
}
2023-06-16 00:12:43 -05:00
// For Gitea
// newRemoteReusableWorkflowWithPlat create a `remoteReusableWorkflow`
// workflows from `.gitea/workflows` and `.github/workflows` are supported
func newRemoteReusableWorkflowWithPlat ( url , uses string ) * remoteReusableWorkflow {
2023-03-29 00:59:22 -05:00
// GitHub docs:
// https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses
r := regexp . MustCompile ( ` ^([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$ ` )
matches := r . FindStringSubmatch ( uses )
if len ( matches ) != 6 {
return nil
}
return & remoteReusableWorkflow {
Org : matches [ 1 ] ,
Repo : matches [ 2 ] ,
GitPlatform : matches [ 3 ] ,
Filename : matches [ 4 ] ,
Ref : matches [ 5 ] ,
2023-06-16 00:12:43 -05:00
URL : url ,
2023-03-29 00:59:22 -05:00
}
}
// deprecated: use newRemoteReusableWorkflowWithPlat
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
}
}