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 )
remoteReusableWorkflow := newRemoteReusableWorkflowWithPlat ( uses )
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 ) )
}
remoteReusableWorkflow . URL = rc . Config . GitHubInstance
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-03-29 00:59:22 -05:00
remoteReusableWorkflow := newRemoteReusableWorkflowWithPlat ( 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
remoteReusableWorkflow . URL = rc . Config . GitHubInstance
2023-03-03 02:38:33 -06:00
workflowDir := fmt . Sprintf ( "%s/%s" , rc . ActionCacheDir ( ) , safeFilename ( uses ) )
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
} ,
git . NewGitCloneExecutor ( git . NewGitCloneExecutorInput {
URL : remoteReusableWorkflow . CloneURL ( ) ,
Ref : remoteReusableWorkflow . Ref ,
Dir : targetDirectory ,
2023-04-06 01:16:20 -05:00
Token : token ,
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 )
}
func newRemoteReusableWorkflowWithPlat ( uses string ) * remoteReusableWorkflow {
// 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 ] ,
}
}
// 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 ] ,
URL : "github.com" ,
}
}