Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker/Dockerfile.linux.amd64
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
FROM docker:dind
FROM docker:29.6.0-dind

ENV DOCKER_HOST=unix:///var/run/docker.sock

Expand Down
14 changes: 9 additions & 5 deletions plugin.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,11 +51,11 @@ func (p Plugin) Exec() error {
}

ctx := context.Background()
repoURL, ref, ok := utils.ParseLookup(p.Action.Uses)
repoURL, ref, actionPath, ok := utils.ParseLookup(p.Action.Uses)
if !ok {
logrus.Warnf("Invalid 'uses' format: %s", p.Action.Uses)
}
logrus.Infof("Parsed 'uses' string. Repo: %s, Ref: %s", repoURL, ref)
logrus.Infof("Parsed 'uses' string. Repo: %s, Ref: %s, Path: %s", repoURL, ref, actionPath)

// Clone the GH Action repository using `cloner` with parsed repo and ref
clone := cloner.NewCache(cloner.NewDefault())
Expand All@@ -70,10 +70,14 @@ func (p Plugin) Exec() error {
outputVars := []string{}

if codedir != "" {
var err error
outputVars, err = utils.ParseActionOutputs(codedir)
actionDir, err := utils.ActionDir(codedir, actionPath)
if err != nil {
logrus.Warnf("Could not parse action.yml outputs from %s: %v", codedir, err)
logrus.Warnf("Invalid action path %q: %v", actionPath, err)
} else {
outputVars, err = utils.ParseActionOutputs(actionDir)
if err != nil {
logrus.Warnf("Could not parse action.yml outputs from %s: %v", actionDir, err)
}
}
}

Expand Down
42 changes: 34 additions & 8 deletions utils/parse.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,14 +60,40 @@ func fileExists(path string) bool {
return !info.IsDir()
}

// ActionDir joins cloneDir with an optional action subdirectory.
// Returns an error if actionPath escapes cloneDir.
func ActionDir(cloneDir, actionPath string) (string, error) {
if actionPath == "" {
return cloneDir, nil
}

clean := filepath.Clean(actionPath)
if clean == "." || clean == "" {
return cloneDir, nil
}
if filepath.IsAbs(clean) || clean == ".." || strings.HasPrefix(clean, ".."+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid action path: %s", actionPath)
}

actionDir := filepath.Join(cloneDir, clean)
rel, err := filepath.Rel(cloneDir, actionDir)
if err != nil {
return "", fmt.Errorf("invalid action path: %w", err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return "", fmt.Errorf("action path escapes clone dir: %s", actionPath)
}
return actionDir, nil
}

// ParseLookup parses the step string and returns the
// associated repositoryand ref.
func ParseLookup(s string) (repo string, ref string, ok bool) {
org, repo, _, ref, err := parseActionName(s)
// associated repository, ref, and optional action subdirectory path.
func ParseLookup(s string) (repo string, ref string, path string, ok bool) {
org, repoName, actionPath, ref, err := parseActionName(s)
if err == nil {
url := fmt.Sprintf("https://github.com/%s/%s", org, repo)
slog.Debug(fmt.Sprintf("parsed repo: %s, ref: %s", url, ref))
return url, ref, true
url := fmt.Sprintf("https://github.com/%s/%s", org, repoName)
slog.Debug(fmt.Sprintf("parsed repo: %s, ref: %s, path: %s", url, ref, actionPath))
return url, ref, actionPath, true
}

slog.Warn(fmt.Sprintf("failed to parse action name: %s with err: %v", s, err))
Expand All@@ -77,9 +103,9 @@ func ParseLookup(s string) (repo string, ref string, ok bool) {

slog.Debug("parsed repo", s)
if parts := strings.SplitN(s, "@", 2); len(parts) == 2 {
return parts[0], parts[1], true
return parts[0], parts[1], "", true
}
return s, "", true
return s, "", "", true
}

func parseActionName(action string) (org, repo, path, ref string, err error) {
Expand Down
105 changes: 105 additions & 0 deletions utils/parse_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,3 +43,108 @@ outputs:
assert.NoError(t, err)
assert.Empty(t, outputs)
}

func TestParseLookup(t *testing.T) {
tests := []struct {
name string
uses string
repo string
ref string
path string
ok bool
}{
{
name: "root action",
uses: "mathieudutour/github-tag-action@v6.2",
repo: "https://github.com/mathieudutour/github-tag-action",
ref: "v6.2",
path: "",
ok: true,
},
{
name: "nested action path",
uses: "my-corp/my-up2-action-external-management/.github/actions/mathieudutour/github-tag-action@v1",
repo: "https://github.com/my-corp/my-up2-action-external-management",
ref: "v1",
path: ".github/actions/mathieudutour/github-tag-action",
ok: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo, ref, path, ok := ParseLookup(tt.uses)
assert.Equal(t, tt.ok, ok)
assert.Equal(t, tt.repo, repo)
assert.Equal(t, tt.ref, ref)
assert.Equal(t, tt.path, path)
})
}
}

func TestActionDir(t *testing.T) {
clone := t.TempDir()

dir, err := ActionDir(clone, "")
assert.NoError(t, err)
assert.Equal(t, clone, dir)

dir, err = ActionDir(clone, ".github/actions/foo")
assert.NoError(t, err)
assert.Equal(t, filepath.Join(clone, ".github/actions/foo"), dir)

_, err = ActionDir(clone, "../outside")
assert.Error(t, err)

// Traversal that only escapes after filepath.Clean
_, err = ActionDir(clone, "foo/../../etc")
assert.Error(t, err)

_, err = ActionDir(clone, "foo/../..")
assert.Error(t, err)

_, err = ActionDir(clone, "foo/bar/../../../outside")
assert.Error(t, err)

// Clean keeps the result inside cloneDir — should succeed
dir, err = ActionDir(clone, "foo/../.github/actions/bar")
assert.NoError(t, err)
assert.Equal(t, filepath.Join(clone, ".github/actions/bar"), dir)
}

func TestParseActionOutputsNested(t *testing.T) {
clone := t.TempDir()
nested := filepath.Join(clone, ".github", "actions", "tag")
assert.NoError(t, os.MkdirAll(nested, 0755))

content := `
outputs:
new_tag:
description: "Generated tag"
new_version:
description: "Generated version"
`
assert.NoError(t, os.WriteFile(filepath.Join(nested, "action.yml"), []byte(content), 0644))

actionDir, err := ActionDir(clone, ".github/actions/tag")
assert.NoError(t, err)

outputs, err := ParseActionOutputs(actionDir)
assert.NoError(t, err)
assert.ElementsMatch(t, outputs, []string{"new_tag", "new_version"})

// Root still empty when only nested action.yml exists
outputs, err = ParseActionOutputs(clone)
assert.NoError(t, err)
assert.Empty(t, outputs)
}

func TestParseActionOutputsMissingNested(t *testing.T) {
clone := t.TempDir()
actionDir, err := ActionDir(clone, ".github/actions/missing")
assert.NoError(t, err)

outputs, err := ParseActionOutputs(actionDir)
assert.NoError(t, err)
assert.Empty(t, outputs)
}