11package view
22
33import (
4+ "archive/zip"
5+ "bufio"
6+ "bytes"
47 "errors"
58 "fmt"
69 "io"
10+ "io/ioutil"
711 "net/http"
8- "os"
9- "path"
12+ "path/filepath"
13+ "sort"
14+ "strconv"
15+ "strings"
1016 "time"
1117
1218 "github.com/AlecAivazis/survey/v2"
@@ -26,6 +32,19 @@ type browser interface {
2632 Browse (string ) error
2733}
2834
35+ type logs 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+ }
47+
2948type ViewOptions struct {
3049 HttpClient func () (* http.Client , error )
3150 IO * iostreams.IOStreams
@@ -41,8 +60,7 @@ type ViewOptions struct {
4160
4261 Prompt bool
4362
44- Now func () time.Time
45- CreateFile func (string ) (io.Writer , error )
63+ Now func () time.Time
4664}
4765
4866func NewCmdView (f * cmdutil.Factory , runF func (* ViewOptions ) error ) * cobra.Command {
@@ -51,10 +69,8 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
5169 HttpClient : f .HttpClient ,
5270 Now : time .Now ,
5371 Browser : f .Browser ,
54- CreateFile : func (fullPath string ) (io.Writer , error ) {
55- return os .Create (fullPath )
56- },
5772 }
73+
5874 cmd := & cobra.Command {
5975 Use : "view [<run-id>]" ,
6076 Short : "View a summary of a workflow run" ,
@@ -234,34 +250,19 @@ func runView(opts *ViewOptions) error {
234250 return fmt .Errorf ("run %d is still in progress; logs will be available when it is complete" , run .ID )
235251 }
236252
237- filename := fmt .Sprintf ("gh-run-log-%d.zip" , run .ID )
238- dir := os .TempDir ()
239- fullpath := path .Join (dir , filename )
240- f , err := opts .CreateFile (fullpath )
241- if err != nil {
242- return fmt .Errorf ("failed to open %s: %w" , fullpath , err )
243- }
244-
245- r , err := runLog (httpClient , repo , run .ID )
253+ runLogZip , err := runLog (httpClient , repo , run .ID )
246254 if err != nil {
247255 return fmt .Errorf ("failed to get run log: %w" , err )
248256 }
249-
250- if _ , err := io .Copy (f , r ); err != nil {
251- return fmt .Errorf ("failed to download log: %w" , err )
252- }
253-
254257 opts .IO .StopProgressIndicator ()
255- if opts .IO .IsStdoutTTY () {
256- cs := opts .IO .ColorScheme ()
257- fmt .Fprintf (opts .IO .Out , "%s Downloaded logs to %s\n " , cs .SuccessIcon (), fullpath )
258- }
259258
260- if opts .ExitStatus && shared .IsFailureState (run .Conclusion ) {
261- return cmdutil .SilentError
259+ logs , err := readLogsFromZip (runLogZip )
260+ if err != nil {
261+ return err
262262 }
263263
264- return nil
264+ err = displayLogs (opts .IO , logs )
265+ return err
265266 }
266267
267268 if selectedJob == nil && len (jobs ) == 0 {
@@ -423,3 +424,106 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
423424 // User wants to see all jobs
424425 return nil , nil
425426}
427+
428+ // Structure of log zip file
429+ // zip/
430+ // ├── jobname1/
431+ // │ ├── 1_stepname.txt
432+ // │ ├── 2_anotherstepname.txt
433+ // │ ├── 3_stepstepname.txt
434+ // │ └── 4_laststepname.txt
435+ // └── jobname2/
436+ // ├── 1_stepname.txt
437+ // └── 2_somestepname.txt
438+ func readLogsFromZip (lz io.ReadCloser ) (logs , error ) {
439+ ls := make (logs )
440+ defer lz .Close ()
441+ z , err := ioutil .ReadAll (lz )
442+ if err != nil {
443+ return ls , err
444+ }
445+
446+ zipReader , err := zip .NewReader (bytes .NewReader (z ), int64 (len (z )))
447+ if err != nil {
448+ return ls , err
449+ }
450+
451+ for _ , zipFile := range zipReader .File {
452+ dir , file := filepath .Split (zipFile .Name )
453+ ext := filepath .Ext (zipFile .Name )
454+
455+ // Skip all top level files and non-text files
456+ if dir != "" && ext == ".txt" {
457+ split := strings .Split (file , "_" )
458+ if len (split ) != 2 {
459+ return ls , errors .New ("invalid step log filename" )
460+ }
461+
462+ jobName := strings .TrimSuffix (dir , "/" )
463+ stepName := strings .TrimSuffix (split [1 ], ".txt" )
464+ stepOrder , err := strconv .Atoi (split [0 ])
465+ if err != nil {
466+ return ls , errors .New ("invalid step log filename" )
467+ }
468+
469+ stepLogs , err := readZipFile (zipFile )
470+ if err != nil {
471+ return ls , err
472+ }
473+
474+ st := step {
475+ order : stepOrder ,
476+ name : stepName ,
477+ logs : string (stepLogs ),
478+ }
479+
480+ if j , ok := ls [jobName ]; ! ok {
481+ ls [jobName ] = & job {name : jobName , steps : []step {st }}
482+ } else {
483+ j .steps = append (j .steps , st )
484+ }
485+ }
486+ }
487+
488+ return ls , nil
489+ }
490+
491+ func readZipFile (zf * zip.File ) ([]byte , error ) {
492+ f , err := zf .Open ()
493+ if err != nil {
494+ return nil , err
495+ }
496+ defer f .Close ()
497+ return ioutil .ReadAll (f )
498+ }
499+
500+ func displayLogs (io * iostreams.IOStreams , ls logs ) error {
501+ err := io .StartPager ()
502+ if err != nil {
503+ return err
504+ }
505+ defer io .StopPager ()
506+
507+ var jobNames []string
508+ for name := range ls {
509+ jobNames = append (jobNames , name )
510+ }
511+ sort .Strings (jobNames )
512+
513+ for _ , name := range jobNames {
514+ job := ls [name ]
515+ steps := job .steps
516+ sort .Slice (steps , func (i , j int ) bool {
517+ return steps [i ].order < steps [j ].order
518+ })
519+ for _ , step := range steps {
520+ prefix := fmt .Sprintf ("%s\t %s\t " , job .name , step .name )
521+ scanner := bufio .NewScanner (strings .NewReader (step .logs ))
522+ for scanner .Scan () {
523+ fmt .Fprintf (io .Out , "%s%s\n " , prefix , scanner .Text ())
524+ }
525+ }
526+ }
527+
528+ return nil
529+ }
0 commit comments