99 "io"
1010 "io/ioutil"
1111 "net/http"
12- "path/filepath"
1312 "sort"
1413 "strconv"
1514 "strings"
@@ -32,18 +31,7 @@ type browser interface {
3231 Browse (string ) error
3332}
3433
35- type runLog map [string ]* job
36-
37- type job struct {
38- name string
39- steps []step
40- }
41-
42- type step struct {
43- order int
44- name string
45- logs string
46- }
34+ type runLog map [string ]* shared.Job
4735
4836type ViewOptions struct {
4937 HttpClient func () (* http.Client , error )
@@ -56,6 +44,7 @@ type ViewOptions struct {
5644 Verbose bool
5745 ExitStatus bool
5846 Log bool
47+ LogFailed bool
5948 Web bool
6049
6150 Prompt bool
@@ -118,6 +107,10 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
118107 return & cmdutil.FlagError {Err : errors .New ("specify only one of --web or --log" )}
119108 }
120109
110+ if opts .Log && opts .LogFailed {
111+ return & cmdutil.FlagError {Err : errors .New ("specify only one of --log or --log-failed" )}
112+ }
113+
121114 if runF != nil {
122115 return runF (opts )
123116 }
@@ -129,6 +122,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
129122 cmd .Flags ().BoolVar (& opts .ExitStatus , "exit-status" , false , "Exit with non-zero status if run failed" )
130123 cmd .Flags ().StringVarP (& opts .JobID , "job" , "j" , "" , "View a specific job ID from a run" )
131124 cmd .Flags ().BoolVar (& opts .Log , "log" , false , "View full log for either a run or specific job" )
125+ cmd .Flags ().BoolVar (& opts .LogFailed , "log-failed" , false , "View log of failed steps for either a run or specific job" )
132126 cmd .Flags ().BoolVarP (& opts .Web , "web" , "w" , false , "Open run in the browser" )
133127
134128 return cmd
@@ -215,63 +209,39 @@ func runView(opts *ViewOptions) error {
215209 return opts .Browser .Browse (url )
216210 }
217211
218- opts .IO .StartProgressIndicator ()
219-
220- if opts .Log && selectedJob != nil {
221- if selectedJob .Status != shared .Completed {
222- return fmt .Errorf ("job %d is still in progress; logs will be available when it is complete" , selectedJob .ID )
223- }
224-
225- r , err := getJobLog (httpClient , repo , selectedJob .ID )
226- if err != nil {
227- return err
228- }
212+ if selectedJob == nil && len (jobs ) == 0 {
213+ opts .IO .StartProgressIndicator ()
214+ jobs , err = shared .GetJobs (client , repo , * run )
229215 opts .IO .StopProgressIndicator ()
230-
231- err = opts .IO .StartPager ()
232216 if err != nil {
233- return err
234- }
235- defer opts .IO .StopPager ()
236-
237- if _ , err := io .Copy (opts .IO .Out , r ); err != nil {
238- return fmt .Errorf ("failed to read log: %w" , err )
217+ return fmt .Errorf ("failed to get jobs: %w" , err )
239218 }
219+ } else if selectedJob != nil {
220+ jobs = []shared.Job {* selectedJob }
221+ }
240222
241- if opts .ExitStatus && shared .IsFailureState (selectedJob .Conclusion ) {
242- return cmdutil .SilentError
223+ if opts .Log || opts .LogFailed {
224+ if selectedJob != nil && selectedJob .Status != shared .Completed {
225+ return fmt .Errorf ("job %d is still in progress; logs will be available when it is complete" , selectedJob .ID )
243226 }
244227
245- return nil
246- }
247-
248- if opts .Log {
249228 if run .Status != shared .Completed {
250229 return fmt .Errorf ("run %d is still in progress; logs will be available when it is complete" , run .ID )
251230 }
252231
232+ opts .IO .StartProgressIndicator ()
253233 runLogZip , err := getRunLog (httpClient , repo , run .ID )
234+ opts .IO .StopProgressIndicator ()
254235 if err != nil {
255236 return fmt .Errorf ("failed to get run log: %w" , err )
256237 }
257- opts .IO .StopProgressIndicator ()
258238
259- runLog , err : = readRunLog (runLogZip )
239+ err = readRunLog (runLogZip , jobs )
260240 if err != nil {
261241 return err
262242 }
263243
264- return displayRunLog (opts .IO , runLog )
265- }
266-
267- if selectedJob == nil && len (jobs ) == 0 {
268- jobs , err = shared .GetJobs (client , repo , * run )
269- opts .IO .StopProgressIndicator ()
270- if err != nil {
271- return fmt .Errorf ("failed to get jobs: %w" , err )
272- }
273- } else if selectedJob != nil {
274- jobs = []shared.Job {* selectedJob }
244+ return displayRunLog (opts .IO , jobs , opts .LogFailed )
275245 }
276246
277247 prNumber := ""
@@ -355,16 +325,24 @@ func runView(opts *ViewOptions) error {
355325 }
356326
357327 fmt .Fprintln (out )
358- fmt .Fprintln (out , "For more information about a job, try: gh run view --job=<job-id>" )
359- // TODO note about run view --log when that exists
360- fmt .Fprintf (out , cs .Gray ("view this run on GitHub: %s\n " ), run .URL )
328+ if shared .IsFailureState (run .Conclusion ) {
329+ fmt .Fprintf (out , "To see what failed, try: gh run view %d --log-failed\n " , run .ID )
330+ } else {
331+ fmt .Fprintln (out , "For more information about a job, try: gh run view --job=<job-id>" )
332+ }
333+ fmt .Fprintf (out , cs .Gray ("View this run on GitHub: %s\n " ), run .URL )
334+
361335 if opts .ExitStatus && shared .IsFailureState (run .Conclusion ) {
362336 return cmdutil .SilentError
363337 }
364338 } else {
365339 fmt .Fprintln (out )
366- fmt .Fprintf (out , "To see the full job log, try: gh run view --log --job=%d\n " , selectedJob .ID )
367- fmt .Fprintf (out , cs .Gray ("view this run on GitHub: %s\n " ), run .URL )
340+ if shared .IsFailureState (selectedJob .Conclusion ) {
341+ fmt .Fprintf (out , "To see the log of steps that failed, try: gh run view --log-failed --job=%d\n " , selectedJob .ID )
342+ } else {
343+ fmt .Fprintf (out , "To see the full job log, try: gh run view --log --job=%d\n " , selectedJob .ID )
344+ }
345+ fmt .Fprintf (out , cs .Gray ("View this run on GitHub: %s\n " ), run .URL )
368346
369347 if opts .ExitStatus && shared .IsFailureState (selectedJob .Conclusion ) {
370348 return cmdutil .SilentError
@@ -412,12 +390,6 @@ func getRunLog(httpClient *http.Client, repo ghrepo.Interface, runID int) (io.Re
412390 return getLog (httpClient , logURL )
413391}
414392
415- func getJobLog (httpClient * http.Client , repo ghrepo.Interface , jobID int ) (io.ReadCloser , error ) {
416- logURL := fmt .Sprintf ("%srepos/%s/actions/jobs/%d/logs" ,
417- ghinstance .RESTPrefix (repo .RepoHost ()), ghrepo .FullName (repo ), jobID )
418- return getLog (httpClient , logURL )
419- }
420-
421393func promptForJob (cs * iostreams.ColorScheme , jobs []shared.Job ) (* shared.Job , error ) {
422394 candidates := []string {"View all jobs in this run" }
423395 for _ , job := range jobs {
@@ -443,7 +415,8 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
443415 return nil , nil
444416}
445417
446- // Structure of log zip file
418+ // This function takes a zip file of logs and a list of jobs.
419+ // Structure of zip file
447420// zip/
448421// ├── jobname1/
449422// │ ├── 1_stepname.txt
@@ -453,57 +426,38 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
453426// └── jobname2/
454427// ├── 1_stepname.txt
455428// └── 2_somestepname.txt
456- func readRunLog (rlz io.ReadCloser ) (runLog , error ) {
457- rl := make (runLog )
429+ // It iterates through the list of jobs and trys to find the matching
430+ // log in the zip file. If the matching log is found it is attached
431+ // to the job.
432+ func readRunLog (rlz io.ReadCloser , jobs []shared.Job ) error {
458433 defer rlz .Close ()
459434 z , err := ioutil .ReadAll (rlz )
460435 if err != nil {
461- return rl , err
436+ return err
462437 }
463438
464439 zipReader , err := zip .NewReader (bytes .NewReader (z ), int64 (len (z )))
465440 if err != nil {
466- return rl , err
441+ return err
467442 }
468443
469- for _ , zipFile := range zipReader .File {
470- dir , file := filepath .Split (zipFile .Name )
471- ext := filepath .Ext (zipFile .Name )
472-
473- // Skip all top level files and non-text files
474- if dir != "" && ext == ".txt" {
475- split := strings .Split (file , "_" )
476- if len (split ) != 2 {
477- return rl , errors .New ("invalid step log filename" )
478- }
479-
480- jobName := strings .TrimSuffix (dir , "/" )
481- stepName := strings .TrimSuffix (split [1 ], ".txt" )
482- stepOrder , err := strconv .Atoi (split [0 ])
483- if err != nil {
484- return rl , errors .New ("invalid step log filename" )
485- }
486-
487- stepLogs , err := readZipFile (zipFile )
488- if err != nil {
489- return rl , err
490- }
491-
492- st := step {
493- order : stepOrder ,
494- name : stepName ,
495- logs : string (stepLogs ),
496- }
497-
498- if j , ok := rl [jobName ]; ! ok {
499- rl [jobName ] = & job {name : jobName , steps : []step {st }}
500- } else {
501- j .steps = append (j .steps , st )
444+ for i , job := range jobs {
445+ for j , step := range job .Steps {
446+ filename := fmt .Sprintf ("%s/%d_%s.txt" , job .Name , step .Number , step .Name )
447+ for _ , file := range zipReader .File {
448+ if file .Name == filename {
449+ log , err := readZipFile (file )
450+ if err != nil {
451+ return err
452+ }
453+ jobs [i ].Steps [j ].Log = string (log )
454+ break
455+ }
502456 }
503457 }
504458 }
505459
506- return rl , nil
460+ return nil
507461}
508462
509463func readZipFile (zf * zip.File ) ([]byte , error ) {
@@ -515,28 +469,22 @@ func readZipFile(zf *zip.File) ([]byte, error) {
515469 return ioutil .ReadAll (f )
516470}
517471
518- func displayRunLog (io * iostreams.IOStreams , rl runLog ) error {
472+ func displayRunLog (io * iostreams.IOStreams , jobs []shared. Job , failed bool ) error {
519473 err := io .StartPager ()
520474 if err != nil {
521475 return err
522476 }
523477 defer io .StopPager ()
524478
525- var jobNames []string
526- for name := range rl {
527- jobNames = append (jobNames , name )
528- }
529- sort .Strings (jobNames )
530-
531- for _ , name := range jobNames {
532- job := rl [name ]
533- steps := job .steps
534- sort .Slice (steps , func (i , j int ) bool {
535- return steps [i ].order < steps [j ].order
536- })
479+ for _ , job := range jobs {
480+ steps := job .Steps
481+ sort .Sort (steps )
537482 for _ , step := range steps {
538- prefix := fmt .Sprintf ("%s\t %s\t " , job .name , step .name )
539- scanner := bufio .NewScanner (strings .NewReader (step .logs ))
483+ if failed && ! shared .IsFailureState (step .Conclusion ) {
484+ continue
485+ }
486+ prefix := fmt .Sprintf ("%s\t %s\t " , job .Name , step .Name )
487+ scanner := bufio .NewScanner (strings .NewReader (step .Log ))
540488 for scanner .Scan () {
541489 fmt .Fprintf (io .Out , "%s%s\n " , prefix , scanner .Text ())
542490 }
0 commit comments