From 874dbaf8d55c2d1c0906fa34e3721cc245e7abf2 Mon Sep 17 00:00:00 2001 From: Ali AKCA Date: Wed, 23 Aug 2023 23:24:37 +0200 Subject: [PATCH] refactor: move load custom actions to core --- internal/core/custom_action.go | 165 +++++++++++++++++ .../core}/custom_action_test.go | 8 +- tools/ghx/actions/custom_action.go | 174 ------------------ tools/ghx/runner/runner_step.go | 3 +- 4 files changed, 170 insertions(+), 180 deletions(-) rename {tools/ghx/actions => internal/core}/custom_action_test.go (89%) delete mode 100644 tools/ghx/actions/custom_action.go diff --git a/internal/core/custom_action.go b/internal/core/custom_action.go index 3a836e4d..bab94cbc 100644 --- a/internal/core/custom_action.go +++ b/internal/core/custom_action.go @@ -1,10 +1,20 @@ package core import ( + "context" "fmt" + "path" + "path/filepath" + "regexp" "strings" "dagger.io/dagger" + + "gopkg.in/yaml.v3" + + "github.com/aweris/gale/internal/config" + "github.com/aweris/gale/internal/fs" + "github.com/aweris/gale/internal/log" ) type CustomAction struct { @@ -147,3 +157,158 @@ func (c *CustomActionRuns) PostCondition() (bool, string) { return true, c.PostIf } + +// LoadActionFromSource loads an action from given source. Source can be a local directory or a remote repository. +func LoadActionFromSource(ctx context.Context, source string) (*CustomAction, error) { + var target string + + // no need to load action if it is a local action + if isLocalAction(source) { + target = source + } else { + target = filepath.Join(config.GhxActionsDir(), source) + + // ensure action exists locally + if err := ensureActionExistsLocally(ctx, source, target); err != nil { + return nil, err + } + } + + dir, err := getActionDirectory(target) + if err != nil { + return nil, err + } + + meta, err := getCustomActionMeta(ctx, dir) + if err != nil { + return nil, err + } + + return &CustomAction{Meta: meta, Path: target, Dir: dir}, nil +} + +// isLocalAction checks if the given source is a local action +func isLocalAction(source string) bool { + return strings.HasPrefix(source, "./") || filepath.IsAbs(source) || strings.HasPrefix(source, "/") +} + +// ensureActionExistsLocally ensures that the action exists locally. If the action does not exist locally, it will be +// downloaded from the source to the target directory. +func ensureActionExistsLocally(ctx context.Context, source, target string) error { + // check if action exists locally + exist, err := fs.Exists(target) + if err != nil { + return fmt.Errorf("failed to check if action exists locally: %w", err) + } + + // do nothing if target path already exists + if exist { + log.Debugf("action already exists locally", "source", source, "target", target) + return nil + } + + log.Debugf("action does not exist locally, downloading...", "source", source, "target", target) + + dir, err := getActionDirectory(source) + if err != nil { + return err + } + + // export the action to the target directory + _, err = dir.Export(ctx, target) + if err != nil { + return err + } + + return nil +} + +// getCustomActionMeta returns the meta information about the custom action from the action directory. +func getCustomActionMeta(ctx context.Context, actionDir *dagger.Directory) (*CustomActionMeta, error) { + var meta CustomActionMeta + + file, err := findActionMetadataFileName(ctx, actionDir) + if err != nil { + return nil, err + } + + content, err := actionDir.File(file).Contents(ctx) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal([]byte(content), &meta) + if err != nil { + return nil, err + } + + return &meta, nil +} + +// getActionDirectory returns the directory of the action from given source. +func getActionDirectory(source string) (*dagger.Directory, error) { + // if path is relative, use the host to resolve the path + if isLocalAction(source) { + return config.Client().Host().Directory(source), nil + } + + // if path is not a relative path, it must be a remote repository in the format "{owner}/{repo}/{path}@{ref}" + // if {path} is not present in the input string, an empty string is returned for the path component. + actionRepo, actionPath, actionRef, err := parseRepoRef(source) + if err != nil { + return nil, fmt.Errorf("failed to parse repo ref %s: %v", source, err) + } + + // if path is empty, use the root of the repo as the action directory + if actionPath == "" { + actionPath = "." + } + + // TODO: handle enterprise github instances as well + // TODO: handle ref type (branch, tag, commit) currently only tags are supported + return config.Client().Git(path.Join("github.com", actionRepo)).Tag(actionRef).Tree().Directory(actionPath), nil +} + +// findActionMetadataFileName finds the action.yml or action.yaml file in the root of the action directory. +func findActionMetadataFileName(ctx context.Context, dir *dagger.Directory) (string, error) { + // list all entries in the root of the action directory + entries, entriesErr := dir.Entries(ctx) + if entriesErr != nil { + return "", fmt.Errorf("failed to list entries for: %v", entriesErr) + } + + file := "" + + // find action.yml or action.yaml exists in the root of the action repo + for _, entry := range entries { + if entry == "action.yml" || entry == "action.yaml" { + file = entry + break + } + } + + // if action.yml or action.yaml does not exist, return an error + if file == "" { + return "", fmt.Errorf("action.yml or action.yaml not found in the root of the action directory") + } + + return file, nil +} + +// parseRepoRef parses a string in the format "{owner}/{repo}/{path}@{ref}" and returns the parsed components. +// If {path} is not present in the input string, an empty string is returned for the path component. +func parseRepoRef(input string) (repo string, path string, ref string, err error) { + regex := regexp.MustCompile(`^([^/]+)/([^/@]+)(?:/([^@]+))?@(.+)$`) + matches := regex.FindStringSubmatch(input) + + if len(matches) == 0 { + err = fmt.Errorf("invalid input format: %q", input) + return + } + + repo = strings.Join([]string{matches[1], matches[2]}, "/") + path = matches[3] + ref = matches[4] + + return +} diff --git a/tools/ghx/actions/custom_action_test.go b/internal/core/custom_action_test.go similarity index 89% rename from tools/ghx/actions/custom_action_test.go rename to internal/core/custom_action_test.go index c8d508e3..481ed251 100644 --- a/tools/ghx/actions/custom_action_test.go +++ b/internal/core/custom_action_test.go @@ -1,4 +1,4 @@ -package actions_test +package core_test import ( "context" @@ -9,7 +9,7 @@ import ( "dagger.io/dagger" "github.com/aweris/gale/internal/config" - "github.com/aweris/gale/tools/ghx/actions" + "github.com/aweris/gale/internal/core" ) func TestCustomActionManager_GetCustomAction(t *testing.T) { @@ -31,7 +31,7 @@ func TestCustomActionManager_GetCustomAction(t *testing.T) { config.SetClient(client) t.Run("download missing action", func(t *testing.T) { - ca, err := actions.LoadActionFromSource(ctx, "actions/checkout@v2") + ca, err := core.LoadActionFromSource(ctx, "actions/checkout@v2") if err != nil { t.Fatal(err) } @@ -67,7 +67,7 @@ func TestCustomActionManager_GetCustomAction(t *testing.T) { t.Fatal(err) } - ca, err := actions.LoadActionFromSource(ctx, "some/action@v1") + ca, err := core.LoadActionFromSource(ctx, "some/action@v1") if err != nil { t.Fatal(err) } diff --git a/tools/ghx/actions/custom_action.go b/tools/ghx/actions/custom_action.go deleted file mode 100644 index 5f70c03c..00000000 --- a/tools/ghx/actions/custom_action.go +++ /dev/null @@ -1,174 +0,0 @@ -package actions - -import ( - "context" - "fmt" - "path" - "path/filepath" - "regexp" - "strings" - - "dagger.io/dagger" - - "gopkg.in/yaml.v3" - - "github.com/aweris/gale/internal/config" - "github.com/aweris/gale/internal/core" - "github.com/aweris/gale/internal/fs" - "github.com/aweris/gale/internal/log" -) - -// LoadActionFromSource loads an action from given source. Source can be a local directory or a remote repository. -func LoadActionFromSource(ctx context.Context, source string) (*core.CustomAction, error) { - var target string - - // no need to load action if it is a local action - if isLocalAction(source) { - target = source - } else { - target = filepath.Join(config.GhxActionsDir(), source) - - // ensure action exists locally - if err := ensureActionExistsLocally(ctx, source, target); err != nil { - return nil, err - } - } - - dir, err := getActionDirectory(target) - if err != nil { - return nil, err - } - - meta, err := getCustomActionMeta(ctx, dir) - if err != nil { - return nil, err - } - - return &core.CustomAction{Meta: meta, Path: target, Dir: dir}, nil -} - -// isLocalAction checks if the given source is a local action -func isLocalAction(source string) bool { - return strings.HasPrefix(source, "./") || filepath.IsAbs(source) || strings.HasPrefix(source, "/") -} - -// ensureActionExistsLocally ensures that the action exists locally. If the action does not exist locally, it will be -// downloaded from the source to the target directory. -func ensureActionExistsLocally(ctx context.Context, source, target string) error { - // check if action exists locally - exist, err := fs.Exists(target) - if err != nil { - return fmt.Errorf("failed to check if action exists locally: %w", err) - } - - // do nothing if target path already exists - if exist { - log.Debugf("action already exists locally", "source", source, "target", target) - return nil - } - - log.Debugf("action does not exist locally, downloading...", "source", source, "target", target) - - dir, err := getActionDirectory(source) - if err != nil { - return err - } - - // export the action to the target directory - _, err = dir.Export(ctx, target) - if err != nil { - return err - } - - return nil -} - -// getCustomActionMeta returns the meta information about the custom action from the action directory. -func getCustomActionMeta(ctx context.Context, actionDir *dagger.Directory) (*core.CustomActionMeta, error) { - var meta core.CustomActionMeta - - file, err := findActionMetadataFileName(ctx, actionDir) - if err != nil { - return nil, err - } - - content, err := actionDir.File(file).Contents(ctx) - if err != nil { - return nil, err - } - - err = yaml.Unmarshal([]byte(content), &meta) - if err != nil { - return nil, err - } - - return &meta, nil -} - -// getActionDirectory returns the directory of the action from given source. -func getActionDirectory(source string) (*dagger.Directory, error) { - // if path is relative, use the host to resolve the path - if isLocalAction(source) { - return config.Client().Host().Directory(source), nil - } - - // if path is not a relative path, it must be a remote repository in the format "{owner}/{repo}/{path}@{ref}" - // if {path} is not present in the input string, an empty string is returned for the path component. - actionRepo, actionPath, actionRef, err := parseRepoRef(source) - if err != nil { - return nil, fmt.Errorf("failed to parse repo ref %s: %v", source, err) - } - - // if path is empty, use the root of the repo as the action directory - if actionPath == "" { - actionPath = "." - } - - // TODO: handle enterprise github instances as well - // TODO: handle ref type (branch, tag, commit) currently only tags are supported - return config.Client().Git(path.Join("github.com", actionRepo)).Tag(actionRef).Tree().Directory(actionPath), nil -} - -// findActionMetadataFileName finds the action.yml or action.yaml file in the root of the action directory. -func findActionMetadataFileName(ctx context.Context, dir *dagger.Directory) (string, error) { - // list all entries in the root of the action directory - entries, entriesErr := dir.Entries(ctx) - if entriesErr != nil { - return "", fmt.Errorf("failed to list entries for: %v", entriesErr) - } - - file := "" - - // find action.yml or action.yaml exists in the root of the action repo - for _, entry := range entries { - if entry == "action.yml" || entry == "action.yaml" { - file = entry - break - } - } - - // if action.yml or action.yaml does not exist, return an error - if file == "" { - return "", fmt.Errorf("action.yml or action.yaml not found in the root of the action directory") - } - - return file, nil -} - -// parseRepoRef parses a string in the format "{owner}/{repo}/{path}@{ref}" and returns the parsed components. -// If {path} is not present in the input string, an empty string is returned for the path component. -func parseRepoRef(input string) (repo string, path string, ref string, err error) { - regex := regexp.MustCompile(`^([^/]+)/([^/@]+)(?:/([^@]+))?@(.+)$`) - matches := regex.FindStringSubmatch(input) - - if len(matches) == 0 { - err = fmt.Errorf("invalid input format: %q", input) - return - } - - repo = strings.Join([]string{matches[1], matches[2]}, "/") - path = matches[3] - ref = matches[4] - - return -} diff --git a/tools/ghx/runner/runner_step.go b/tools/ghx/runner/runner_step.go index a34b7b16..6f6e6130 100644 --- a/tools/ghx/runner/runner_step.go +++ b/tools/ghx/runner/runner_step.go @@ -12,7 +12,6 @@ import ( "github.com/aweris/gale/internal/core" "github.com/aweris/gale/internal/fs" "github.com/aweris/gale/internal/log" - "github.com/aweris/gale/tools/ghx/actions" ) type Step interface { @@ -64,7 +63,7 @@ type StepAction struct { func (s *StepAction) setup() TaskExecutorFn { return func(ctx context.Context) (core.Conclusion, error) { - ca, err := actions.LoadActionFromSource(ctx, s.Step.Uses) + ca, err := core.LoadActionFromSource(ctx, s.Step.Uses) if err != nil { return core.ConclusionFailure, err }