@@ -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\n Commit: %s\n Date: %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 \n Commit: %s \n Date: %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