Skip to content

Commit dc63480

Browse files
committed
Add flag log-failed to display only logs of failed steps
1 parent d78e215 commit dc63480

File tree

4 files changed

+250
-150
lines changed

4 files changed

+250
-150
lines changed

pkg/cmd/run/shared/shared.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ type Job struct {
8181
Status Status
8282
Conclusion Conclusion
8383
Name string
84-
Steps []Step
84+
Steps Steps
8585
StartedAt time.Time `json:"started_at"`
8686
CompletedAt time.Time `json:"completed_at"`
8787
URL string `json:"html_url"`
@@ -93,8 +93,15 @@ type Step struct {
9393
Status Status
9494
Conclusion Conclusion
9595
Number int
96+
Log string
9697
}
9798

99+
type Steps []Step
100+
101+
func (s Steps) Len() int { return len(s) }
102+
func (s Steps) Less(i, j int) bool { return s[i].Number < s[j].Number }
103+
func (s Steps) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
104+
98105
type Annotation struct {
99106
JobName string
100107
Message string
102 Bytes
Binary file not shown.

pkg/cmd/run/view/view.go

Lines changed: 64 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
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

4836
type 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-
421393
func 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

509463
func 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

Comments
 (0)