Skip to content

Commit d78e215

Browse files
author
Nate Smith
authored
Merge pull request cli#3368 from cli/display-all-the-logs
Display all run logs
2 parents 35c55fd + ed0ef6a commit d78e215

File tree

3 files changed

+276
-14
lines changed

3 files changed

+276
-14
lines changed
1018 Bytes
Binary file not shown.

pkg/cmd/run/view/view.go

Lines changed: 169 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +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"
12+
"path/filepath"
13+
"sort"
814
"strconv"
15+
"strings"
916
"time"
1017

1118
"github.com/AlecAivazis/survey/v2"
@@ -25,6 +32,19 @@ type browser interface {
2532
Browse(string) error
2633
}
2734

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+
}
47+
2848
type ViewOptions struct {
2949
HttpClient func() (*http.Client, error)
3050
IO *iostreams.IOStreams
@@ -50,6 +70,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
5070
Now: time.Now,
5171
Browser: f.Browser,
5272
}
73+
5374
cmd := &cobra.Command{
5475
Use: "view [<run-id>]",
5576
Short: "View a summary of a workflow run",
@@ -71,7 +92,6 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
7192
# Exit non-zero if a run failed
7293
$ gh run view 0451 -e && echo "run pending or passed"
7394
`),
74-
// TODO should exit status respect only a selected job if --job is passed?
7595
RunE: func(cmd *cobra.Command, args []string) error {
7696
// support `-R, --repo` override
7797
opts.BaseRepo = f.BaseRepo
@@ -198,7 +218,11 @@ func runView(opts *ViewOptions) error {
198218
opts.IO.StartProgressIndicator()
199219

200220
if opts.Log && selectedJob != nil {
201-
r, err := jobLog(httpClient, repo, selectedJob.ID)
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)
202226
if err != nil {
203227
return err
204228
}
@@ -214,14 +238,31 @@ func runView(opts *ViewOptions) error {
214238
return fmt.Errorf("failed to read log: %w", err)
215239
}
216240

217-
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
241+
if opts.ExitStatus && shared.IsFailureState(selectedJob.Conclusion) {
218242
return cmdutil.SilentError
219243
}
220244

221245
return nil
222246
}
223247

224-
// TODO support --log without selectedJob
248+
if opts.Log {
249+
if run.Status != shared.Completed {
250+
return fmt.Errorf("run %d is still in progress; logs will be available when it is complete", run.ID)
251+
}
252+
253+
runLogZip, err := getRunLog(httpClient, repo, run.ID)
254+
if err != nil {
255+
return fmt.Errorf("failed to get run log: %w", err)
256+
}
257+
opts.IO.StopProgressIndicator()
258+
259+
runLog, err := readRunLog(runLogZip)
260+
if err != nil {
261+
return err
262+
}
263+
264+
return displayRunLog(opts.IO, runLog)
265+
}
225266

226267
if selectedJob == nil && len(jobs) == 0 {
227268
jobs, err = shared.GetJobs(client, repo, *run)
@@ -317,15 +358,17 @@ func runView(opts *ViewOptions) error {
317358
fmt.Fprintln(out, "For more information about a job, try: gh run view --job=<job-id>")
318359
// TODO note about run view --log when that exists
319360
fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL)
361+
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
362+
return cmdutil.SilentError
363+
}
320364
} else {
321365
fmt.Fprintln(out)
322-
// TODO this does not exist yet
323366
fmt.Fprintf(out, "To see the full job log, try: gh run view --log --job=%d\n", selectedJob.ID)
324367
fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL)
325-
}
326368

327-
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
328-
return cmdutil.SilentError
369+
if opts.ExitStatus && shared.IsFailureState(selectedJob.Conclusion) {
370+
return cmdutil.SilentError
371+
}
329372
}
330373

331374
return nil
@@ -343,10 +386,8 @@ func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Jo
343386
return &result, nil
344387
}
345388

346-
func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadCloser, error) {
347-
url := fmt.Sprintf("%srepos/%s/actions/jobs/%d/logs",
348-
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), jobID)
349-
req, err := http.NewRequest("GET", url, nil)
389+
func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) {
390+
req, err := http.NewRequest("GET", logURL, nil)
350391
if err != nil {
351392
return nil, err
352393
}
@@ -357,14 +398,26 @@ func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadC
357398
}
358399

359400
if resp.StatusCode == 404 {
360-
return nil, errors.New("job not found")
401+
return nil, errors.New("log not found")
361402
} else if resp.StatusCode != 200 {
362403
return nil, api.HandleHTTPError(resp)
363404
}
364405

365406
return resp.Body, nil
366407
}
367408

409+
func getRunLog(httpClient *http.Client, repo ghrepo.Interface, runID int) (io.ReadCloser, error) {
410+
logURL := fmt.Sprintf("%srepos/%s/actions/runs/%d/logs",
411+
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), runID)
412+
return getLog(httpClient, logURL)
413+
}
414+
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+
368421
func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, error) {
369422
candidates := []string{"View all jobs in this run"}
370423
for _, job := range jobs {
@@ -389,3 +442,106 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
389442
// User wants to see all jobs
390443
return nil, nil
391444
}
445+
446+
// Structure of log zip file
447+
// zip/
448+
// ├── jobname1/
449+
// │ ├── 1_stepname.txt
450+
// │ ├── 2_anotherstepname.txt
451+
// │ ├── 3_stepstepname.txt
452+
// │ └── 4_laststepname.txt
453+
// └── jobname2/
454+
// ├── 1_stepname.txt
455+
// └── 2_somestepname.txt
456+
func readRunLog(rlz io.ReadCloser) (runLog, error) {
457+
rl := make(runLog)
458+
defer rlz.Close()
459+
z, err := ioutil.ReadAll(rlz)
460+
if err != nil {
461+
return rl, err
462+
}
463+
464+
zipReader, err := zip.NewReader(bytes.NewReader(z), int64(len(z)))
465+
if err != nil {
466+
return rl, err
467+
}
468+
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)
502+
}
503+
}
504+
}
505+
506+
return rl, nil
507+
}
508+
509+
func readZipFile(zf *zip.File) ([]byte, error) {
510+
f, err := zf.Open()
511+
if err != nil {
512+
return nil, err
513+
}
514+
defer f.Close()
515+
return ioutil.ReadAll(f)
516+
}
517+
518+
func displayRunLog(io *iostreams.IOStreams, rl runLog) error {
519+
err := io.StartPager()
520+
if err != nil {
521+
return err
522+
}
523+
defer io.StopPager()
524+
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+
})
537+
for _, step := range steps {
538+
prefix := fmt.Sprintf("%s\t%s\t", job.name, step.name)
539+
scanner := bufio.NewScanner(strings.NewReader(step.logs))
540+
for scanner.Scan() {
541+
fmt.Fprintf(io.Out, "%s%s\n", prefix, scanner.Text())
542+
}
543+
}
544+
}
545+
546+
return nil
547+
}

0 commit comments

Comments
 (0)