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
23 changes: 20 additions & 3 deletions cmd/gh-actions-pin/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ $ gh actions-pin --no-fix --json=valid,findings
// from the existing lockfile so repeat scans short-circuit the per-branch
// Compare walk. newResolver is the DI seam; pass nil for production wiring.
func newRun(workflowPaths []string, hostname string, pool *pinpool.Pool, newResolver resolverFunc) ([]string, *resolve.Resolver, *lockfile.State, error) {
paths, err := discoverWorkflowPaths(workflowPaths)
workflowsDir := os.Getenv("GH_ACTIONS_PIN_WORKFLOWS_DIR")
paths, err := discoverWorkflowPaths(workflowPaths, workflowsDir)
if err != nil {
return nil, nil, nil, err
}
Expand All @@ -139,7 +140,12 @@ func newRun(workflowPaths []string, hostname string, pool *pinpool.Pool, newReso
return nil, nil, nil, err
}

store, err := lockfile.LoadState(".", r)
var store *lockfile.State
if workflowsDir != "" {
store, err = lockfile.LoadStateAt(filepath.Join(workflowsDir, "actions.lock"), r)
} else {
store, err = lockfile.LoadState(".", r)
}
if err != nil {
return nil, nil, nil, fmt.Errorf("opening lockfile: %w", err)
}
Expand All @@ -148,11 +154,22 @@ func newRun(workflowPaths []string, hostname string, pool *pinpool.Pool, newReso
return paths, r, store, nil
}

func discoverWorkflowPaths(existing []string) ([]string, error) {
func discoverWorkflowPaths(existing []string, workflowsDir string) ([]string, error) {
if len(existing) > 0 {
return expandWorkflowPaths(existing)
}

if workflowsDir != "" {
paths, err := workflowfile.DiscoverWorkflowsIn(workflowsDir)
if err != nil {
return nil, err
}
if len(paths) == 0 {
return nil, fmt.Errorf("no workflow files found in %s", workflowsDir)
}
return paths, nil
}
Comment on lines +162 to +171

paths, err := workflowfile.DiscoverWorkflows()
if err != nil {
return nil, err
Expand Down
16 changes: 11 additions & 5 deletions internal/lockfile/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ type MetadataResolver interface {
// a single store instance without external synchronization.
type State struct {
mu sync.Mutex
repoRoot string
lockPath string // full path to actions.lock on disk
file parserlock.File
meta MetadataResolver
idCache map[string][2]int64
Expand All @@ -48,8 +48,14 @@ type State struct {
// LoadState reads the lockfile at repoRoot, returning an empty in-memory file
// when none exists on disk.
func LoadState(repoRoot string, meta MetadataResolver) (*State, error) {
full := filepath.Join(repoRoot, parserlock.Path)
contents, err := os.ReadFile(full)
return LoadStateAt(filepath.Join(repoRoot, parserlock.Path), meta)
}

// LoadStateAt reads the lockfile at the given path, returning an empty
// in-memory file when none exists on disk. Use this when the lockfile
// lives outside the standard .github/workflows/ location.
func LoadStateAt(lockfilePath string, meta MetadataResolver) (*State, error) {
contents, err := os.ReadFile(lockfilePath)

var file parserlock.File
switch {
Expand Down Expand Up @@ -88,7 +94,7 @@ func LoadState(repoRoot string, meta MetadataResolver) (*State, error) {
}

s := &State{
repoRoot: repoRoot,
lockPath: lockfilePath,
file: file,
meta: meta,
idCache: map[string][2]int64{},
Expand Down Expand Up @@ -360,7 +366,7 @@ func (s *State) Save() error {
}
}

full := filepath.Join(s.repoRoot, parserlock.Path)
full := s.lockPath

if len(s.file.Dependencies) == 0 && len(s.file.Workflows) == 0 {
if err := os.Remove(full); err != nil && !errors.Is(err, os.ErrNotExist) {
Expand Down
7 changes: 6 additions & 1 deletion internal/workflowfile/workflowfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ func (f *File) ExtractActionRefs() ([]parserlock.ActionRef, []string, []string)
// DiscoverWorkflows finds all workflow files in .github/workflows/ relative to
// the current directory. Returns nil if the directory doesn't exist.
func DiscoverWorkflows() ([]string, error) {
dir := filepath.Join(".github", "workflows")
return DiscoverWorkflowsIn(filepath.Join(".github", "workflows"))
}

// DiscoverWorkflowsIn finds all workflow files (*.yml, *.yaml) in dir.
// Returns nil if the directory doesn't exist.
func DiscoverWorkflowsIn(dir string) ([]string, error) {
entries, err := os.ReadDir(dir)
if os.IsNotExist(err) {
return nil, nil
Expand Down
19 changes: 19 additions & 0 deletions internal/workflowfile/workflowfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ func TestExtractActionRefsMixed(t *testing.T) {
assert.Contains(t, warnings[0], "expression-based")
}

func TestDiscoverWorkflowsIn(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "ci.yml"), []byte("name: ci\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "deploy.yaml"), []byte("name: deploy\n"), 0o644))
require.NoError(t, os.WriteFile(filepath.Join(dir, "README.md"), []byte("# ignore\n"), 0o644))

paths, err := DiscoverWorkflowsIn(dir)
require.NoError(t, err)
assert.Len(t, paths, 2)
assert.Equal(t, filepath.Join(dir, "ci.yml"), paths[0])
assert.Equal(t, filepath.Join(dir, "deploy.yaml"), paths[1])
}

func TestDiscoverWorkflowsIn_MissingDir(t *testing.T) {
paths, err := DiscoverWorkflowsIn(filepath.Join(t.TempDir(), "nope"))
require.NoError(t, err)
assert.Nil(t, paths)
}

func TestExtractLocalCompositeRefs_RejectsPathTraversal(t *testing.T) {
repoRoot := t.TempDir()
require.NoError(t, os.Mkdir(filepath.Join(repoRoot, ".git"), 0o755))
Expand Down