Skip to content

Commit 67e45f1

Browse files
committed
Display all run logs
1 parent c8d1d6e commit 67e45f1

File tree

3 files changed

+154
-47
lines changed

3 files changed

+154
-47
lines changed
1018 Bytes
Binary file not shown.

pkg/cmd/run/view/view.go

Lines changed: 132 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
package view
22

33
import (
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+
2948
type 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

4866
func 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+
}

pkg/cmd/run/view/view_test.go

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ package view
22

33
import (
44
"bytes"
5-
"io"
65
"io/ioutil"
76
"net/http"
8-
"os"
9-
"path"
107
"testing"
118
"time"
129

10+
"github.com/MakeNowJust/heredoc"
1311
"github.com/cli/cli/internal/ghrepo"
1412
"github.com/cli/cli/pkg/cmd/run/shared"
1513
"github.com/cli/cli/pkg/cmdutil"
@@ -151,7 +149,6 @@ func TestNewCmdView(t *testing.T) {
151149
}
152150

153151
func TestViewRun(t *testing.T) {
154-
155152
tests := []struct {
156153
name string
157154
httpStubs func(*httpmock.Registry)
@@ -161,7 +158,6 @@ func TestViewRun(t *testing.T) {
161158
wantErr bool
162159
wantOut string
163160
browsedURL string
164-
wantWrite string
165161
errMsg string
166162
}{
167163
{
@@ -399,14 +395,13 @@ func TestViewRun(t *testing.T) {
399395
}))
400396
reg.Register(
401397
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
402-
httpmock.StringResponse("pretend these bytes constitute a zip file"))
398+
httpmock.FileResponse("./fixtures/run_log.zip"))
403399
},
404400
askStubs: func(as *prompt.AskStubber) {
405401
as.StubOne(2)
406402
as.StubOne(0)
407403
},
408-
wantOut: "✓ Downloaded logs to " + path.Join(os.TempDir(), "gh-run-log-3.zip") + "\n",
409-
wantWrite: "pretend these bytes constitute a zip file",
404+
wantOut: runLogOutput(),
410405
},
411406
{
412407
name: "noninteractive with run log",
@@ -421,10 +416,9 @@ func TestViewRun(t *testing.T) {
421416
httpmock.JSONResponse(shared.SuccessfulRun))
422417
reg.Register(
423418
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
424-
httpmock.StringResponse("pretend these bytes constitute a zip file"))
419+
httpmock.FileResponse("./fixtures/run_log.zip"))
425420
},
426-
wantOut: "✓ Downloaded logs to " + path.Join(os.TempDir(), "gh-run-log-3.zip") + "\n",
427-
wantWrite: "pretend these bytes constitute a zip file",
421+
wantOut: runLogOutput(),
428422
},
429423
{
430424
name: "run log but run is not done",
@@ -597,11 +591,6 @@ func TestViewRun(t *testing.T) {
597591
return notnow
598592
}
599593

600-
fileBuff := bytes.Buffer{}
601-
tt.opts.CreateFile = func(fullPath string) (io.Writer, error) {
602-
return &fileBuff, nil
603-
}
604-
605594
io, _, stdout, _ := iostreams.Test()
606595
io.SetStdoutTTY(tt.tty)
607596
tt.opts.IO = io
@@ -636,10 +625,24 @@ func TestViewRun(t *testing.T) {
636625
if tt.browsedURL != "" {
637626
assert.Equal(t, tt.browsedURL, browser.BrowsedURL())
638627
}
639-
if tt.wantWrite != "" {
640-
assert.Equal(t, tt.wantWrite, fileBuff.String())
641-
}
642628
reg.Verify(t)
643629
})
644630
}
645631
}
632+
633+
func runLogOutput() string {
634+
return heredoc.Doc(`
635+
job1 step1 log line 1
636+
job1 step1 log line 2
637+
job1 step1 log line 3
638+
job1 step2 log line 1
639+
job1 step2 log line 2
640+
job1 step2 log line 3
641+
job2 step1 log line 1
642+
job2 step1 log line 2
643+
job2 step1 log line 3
644+
job2 step2 log line 1
645+
job2 step2 log line 2
646+
job2 step2 log line 3
647+
`)
648+
}

0 commit comments

Comments
 (0)