Merge pull request #3379 from cli/failed-logs
Add flag log-failed to display only logs of failed steps
This commit is contained in:
commit
76b0ff7a6d
4 changed files with 327 additions and 190 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
|
@ -81,7 +82,7 @@ type Job struct {
|
|||
Status Status
|
||||
Conclusion Conclusion
|
||||
Name string
|
||||
Steps []Step
|
||||
Steps Steps
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
URL string `json:"html_url"`
|
||||
|
|
@ -93,8 +94,15 @@ type Step struct {
|
|||
Status Status
|
||||
Conclusion Conclusion
|
||||
Number int
|
||||
Log *zip.File
|
||||
}
|
||||
|
||||
type Steps []Step
|
||||
|
||||
func (s Steps) Len() int { return len(s) }
|
||||
func (s Steps) Less(i, j int) bool { return s[i].Number < s[j].Number }
|
||||
func (s Steps) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
type Annotation struct {
|
||||
JobName string
|
||||
Message string
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -3,16 +3,14 @@ package view
|
|||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
|
|
@ -32,30 +30,46 @@ type browser interface {
|
|||
Browse(string) error
|
||||
}
|
||||
|
||||
type runLog map[string]*job
|
||||
|
||||
type job struct {
|
||||
name string
|
||||
steps []step
|
||||
type runLogCache interface {
|
||||
Exists(string) bool
|
||||
Create(string, io.ReadCloser) error
|
||||
Open(string) (*zip.ReadCloser, error)
|
||||
}
|
||||
|
||||
type step struct {
|
||||
order int
|
||||
name string
|
||||
logs string
|
||||
type rlc struct{}
|
||||
|
||||
func (rlc) Exists(path string) bool {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (rlc) Create(path string, content io.ReadCloser) error {
|
||||
out, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, content)
|
||||
return err
|
||||
}
|
||||
func (rlc) Open(path string) (*zip.ReadCloser, error) {
|
||||
return zip.OpenReader(path)
|
||||
}
|
||||
|
||||
type ViewOptions struct {
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser
|
||||
HttpClient func() (*http.Client, error)
|
||||
IO *iostreams.IOStreams
|
||||
BaseRepo func() (ghrepo.Interface, error)
|
||||
Browser browser
|
||||
RunLogCache runLogCache
|
||||
|
||||
RunID string
|
||||
JobID string
|
||||
Verbose bool
|
||||
ExitStatus bool
|
||||
Log bool
|
||||
LogFailed bool
|
||||
Web bool
|
||||
|
||||
Prompt bool
|
||||
|
|
@ -65,10 +79,11 @@ type ViewOptions struct {
|
|||
|
||||
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
|
||||
opts := &ViewOptions{
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Now: time.Now,
|
||||
Browser: f.Browser,
|
||||
IO: f.IOStreams,
|
||||
HttpClient: f.HttpClient,
|
||||
Now: time.Now,
|
||||
Browser: f.Browser,
|
||||
RunLogCache: rlc{},
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
|
|
@ -118,6 +133,10 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
return &cmdutil.FlagError{Err: errors.New("specify only one of --web or --log")}
|
||||
}
|
||||
|
||||
if opts.Log && opts.LogFailed {
|
||||
return &cmdutil.FlagError{Err: errors.New("specify only one of --log or --log-failed")}
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
|
|
@ -129,6 +148,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
|
|||
cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if run failed")
|
||||
cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "View a specific job ID from a run")
|
||||
cmd.Flags().BoolVar(&opts.Log, "log", false, "View full log for either a run or specific job")
|
||||
cmd.Flags().BoolVar(&opts.LogFailed, "log-failed", false, "View the log for any failed steps in a run or specific job")
|
||||
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open run in the browser")
|
||||
|
||||
return cmd
|
||||
|
|
@ -215,56 +235,8 @@ func runView(opts *ViewOptions) error {
|
|||
return opts.Browser.Browse(url)
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
|
||||
if opts.Log && selectedJob != nil {
|
||||
if selectedJob.Status != shared.Completed {
|
||||
return fmt.Errorf("job %d is still in progress; logs will be available when it is complete", selectedJob.ID)
|
||||
}
|
||||
|
||||
r, err := getJobLog(httpClient, repo, selectedJob.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.IO.StopProgressIndicator()
|
||||
|
||||
err = opts.IO.StartPager()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer opts.IO.StopPager()
|
||||
|
||||
if _, err := io.Copy(opts.IO.Out, r); err != nil {
|
||||
return fmt.Errorf("failed to read log: %w", err)
|
||||
}
|
||||
|
||||
if opts.ExitStatus && shared.IsFailureState(selectedJob.Conclusion) {
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if opts.Log {
|
||||
if run.Status != shared.Completed {
|
||||
return fmt.Errorf("run %d is still in progress; logs will be available when it is complete", run.ID)
|
||||
}
|
||||
|
||||
runLogZip, err := getRunLog(httpClient, repo, run.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get run log: %w", err)
|
||||
}
|
||||
opts.IO.StopProgressIndicator()
|
||||
|
||||
runLog, err := readRunLog(runLogZip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return displayRunLog(opts.IO, runLog)
|
||||
}
|
||||
|
||||
if selectedJob == nil && len(jobs) == 0 {
|
||||
opts.IO.StartProgressIndicator()
|
||||
jobs, err = shared.GetJobs(client, repo, *run)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
|
|
@ -274,6 +246,28 @@ func runView(opts *ViewOptions) error {
|
|||
jobs = []shared.Job{*selectedJob}
|
||||
}
|
||||
|
||||
if opts.Log || opts.LogFailed {
|
||||
if selectedJob != nil && selectedJob.Status != shared.Completed {
|
||||
return fmt.Errorf("job %d is still in progress; logs will be available when it is complete", selectedJob.ID)
|
||||
}
|
||||
|
||||
if run.Status != shared.Completed {
|
||||
return fmt.Errorf("run %d is still in progress; logs will be available when it is complete", run.ID)
|
||||
}
|
||||
|
||||
opts.IO.StartProgressIndicator()
|
||||
runLogZip, err := getRunLog(opts.RunLogCache, httpClient, repo, run.ID)
|
||||
opts.IO.StopProgressIndicator()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get run log: %w", err)
|
||||
}
|
||||
defer runLogZip.Close()
|
||||
|
||||
attachRunLog(runLogZip, jobs)
|
||||
|
||||
return displayRunLog(opts.IO, jobs, opts.LogFailed)
|
||||
}
|
||||
|
||||
prNumber := ""
|
||||
number, err := shared.PullRequestForRun(client, repo, *run)
|
||||
if err == nil {
|
||||
|
|
@ -355,16 +349,24 @@ func runView(opts *ViewOptions) error {
|
|||
}
|
||||
|
||||
fmt.Fprintln(out)
|
||||
fmt.Fprintln(out, "For more information about a job, try: gh run view --job=<job-id>")
|
||||
// TODO note about run view --log when that exists
|
||||
fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL)
|
||||
if shared.IsFailureState(run.Conclusion) {
|
||||
fmt.Fprintf(out, "To see what failed, try: gh run view %d --log-failed\n", run.ID)
|
||||
} else {
|
||||
fmt.Fprintln(out, "For more information about a job, try: gh run view --job=<job-id>")
|
||||
}
|
||||
fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL)
|
||||
|
||||
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(out)
|
||||
fmt.Fprintf(out, "To see the full job log, try: gh run view --log --job=%d\n", selectedJob.ID)
|
||||
fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL)
|
||||
if shared.IsFailureState(selectedJob.Conclusion) {
|
||||
fmt.Fprintf(out, "To see the logs for the failed steps, try: gh run view --log-failed --job=%d\n", selectedJob.ID)
|
||||
} else {
|
||||
fmt.Fprintf(out, "To see the full job log, try: gh run view --log --job=%d\n", selectedJob.ID)
|
||||
}
|
||||
fmt.Fprintf(out, cs.Gray("View this run on GitHub: %s\n"), run.URL)
|
||||
|
||||
if opts.ExitStatus && shared.IsFailureState(selectedJob.Conclusion) {
|
||||
return cmdutil.SilentError
|
||||
|
|
@ -406,16 +408,27 @@ func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) {
|
|||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func getRunLog(httpClient *http.Client, repo ghrepo.Interface, runID int) (io.ReadCloser, error) {
|
||||
logURL := fmt.Sprintf("%srepos/%s/actions/runs/%d/logs",
|
||||
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), runID)
|
||||
return getLog(httpClient, logURL)
|
||||
}
|
||||
func getRunLog(cache runLogCache, httpClient *http.Client, repo ghrepo.Interface, runID int) (*zip.ReadCloser, error) {
|
||||
filename := fmt.Sprintf("run-log-%d.zip", runID)
|
||||
filepath := filepath.Join(os.TempDir(), "gh-cli-cache", filename)
|
||||
if !cache.Exists(filepath) {
|
||||
// Run log does not exist in cache so retrieve and store it
|
||||
logURL := fmt.Sprintf("%srepos/%s/actions/runs/%d/logs",
|
||||
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), runID)
|
||||
|
||||
func getJobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadCloser, error) {
|
||||
logURL := fmt.Sprintf("%srepos/%s/actions/jobs/%d/logs",
|
||||
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), jobID)
|
||||
return getLog(httpClient, logURL)
|
||||
resp, err := getLog(httpClient, logURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
err = cache.Create(filepath, resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return cache.Open(filepath)
|
||||
}
|
||||
|
||||
func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, error) {
|
||||
|
|
@ -443,7 +456,8 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// Structure of log zip file
|
||||
// This function takes a zip file of logs and a list of jobs.
|
||||
// Structure of zip file
|
||||
// zip/
|
||||
// ├── jobname1/
|
||||
// │ ├── 1_stepname.txt
|
||||
|
|
@ -453,93 +467,47 @@ func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, er
|
|||
// └── jobname2/
|
||||
// ├── 1_stepname.txt
|
||||
// └── 2_somestepname.txt
|
||||
func readRunLog(rlz io.ReadCloser) (runLog, error) {
|
||||
rl := make(runLog)
|
||||
defer rlz.Close()
|
||||
z, err := ioutil.ReadAll(rlz)
|
||||
if err != nil {
|
||||
return rl, err
|
||||
}
|
||||
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(z), int64(len(z)))
|
||||
if err != nil {
|
||||
return rl, 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 rl, 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 rl, errors.New("invalid step log filename")
|
||||
}
|
||||
|
||||
stepLogs, err := readZipFile(zipFile)
|
||||
if err != nil {
|
||||
return rl, err
|
||||
}
|
||||
|
||||
st := step{
|
||||
order: stepOrder,
|
||||
name: stepName,
|
||||
logs: string(stepLogs),
|
||||
}
|
||||
|
||||
if j, ok := rl[jobName]; !ok {
|
||||
rl[jobName] = &job{name: jobName, steps: []step{st}}
|
||||
} else {
|
||||
j.steps = append(j.steps, st)
|
||||
// It iterates through the list of jobs and trys to find the matching
|
||||
// log in the zip file. If the matching log is found it is attached
|
||||
// to the job.
|
||||
func attachRunLog(rlz *zip.ReadCloser, jobs []shared.Job) {
|
||||
for i, job := range jobs {
|
||||
for j, step := range job.Steps {
|
||||
filename := fmt.Sprintf("%s/%d_%s.txt", job.Name, step.Number, step.Name)
|
||||
for _, file := range rlz.File {
|
||||
if file.Name == filename {
|
||||
jobs[i].Steps[j].Log = file
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rl, 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 displayRunLog(io *iostreams.IOStreams, rl runLog) error {
|
||||
func displayRunLog(io *iostreams.IOStreams, jobs []shared.Job, failed bool) error {
|
||||
err := io.StartPager()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer io.StopPager()
|
||||
|
||||
var jobNames []string
|
||||
for name := range rl {
|
||||
jobNames = append(jobNames, name)
|
||||
}
|
||||
sort.Strings(jobNames)
|
||||
|
||||
for _, name := range jobNames {
|
||||
job := rl[name]
|
||||
steps := job.steps
|
||||
sort.Slice(steps, func(i, j int) bool {
|
||||
return steps[i].order < steps[j].order
|
||||
})
|
||||
for _, job := range jobs {
|
||||
steps := job.Steps
|
||||
sort.Sort(steps)
|
||||
for _, step := range steps {
|
||||
prefix := fmt.Sprintf("%s\t%s\t", job.name, step.name)
|
||||
scanner := bufio.NewScanner(strings.NewReader(step.logs))
|
||||
if failed && !shared.IsFailureState(step.Conclusion) {
|
||||
continue
|
||||
}
|
||||
prefix := fmt.Sprintf("%s\t%s\t", job.Name, step.Name)
|
||||
f, err := step.Log.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
fmt.Fprintf(io.Out, "%s%s\n", prefix, scanner.Text())
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package view
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
|
@ -60,6 +63,12 @@ func TestNewCmdView(t *testing.T) {
|
|||
cli: "-w --log",
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "disallow log and log-failed",
|
||||
tty: true,
|
||||
cli: "--log --log-failed",
|
||||
wantsErr: true,
|
||||
},
|
||||
{
|
||||
name: "exit status",
|
||||
cli: "--exit-status 1234",
|
||||
|
|
@ -198,7 +207,7 @@ func TestViewRun(t *testing.T) {
|
|||
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
|
||||
httpmock.JSONResponse([]shared.Annotation{}))
|
||||
},
|
||||
wantOut: "\n✓ trunk successful #2898 · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about a job, try: gh run view --job=<job-id>\nview this run on GitHub: runs/3\n",
|
||||
wantOut: "\n✓ trunk successful #2898 · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about a job, try: gh run view --job=<job-id>\nView this run on GitHub: runs/3\n",
|
||||
},
|
||||
{
|
||||
name: "exit status, failed run",
|
||||
|
|
@ -227,7 +236,7 @@ func TestViewRun(t *testing.T) {
|
|||
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"),
|
||||
httpmock.JSONResponse(shared.FailedJobAnnotations))
|
||||
},
|
||||
wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=<job-id>\nview this run on GitHub: runs/1234\n",
|
||||
wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: runs/1234\n",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -256,20 +265,20 @@ func TestViewRun(t *testing.T) {
|
|||
httpmock.JSONResponse(shared.JobsPayload{}))
|
||||
},
|
||||
wantOut: heredoc.Doc(`
|
||||
|
||||
|
||||
✓ trunk successful · 3
|
||||
Triggered via push about 59 minutes ago
|
||||
|
||||
|
||||
JOBS
|
||||
|
||||
|
||||
|
||||
|
||||
ARTIFACTS
|
||||
artifact-1
|
||||
artifact-2 (expired)
|
||||
artifact-3
|
||||
|
||||
|
||||
For more information about a job, try: gh run view --job=<job-id>
|
||||
view this run on GitHub: runs/3
|
||||
View this run on GitHub: runs/3
|
||||
`),
|
||||
},
|
||||
{
|
||||
|
|
@ -299,7 +308,7 @@ func TestViewRun(t *testing.T) {
|
|||
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
|
||||
httpmock.JSONResponse([]shared.Annotation{}))
|
||||
},
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about a job, try: gh run view --job=<job-id>\nview this run on GitHub: runs/3\n",
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about a job, try: gh run view --job=<job-id>\nView this run on GitHub: runs/3\n",
|
||||
},
|
||||
{
|
||||
name: "verbose",
|
||||
|
|
@ -334,7 +343,7 @@ func TestViewRun(t *testing.T) {
|
|||
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"),
|
||||
httpmock.JSONResponse(shared.FailedJobAnnotations))
|
||||
},
|
||||
wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=<job-id>\nview this run on GitHub: runs/1234\n",
|
||||
wantOut: "\nX trunk failed · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: runs/1234\n",
|
||||
},
|
||||
{
|
||||
name: "prompts for choice, one job",
|
||||
|
|
@ -371,7 +380,7 @@ func TestViewRun(t *testing.T) {
|
|||
opts: &ViewOptions{
|
||||
Prompt: true,
|
||||
},
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about a job, try: gh run view --job=<job-id>\nview this run on GitHub: runs/3\n",
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about a job, try: gh run view --job=<job-id>\nView this run on GitHub: runs/3\n",
|
||||
},
|
||||
{
|
||||
name: "interactive with log",
|
||||
|
|
@ -398,14 +407,14 @@ func TestViewRun(t *testing.T) {
|
|||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10/logs"),
|
||||
httpmock.StringResponse("it's a log\nfor this job\nbeautiful log\n"))
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
|
||||
httpmock.FileResponse("./fixtures/run_log.zip"))
|
||||
},
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubOne(2)
|
||||
as.StubOne(1)
|
||||
},
|
||||
wantOut: "it's a log\nfor this job\nbeautiful log\n",
|
||||
wantOut: coolJobRunLogOutput,
|
||||
},
|
||||
{
|
||||
name: "noninteractive with log",
|
||||
|
|
@ -421,10 +430,10 @@ func TestViewRun(t *testing.T) {
|
|||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
|
||||
httpmock.JSONResponse(shared.SuccessfulRun))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10/logs"),
|
||||
httpmock.StringResponse("it's a log\nfor this job\nbeautiful log\n"))
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
|
||||
httpmock.FileResponse("./fixtures/run_log.zip"))
|
||||
},
|
||||
wantOut: "it's a log\nfor this job\nbeautiful log\n",
|
||||
wantOut: coolJobRunLogOutput,
|
||||
},
|
||||
{
|
||||
name: "interactive with run log",
|
||||
|
|
@ -471,12 +480,132 @@ func TestViewRun(t *testing.T) {
|
|||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
|
||||
httpmock.JSONResponse(shared.SuccessfulRun))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "runs/3/jobs"),
|
||||
httpmock.JSONResponse(shared.JobsPayload{
|
||||
Jobs: []shared.Job{
|
||||
shared.SuccessfulJob,
|
||||
shared.FailedJob,
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
|
||||
httpmock.FileResponse("./fixtures/run_log.zip"))
|
||||
},
|
||||
wantOut: expectedRunLogOutput,
|
||||
},
|
||||
{
|
||||
name: "interactive with log-failed",
|
||||
tty: true,
|
||||
opts: &ViewOptions{
|
||||
Prompt: true,
|
||||
LogFailed: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
|
||||
httpmock.JSONResponse(shared.RunsPayload{
|
||||
WorkflowRuns: shared.TestRuns,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
|
||||
httpmock.JSONResponse(shared.FailedRun))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "runs/1234/jobs"),
|
||||
httpmock.JSONResponse(shared.JobsPayload{
|
||||
Jobs: []shared.Job{
|
||||
shared.SuccessfulJob,
|
||||
shared.FailedJob,
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
|
||||
httpmock.FileResponse("./fixtures/run_log.zip"))
|
||||
},
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubOne(4)
|
||||
as.StubOne(2)
|
||||
},
|
||||
wantOut: quuxTheBarfLogOutput,
|
||||
},
|
||||
{
|
||||
name: "noninteractive with log-failed",
|
||||
opts: &ViewOptions{
|
||||
JobID: "20",
|
||||
LogFailed: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"),
|
||||
httpmock.JSONResponse(shared.FailedJob))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
|
||||
httpmock.JSONResponse(shared.FailedRun))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
|
||||
httpmock.FileResponse("./fixtures/run_log.zip"))
|
||||
},
|
||||
wantOut: quuxTheBarfLogOutput,
|
||||
},
|
||||
{
|
||||
name: "interactive with run log-failed",
|
||||
tty: true,
|
||||
opts: &ViewOptions{
|
||||
Prompt: true,
|
||||
LogFailed: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
|
||||
httpmock.JSONResponse(shared.RunsPayload{
|
||||
WorkflowRuns: shared.TestRuns,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
|
||||
httpmock.JSONResponse(shared.FailedRun))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "runs/1234/jobs"),
|
||||
httpmock.JSONResponse(shared.JobsPayload{
|
||||
Jobs: []shared.Job{
|
||||
shared.SuccessfulJob,
|
||||
shared.FailedJob,
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
|
||||
httpmock.FileResponse("./fixtures/run_log.zip"))
|
||||
},
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubOne(4)
|
||||
as.StubOne(0)
|
||||
},
|
||||
wantOut: quuxTheBarfLogOutput,
|
||||
},
|
||||
{
|
||||
name: "noninteractive with run log-failed",
|
||||
tty: true,
|
||||
opts: &ViewOptions{
|
||||
RunID: "1234",
|
||||
LogFailed: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"),
|
||||
httpmock.JSONResponse(shared.FailedRun))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "runs/1234/jobs"),
|
||||
httpmock.JSONResponse(shared.JobsPayload{
|
||||
Jobs: []shared.Job{
|
||||
shared.SuccessfulJob,
|
||||
shared.FailedJob,
|
||||
},
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"),
|
||||
httpmock.FileResponse("./fixtures/run_log.zip"))
|
||||
},
|
||||
wantOut: quuxTheBarfLogOutput,
|
||||
},
|
||||
{
|
||||
name: "run log but run is not done",
|
||||
tty: true,
|
||||
|
|
@ -488,6 +617,11 @@ func TestViewRun(t *testing.T) {
|
|||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"),
|
||||
httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, "")))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "runs/2/jobs"),
|
||||
httpmock.JSONResponse(shared.JobsPayload{
|
||||
Jobs: []shared.Job{},
|
||||
}))
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "run 2 is still in progress; logs will be available when it is complete",
|
||||
|
|
@ -530,7 +664,7 @@ func TestViewRun(t *testing.T) {
|
|||
httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"),
|
||||
httpmock.JSONResponse([]shared.Annotation{}))
|
||||
},
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nview this run on GitHub: runs/3\n",
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: runs/3\n",
|
||||
},
|
||||
{
|
||||
name: "interactive, multiple jobs, choose all jobs",
|
||||
|
|
@ -569,7 +703,7 @@ func TestViewRun(t *testing.T) {
|
|||
as.StubOne(2)
|
||||
as.StubOne(0)
|
||||
},
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=<job-id>\nview this run on GitHub: runs/3\n",
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=<job-id>\nView this run on GitHub: runs/3\n",
|
||||
},
|
||||
{
|
||||
name: "interactive, multiple jobs, choose specific jobs",
|
||||
|
|
@ -602,7 +736,7 @@ func TestViewRun(t *testing.T) {
|
|||
as.StubOne(2)
|
||||
as.StubOne(1)
|
||||
},
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nview this run on GitHub: runs/3\n",
|
||||
wantOut: "\n✓ trunk successful · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: runs/3\n",
|
||||
},
|
||||
{
|
||||
name: "web run",
|
||||
|
|
@ -666,6 +800,8 @@ func TestViewRun(t *testing.T) {
|
|||
|
||||
browser := &cmdutil.TestBrowser{}
|
||||
tt.opts.Browser = browser
|
||||
rlc := testRunLogCache{}
|
||||
tt.opts.RunLogCache = rlc
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := runView(tt.opts)
|
||||
|
|
@ -690,17 +826,42 @@ func TestViewRun(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
var expectedRunLogOutput = 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
|
||||
type testRunLogCache struct{}
|
||||
|
||||
func (testRunLogCache) Exists(path string) bool {
|
||||
return false
|
||||
}
|
||||
func (testRunLogCache) Create(path string, content io.ReadCloser) error {
|
||||
return nil
|
||||
}
|
||||
func (testRunLogCache) Open(path string) (*zip.ReadCloser, error) {
|
||||
return zip.OpenReader("./fixtures/run_log.zip")
|
||||
}
|
||||
|
||||
var barfTheFobLogOutput = heredoc.Doc(`
|
||||
cool job barz the fob log line 1
|
||||
cool job barz the fob log line 2
|
||||
cool job barz the fob log line 3
|
||||
`)
|
||||
|
||||
var fobTheBarzLogOutput = heredoc.Doc(`
|
||||
cool job fob the barz log line 1
|
||||
cool job fob the barz log line 2
|
||||
cool job fob the barz log line 3
|
||||
`)
|
||||
|
||||
var barfTheQuuxLogOutput = heredoc.Doc(`
|
||||
sad job barf the quux log line 1
|
||||
sad job barf the quux log line 2
|
||||
sad job barf the quux log line 3
|
||||
`)
|
||||
|
||||
var quuxTheBarfLogOutput = heredoc.Doc(`
|
||||
sad job quux the barf log line 1
|
||||
sad job quux the barf log line 2
|
||||
sad job quux the barf log line 3
|
||||
`)
|
||||
|
||||
var coolJobRunLogOutput = fmt.Sprintf("%s%s", fobTheBarzLogOutput, barfTheFobLogOutput)
|
||||
var sadJobRunLogOutput = fmt.Sprintf("%s%s", barfTheQuuxLogOutput, quuxTheBarfLogOutput)
|
||||
var expectedRunLogOutput = fmt.Sprintf("%s%s", coolJobRunLogOutput, sadJobRunLogOutput)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue