Skip to content
This repository was archived by the owner on Feb 12, 2026. It is now read-only.

Commit 308975d

Browse files
authored
feat: Add tests for GitHub Actions digest pinner and implement finder/parser interfaces (#9)
* feat: Add tests for GitHub Actions digest pinner and implement finder/parser interfaces - Created comprehensive tests for the GitHub Actions digest pinner, covering version, scan, and update commands. - Implemented mock structures for GitHub client, finder, parser, updater, and file handling to facilitate testing. - Added DefaultFinder and DefaultParser interfaces to encapsulate workflow file finding and action parsing logic. * refactor: Enhance application structure and improve file handling in GitHub Actions digest pinner
1 parent b9fb397 commit 308975d

10 files changed

Lines changed: 581 additions & 107 deletions

File tree

cmd/github-actions-digest-pinner/main.go

Lines changed: 192 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"io/fs"
78
"log"
89
"os"
10+
"path/filepath"
911
"time"
1012

1113
"github.com/spf13/cobra"
1214
"github.com/zisuu/github-actions-digest-pinner/internal/finder"
1315
"github.com/zisuu/github-actions-digest-pinner/internal/ghclient"
1416
"github.com/zisuu/github-actions-digest-pinner/internal/parser"
1517
"github.com/zisuu/github-actions-digest-pinner/internal/updater"
18+
"github.com/zisuu/github-actions-digest-pinner/pgk/types"
1619
)
1720

1821
// Build number and versions injected at compile time
@@ -22,150 +25,242 @@ var (
2225
date = "unknown"
2326
)
2427

25-
// Centralized error handling function
26-
func handleError(err error, message string, exit bool) {
28+
type WorkflowFinder interface {
29+
FindWorkflowFiles(fsys fs.FS) ([]string, error)
30+
}
31+
32+
type WorkflowParser interface {
33+
ParseWorkflowActions(content []byte) ([]types.ActionRef, error)
34+
}
35+
36+
type WorkflowUpdater interface {
37+
UpdateWorkflows(ctx context.Context, fsys fs.FS) (int, error)
38+
}
39+
40+
// App represents the main application structure.
41+
type App struct {
42+
Out io.Writer
43+
Err io.Writer
44+
Client ghclient.GitHubClient
45+
Finder WorkflowFinder
46+
Parser WorkflowParser
47+
Updater WorkflowUpdater
48+
FS func(dir string) fs.FS
49+
ReadFile func(fsys fs.FS, name string) ([]byte, error)
50+
}
51+
52+
// NewApp creates a new instance of App with the provided output and error writers.
53+
func NewApp(out, err io.Writer) *App {
54+
return &App{
55+
Out: out,
56+
Err: err,
57+
Client: ghclient.NewGitHubClient(),
58+
Finder: finder.DefaultFinder{},
59+
Parser: parser.DefaultParser{},
60+
Updater: updater.NewUpdater(ghclient.NewGitHubClient()),
61+
FS: func(dir string) fs.FS {
62+
return os.DirFS(dir)
63+
},
64+
ReadFile: fs.ReadFile,
65+
}
66+
}
67+
68+
// scanCommand scans the specified directory for GitHub Actions workflows and prints the actions found.
69+
func (a *App) scanCommand(dir string, verbose bool) error {
70+
if verbose {
71+
log.SetOutput(a.Err)
72+
log.Println("Starting GitHub Actions digest pinner utility")
73+
log.Printf("Scanning directory: %s", dir)
74+
}
75+
76+
fsys := a.FS(dir)
77+
78+
if verbose {
79+
log.Println("Finding workflow files...")
80+
}
81+
82+
files, err := a.Finder.FindWorkflowFiles(fsys)
2783
if err != nil {
28-
log.Printf("%s: %v", message, err)
29-
if exit {
30-
os.Exit(1)
84+
return fmt.Errorf("failed to find workflow files: %w", err)
85+
}
86+
87+
if verbose {
88+
log.Printf("Found %d workflow files", len(files))
89+
log.Println("Parsing actions in workflow files...")
90+
}
91+
92+
for _, file := range files {
93+
if verbose {
94+
log.Printf("Processing file: %s", file)
95+
}
96+
97+
fileContent, err := a.ReadFile(fsys, file)
98+
if err != nil {
99+
return fmt.Errorf("failed to read content of file %s: %w", file, err)
100+
}
101+
102+
actions, err := a.Parser.ParseWorkflowActions(fileContent)
103+
if err != nil {
104+
return fmt.Errorf("failed to parse actions in file %s: %w", file, err)
105+
}
106+
107+
if verbose {
108+
log.Printf("Found %d actions in file %s", len(actions), file)
109+
for _, action := range actions {
110+
_, err := fmt.Fprintf(a.Out, "- Action: %s/%s@%s\n", action.Owner, action.Repo, action.Ref)
111+
if err != nil {
112+
return fmt.Errorf("failed to write action output: %w", err)
113+
}
114+
}
115+
} else if len(actions) > 0 {
116+
_, err := fmt.Fprintf(a.Out, "%s: %d actions found\n", file, len(actions))
117+
if err != nil {
118+
return fmt.Errorf("failed to write actions found output: %w", err)
119+
}
31120
}
32121
}
122+
123+
return nil
33124
}
34125

35-
func main() {
36-
rootCmd := &cobra.Command{
126+
// updateCommand updates the GitHub Actions workflows in the specified directory to use pinned digests.
127+
func (a *App) updateCommand(dir string, timeout int, verbose bool) error {
128+
start := time.Now()
129+
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
130+
defer cancel()
131+
132+
if verbose {
133+
log.SetOutput(a.Err)
134+
log.Println("Starting GitHub Actions digest pinner utility")
135+
log.Printf("Scanning directory: %s", dir)
136+
}
137+
138+
absDir, err := filepath.Abs(dir)
139+
if err != nil {
140+
return fmt.Errorf("failed to get absolute path: %w", err)
141+
}
142+
143+
fsys := a.FS(absDir)
144+
145+
if verbose {
146+
log.Println("Finding workflow files...")
147+
}
148+
149+
files, err := a.Finder.FindWorkflowFiles(fsys)
150+
if err != nil {
151+
return fmt.Errorf("failed to find workflow files: %w", err)
152+
}
153+
154+
if verbose {
155+
log.Printf("Found %d workflow files", len(files))
156+
}
157+
158+
if upd, ok := a.Updater.(*updater.Updater); ok {
159+
upd.SetBaseDir(absDir)
160+
}
161+
162+
totalUpdates, err := a.Updater.UpdateWorkflows(ctx, fsys)
163+
if err != nil {
164+
return fmt.Errorf("failed to update workflows: %w", err)
165+
}
166+
167+
if verbose {
168+
log.Printf("Updated %d action references in %v", totalUpdates, time.Since(start).Round(time.Millisecond))
169+
for _, file := range files {
170+
_, err := fmt.Fprintf(a.Out, "- Processed: %s\n", file)
171+
if err != nil {
172+
return fmt.Errorf("failed to write processed file output: %w", err)
173+
}
174+
}
175+
} else {
176+
_, err := fmt.Fprintf(a.Out, "Updated %d action references in %v\n", totalUpdates, time.Since(start).Round(time.Millisecond))
177+
if err != nil {
178+
return fmt.Errorf("failed to write update summary output: %w", err)
179+
}
180+
}
181+
182+
return nil
183+
}
184+
185+
// versionCommand prints the version information of the application.
186+
func (a *App) versionCommand() {
187+
_, err := fmt.Fprintf(a.Out, "Version: %s\nCommit: %s\nDate: %s\n", version, commit, date)
188+
if err != nil {
189+
log.Printf("Failed to write version output: %v", err)
190+
}
191+
}
192+
193+
// newRootCommand creates the root command for the CLI application.
194+
func newRootCommand(app *App) *cobra.Command {
195+
cmd := &cobra.Command{
37196
Use: "github-actions-digest-pinner",
38197
Short: "A tool to pin GitHub Actions to specific digests",
39-
Long: `GitHub Actions Digest Pinner is a tool to help you pin
40-
GitHub Actions to specific digests for better security and reliability.`,
198+
Long: "GitHub Actions Digest Pinner is a tool to help you pin GitHub Actions to specific digests for better security and reliability.",
41199
Run: func(cmd *cobra.Command, args []string) {
42-
err := cmd.Help()
43-
handleError(err, "Failed to display help", true)
200+
if err := cmd.Help(); err != nil {
201+
log.Printf("Failed to display help: %v", err)
202+
os.Exit(1)
203+
}
44204
},
45205
}
46206

47-
// Add version command
48-
rootCmd.AddCommand(&cobra.Command{
207+
cmd.AddCommand(&cobra.Command{
49208
Use: "version",
50209
Short: "Show the version information",
51210
Run: func(cmd *cobra.Command, args []string) {
52-
fmt.Printf("Version: %s\nCommit: %s\nDate: %s\n", version, commit, date)
211+
app.versionCommand()
53212
},
54213
})
55214

56-
// Add scan command
57215
scanCmd := &cobra.Command{
58216
Use: "scan",
59217
Short: "Scan the repository for GitHub Actions workflows",
60218
Run: func(cmd *cobra.Command, args []string) {
61219
dir, _ := cmd.Flags().GetString("dir")
62220
verbose, _ := cmd.Flags().GetBool("verbose")
63-
64-
if verbose {
65-
log.Println("Starting GitHub Actions digest pinner utility")
66-
log.Printf("Scanning directory: %s", dir)
67-
}
68-
69-
fsys := os.DirFS(dir)
70-
71-
if verbose {
72-
log.Println("Finding workflow files...")
73-
}
74-
75-
files, err := finder.FindWorkflowFiles(fsys)
76-
handleError(err, "Failed to find workflow files", true)
77-
78-
if verbose {
79-
log.Printf("Found %d workflow files", len(files))
80-
log.Println("Parsing actions in workflow files...")
81-
}
82-
83-
for _, file := range files {
84-
if verbose {
85-
log.Printf("Processing file: %s", file)
86-
}
87-
88-
content, err := fsys.Open(file)
89-
handleError(err, fmt.Sprintf("Failed to read file %s", file), true)
90-
91-
fileContent, err := io.ReadAll(content)
92-
handleError(err, fmt.Sprintf("Failed to read content of file %s", file), true)
93-
94-
actions, err := parser.ParseWorkflowActions(fileContent)
95-
handleError(err, fmt.Sprintf("Failed to parse actions in file %s", file), true)
96-
97-
if verbose {
98-
log.Printf("Found %d actions in file %s", len(actions), file)
99-
for _, action := range actions {
100-
fmt.Printf("- Action: %s/%s@%s\n", action.Owner, action.Repo, action.Ref)
101-
}
102-
} else if len(actions) > 0 {
103-
fmt.Printf("%s: %d actions found\n", file, len(actions))
104-
}
221+
if err := app.scanCommand(dir, verbose); err != nil {
222+
log.Printf("Scan failed: %v", err)
223+
os.Exit(1)
105224
}
106225
},
107226
}
108227

109228
scanCmd.Flags().String("dir", ".", "Directory containing GitHub workflows")
110229
scanCmd.Flags().Bool("verbose", false, "Verbose output")
230+
cmd.AddCommand(scanCmd)
111231

112-
rootCmd.AddCommand(scanCmd)
113-
114-
// Add update command
115232
updateCmd := &cobra.Command{
116233
Use: "update",
117234
Short: "Update GitHub Actions workflows to use pinned digests",
118235
Run: func(cmd *cobra.Command, args []string) {
119236
dir, _ := cmd.Flags().GetString("dir")
120237
timeout, _ := cmd.Flags().GetInt("timeout")
121238
verbose, _ := cmd.Flags().GetBool("verbose")
122-
123-
start := time.Now()
124-
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
125-
defer cancel()
126-
127-
if verbose {
128-
log.Println("Starting GitHub Actions digest pinner utility")
129-
log.Printf("Scanning directory: %s", dir)
130-
}
131-
132-
client := ghclient.NewGitHubClient()
133-
workflowUpdater := updater.NewUpdater(client, dir)
134-
fsys := os.DirFS(dir)
135-
136-
if verbose {
137-
log.Println("Finding workflow files...")
138-
}
139-
140-
files, err := finder.FindWorkflowFiles(fsys)
141-
handleError(err, "Failed to find workflow files", true)
142-
143-
if verbose {
144-
log.Printf("Found %d workflow files", len(files))
145-
}
146-
147-
totalUpdates, err := workflowUpdater.UpdateWorkflows(ctx, fsys)
148-
handleError(err, "Failed to update workflows", true)
149-
150-
if verbose {
151-
log.Printf("Updated %d action references in %v", totalUpdates, time.Since(start).Round(time.Millisecond))
152-
for _, file := range files {
153-
fmt.Printf("- Processed: %s\n", file)
154-
}
155-
} else {
156-
fmt.Printf("Updated %d action references in %v\n", totalUpdates, time.Since(start).Round(time.Millisecond))
239+
if err := app.updateCommand(dir, timeout, verbose); err != nil {
240+
log.Printf("Update failed: %v", err)
241+
os.Exit(1)
157242
}
158243
},
159244
}
160245

161246
updateCmd.Flags().String("dir", ".", "Directory containing GitHub workflows")
162247
updateCmd.Flags().Int("timeout", 30, "API timeout in seconds")
163248
updateCmd.Flags().Bool("verbose", false, "Verbose output")
249+
cmd.AddCommand(updateCmd)
164250

165-
rootCmd.AddCommand(updateCmd)
251+
return cmd
252+
}
253+
254+
// main is the entry point of the application.
255+
func main() {
256+
app := NewApp(os.Stdout, os.Stderr)
257+
rootCmd := newRootCommand(app)
166258

167259
if err := rootCmd.Execute(); err != nil {
168-
fmt.Println(err)
260+
_, fmtErr := fmt.Fprintln(app.Err, err)
261+
if fmtErr != nil {
262+
log.Printf("Failed to write error output: %v", fmtErr)
263+
}
169264
os.Exit(1)
170265
}
171266
}

0 commit comments

Comments
 (0)