From 67e45f1bceddb3dc532da047994a08be0e2bd1ab Mon Sep 17 00:00:00 2001 From: Sam Coe Date: Tue, 6 Apr 2021 11:36:29 -0700 Subject: [PATCH] Display all run logs --- pkg/cmd/run/view/fixtures/run_log.zip | Bin 0 -> 1018 bytes pkg/cmd/run/view/view.go | 162 +++++++++++++++++++++----- pkg/cmd/run/view/view_test.go | 41 ++++--- 3 files changed, 155 insertions(+), 48 deletions(-) create mode 100644 pkg/cmd/run/view/fixtures/run_log.zip diff --git a/pkg/cmd/run/view/fixtures/run_log.zip b/pkg/cmd/run/view/fixtures/run_log.zip new file mode 100644 index 0000000000000000000000000000000000000000..d30f088931b8e8706225779f10fe75f0e1948d8d GIT binary patch literal 1018 zcmWIWW@h1H0D-xl?LlA$lwf6$VaUo)GSm+Z;bdUux|N#%!lf1542&#a85tN@M1Tqd zfZ9Pc2g6LDc7aDF*&v=G5DUPx8^#xxq!t+Jl~j~~O=1F?!iH(m>68B9Cr+O64mEgk zs?o4lz$aM`$#jt8u$pcJHr)trI>?PY#G4NBdJlH58$rDO;%08bJ0QmC^$zTgGtx)* z`hB1&4=_!_?{$#luz1}_AKmL9cfKUnbVepQW?bfEAJoFoFSO z6s|M?F$$Pk7?w2J!;C@+7NDu16oJQ7%(zE3buP?Ql;FVUR$Ot8?CswWQ$Z;JXcQ=9 z@i-7O{*fJM1@t{I1kq9h&{R-rz+)]", Short: "View a summary of a workflow run", @@ -234,34 +250,19 @@ func runView(opts *ViewOptions) error { return fmt.Errorf("run %d is still in progress; logs will be available when it is complete", run.ID) } - filename := fmt.Sprintf("gh-run-log-%d.zip", run.ID) - dir := os.TempDir() - fullpath := path.Join(dir, filename) - f, err := opts.CreateFile(fullpath) - if err != nil { - return fmt.Errorf("failed to open %s: %w", fullpath, err) - } - - r, err := runLog(httpClient, repo, run.ID) + runLogZip, err := runLog(httpClient, repo, run.ID) if err != nil { return fmt.Errorf("failed to get run log: %w", err) } - - if _, err := io.Copy(f, r); err != nil { - return fmt.Errorf("failed to download log: %w", err) - } - opts.IO.StopProgressIndicator() - if opts.IO.IsStdoutTTY() { - cs := opts.IO.ColorScheme() - fmt.Fprintf(opts.IO.Out, "%s Downloaded logs to %s\n", cs.SuccessIcon(), fullpath) + + logs, err := readLogsFromZip(runLogZip) + if err != nil { + return err } - if opts.ExitStatus && shared.IsFailureState(run.Conclusion) { - return cmdutil.SilentError - } - - return nil + err = displayLogs(opts.IO, logs) + return err } if selectedJob == nil && len(jobs) == 0 { @@ -423,3 +424,106 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er // User wants to see all jobs return nil, nil } + +// Structure of log zip file +// zip/ +// ├── jobname1/ +// │ ├── 1_stepname.txt +// │ ├── 2_anotherstepname.txt +// │ ├── 3_stepstepname.txt +// │ └── 4_laststepname.txt +// └── jobname2/ +// ├── 1_stepname.txt +// └── 2_somestepname.txt +func readLogsFromZip(lz io.ReadCloser) (logs, error) { + ls := make(logs) + defer lz.Close() + z, err := ioutil.ReadAll(lz) + if err != nil { + return ls, err + } + + zipReader, err := zip.NewReader(bytes.NewReader(z), int64(len(z))) + if err != nil { + return ls, err + } + + for _, zipFile := range zipReader.File { + dir, file := filepath.Split(zipFile.Name) + ext := filepath.Ext(zipFile.Name) + + // Skip all top level files and non-text files + if dir != "" && ext == ".txt" { + split := strings.Split(file, "_") + if len(split) != 2 { + return ls, errors.New("invalid step log filename") + } + + jobName := strings.TrimSuffix(dir, "/") + stepName := strings.TrimSuffix(split[1], ".txt") + stepOrder, err := strconv.Atoi(split[0]) + if err != nil { + return ls, errors.New("invalid step log filename") + } + + stepLogs, err := readZipFile(zipFile) + if err != nil { + return ls, err + } + + st := step{ + order: stepOrder, + name: stepName, + logs: string(stepLogs), + } + + if j, ok := ls[jobName]; !ok { + ls[jobName] = &job{name: jobName, steps: []step{st}} + } else { + j.steps = append(j.steps, st) + } + } + } + + return ls, nil +} + +func readZipFile(zf *zip.File) ([]byte, error) { + f, err := zf.Open() + if err != nil { + return nil, err + } + defer f.Close() + return ioutil.ReadAll(f) +} + +func displayLogs(io *iostreams.IOStreams, ls logs) error { + err := io.StartPager() + if err != nil { + return err + } + defer io.StopPager() + + var jobNames []string + for name := range ls { + jobNames = append(jobNames, name) + } + sort.Strings(jobNames) + + for _, name := range jobNames { + job := ls[name] + steps := job.steps + sort.Slice(steps, func(i, j int) bool { + return steps[i].order < steps[j].order + }) + for _, step := range steps { + prefix := fmt.Sprintf("%s\t%s\t", job.name, step.name) + scanner := bufio.NewScanner(strings.NewReader(step.logs)) + for scanner.Scan() { + fmt.Fprintf(io.Out, "%s%s\n", prefix, scanner.Text()) + } + } + } + + return nil +} diff --git a/pkg/cmd/run/view/view_test.go b/pkg/cmd/run/view/view_test.go index 1cbd7fda6..d665aa033 100644 --- a/pkg/cmd/run/view/view_test.go +++ b/pkg/cmd/run/view/view_test.go @@ -2,14 +2,12 @@ package view import ( "bytes" - "io" "io/ioutil" "net/http" - "os" - "path" "testing" "time" + "github.com/MakeNowJust/heredoc" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/cmd/run/shared" "github.com/cli/cli/pkg/cmdutil" @@ -151,7 +149,6 @@ func TestNewCmdView(t *testing.T) { } func TestViewRun(t *testing.T) { - tests := []struct { name string httpStubs func(*httpmock.Registry) @@ -161,7 +158,6 @@ func TestViewRun(t *testing.T) { wantErr bool wantOut string browsedURL string - wantWrite string errMsg string }{ { @@ -399,14 +395,13 @@ func TestViewRun(t *testing.T) { })) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), - httpmock.StringResponse("pretend these bytes constitute a zip file")) + httpmock.FileResponse("./fixtures/run_log.zip")) }, askStubs: func(as *prompt.AskStubber) { as.StubOne(2) as.StubOne(0) }, - wantOut: "✓ Downloaded logs to " + path.Join(os.TempDir(), "gh-run-log-3.zip") + "\n", - wantWrite: "pretend these bytes constitute a zip file", + wantOut: runLogOutput(), }, { name: "noninteractive with run log", @@ -421,10 +416,9 @@ func TestViewRun(t *testing.T) { httpmock.JSONResponse(shared.SuccessfulRun)) reg.Register( httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), - httpmock.StringResponse("pretend these bytes constitute a zip file")) + httpmock.FileResponse("./fixtures/run_log.zip")) }, - wantOut: "✓ Downloaded logs to " + path.Join(os.TempDir(), "gh-run-log-3.zip") + "\n", - wantWrite: "pretend these bytes constitute a zip file", + wantOut: runLogOutput(), }, { name: "run log but run is not done", @@ -597,11 +591,6 @@ func TestViewRun(t *testing.T) { return notnow } - fileBuff := bytes.Buffer{} - tt.opts.CreateFile = func(fullPath string) (io.Writer, error) { - return &fileBuff, nil - } - io, _, stdout, _ := iostreams.Test() io.SetStdoutTTY(tt.tty) tt.opts.IO = io @@ -636,10 +625,24 @@ func TestViewRun(t *testing.T) { if tt.browsedURL != "" { assert.Equal(t, tt.browsedURL, browser.BrowsedURL()) } - if tt.wantWrite != "" { - assert.Equal(t, tt.wantWrite, fileBuff.String()) - } reg.Verify(t) }) } } + +func runLogOutput() string { + return heredoc.Doc(` +job1 step1 log line 1 +job1 step1 log line 2 +job1 step1 log line 3 +job1 step2 log line 1 +job1 step2 log line 2 +job1 step2 log line 3 +job2 step1 log line 1 +job2 step1 log line 2 +job2 step1 log line 3 +job2 step2 log line 1 +job2 step2 log line 2 +job2 step2 log line 3 +`) +}