Skip to content

Commit b5fc794

Browse files
committed
support --log for runs
1 parent b705b3d commit b5fc794

File tree

2 files changed

+164
-7
lines changed

2 files changed

+164
-7
lines changed

pkg/cmd/run/view/view.go

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"io"
77
"net/http"
8+
"os"
9+
"path"
810
"time"
911

1012
"github.com/AlecAivazis/survey/v2"
@@ -39,7 +41,8 @@ type ViewOptions struct {
3941

4042
Prompt bool
4143

42-
Now func() time.Time
44+
Now func() time.Time
45+
CreateFile func(string) (io.Writer, error)
4346
}
4447

4548
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
@@ -48,6 +51,9 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
4851
HttpClient: f.HttpClient,
4952
Now: time.Now,
5053
Browser: f.Browser,
54+
CreateFile: func(fullPath string) (io.Writer, error) {
55+
return os.Create(fullPath)
56+
},
5157
}
5258
cmd := &cobra.Command{
5359
Use: "view [<run-id>]",
@@ -196,6 +202,10 @@ func runView(opts *ViewOptions) error {
196202
opts.IO.StartProgressIndicator()
197203

198204
if opts.Log && selectedJob != nil {
205+
if selectedJob.Status != shared.Completed {
206+
return fmt.Errorf("job %d is still in progress; logs will be available when it is complete", selectedJob.ID)
207+
}
208+
199209
r, err := jobLog(httpClient, repo, selectedJob.ID)
200210
if err != nil {
201211
return err
@@ -219,7 +229,40 @@ func runView(opts *ViewOptions) error {
219229
return nil
220230
}
221231

222-
// TODO support --log without selectedJob
232+
if opts.Log {
233+
if run.Status != shared.Completed {
234+
return fmt.Errorf("run %d is still in progress; logs will be available when it is complete", run.ID)
235+
}
236+
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)
246+
if err != nil {
247+
return fmt.Errorf("failed to get run log: %w", err)
248+
}
249+
250+
if _, err := io.Copy(f, r); err != nil {
251+
return fmt.Errorf("failed to download log: %w", err)
252+
}
253+
254+
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+
}
259+
260+
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
261+
return cmdutil.SilentError
262+
}
263+
264+
return nil
265+
}
223266

224267
if selectedJob == nil && len(jobs) == 0 {
225268
jobs, err = shared.GetJobs(client, repo, *run)
@@ -324,10 +367,8 @@ func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Jo
324367
return &result, nil
325368
}
326369

327-
func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadCloser, error) {
328-
url := fmt.Sprintf("%srepos/%s/actions/jobs/%d/logs",
329-
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), jobID)
330-
req, err := http.NewRequest("GET", url, nil)
370+
func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) {
371+
req, err := http.NewRequest("GET", logURL, nil)
331372
if err != nil {
332373
return nil, err
333374
}
@@ -338,14 +379,26 @@ func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadC
338379
}
339380

340381
if resp.StatusCode == 404 {
341-
return nil, errors.New("job not found")
382+
return nil, errors.New("log not found")
342383
} else if resp.StatusCode != 200 {
343384
return nil, api.HandleHTTPError(resp)
344385
}
345386

346387
return resp.Body, nil
347388
}
348389

390+
func runLog(httpClient *http.Client, repo ghrepo.Interface, runID int) (io.ReadCloser, error) {
391+
logURL := fmt.Sprintf("%srepos/%s/actions/runs/%d/logs",
392+
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), runID)
393+
return getLog(httpClient, logURL)
394+
}
395+
396+
func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadCloser, error) {
397+
logURL := fmt.Sprintf("%srepos/%s/actions/jobs/%d/logs",
398+
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), jobID)
399+
return getLog(httpClient, logURL)
400+
}
401+
349402
func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, error) {
350403
candidates := []string{"View all jobs in this run"}
351404
for _, job := range jobs {

pkg/cmd/run/view/view_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package view
22

33
import (
44
"bytes"
5+
"io"
56
"io/ioutil"
67
"net/http"
78
"testing"
@@ -158,6 +159,8 @@ func TestViewRun(t *testing.T) {
158159
wantErr bool
159160
wantOut string
160161
browsedURL string
162+
wantWrite string
163+
errMsg string
161164
}{
162165
{
163166
name: "associate with PR",
@@ -368,6 +371,96 @@ func TestViewRun(t *testing.T) {
368371
},
369372
wantOut: "it's a log\nfor this job\nbeautiful log\n",
370373
},
374+
{
375+
name: "interactive with run log",
376+
tty: true,
377+
opts: &ViewOptions{
378+
Prompt: true,
379+
Log: true,
380+
},
381+
httpStubs: func(reg *httpmock.Registry) {
382+
reg.Register(
383+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"),
384+
httpmock.JSONResponse(shared.RunsPayload{
385+
WorkflowRuns: shared.TestRuns,
386+
}))
387+
reg.Register(
388+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
389+
httpmock.JSONResponse(shared.SuccessfulRun))
390+
reg.Register(
391+
httpmock.REST("GET", "runs/3/jobs"),
392+
httpmock.JSONResponse(shared.JobsPayload{
393+
Jobs: []shared.Job{
394+
shared.SuccessfulJob,
395+
shared.FailedJob,
396+
},
397+
}))
398+
reg.Register(
399+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
400+
httpmock.StringResponse("pretend these bytes constitute a zip file"))
401+
},
402+
askStubs: func(as *prompt.AskStubber) {
403+
as.StubOne(2)
404+
as.StubOne(0)
405+
},
406+
wantOut: "✓ Downloaded logs to /tmp/gh-run-log-3.zip\n",
407+
wantWrite: "pretend these bytes constitute a zip file",
408+
},
409+
{
410+
name: "noninteractive with run log",
411+
tty: true,
412+
opts: &ViewOptions{
413+
RunID: "3",
414+
Log: true,
415+
},
416+
httpStubs: func(reg *httpmock.Registry) {
417+
reg.Register(
418+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
419+
httpmock.JSONResponse(shared.SuccessfulRun))
420+
reg.Register(
421+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
422+
httpmock.StringResponse("pretend these bytes constitute a zip file"))
423+
},
424+
wantOut: "✓ Downloaded logs to /tmp/gh-run-log-3.zip\n",
425+
wantWrite: "pretend these bytes constitute a zip file",
426+
},
427+
{
428+
name: "run log but run is not done",
429+
tty: true,
430+
opts: &ViewOptions{
431+
RunID: "2",
432+
Log: true,
433+
},
434+
httpStubs: func(reg *httpmock.Registry) {
435+
reg.Register(
436+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"),
437+
httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, "")))
438+
},
439+
wantErr: true,
440+
errMsg: "run 2 is still in progress; logs will be available when it is complete",
441+
},
442+
{
443+
name: "job log but job is not done",
444+
tty: true,
445+
opts: &ViewOptions{
446+
JobID: "20",
447+
Log: true,
448+
},
449+
httpStubs: func(reg *httpmock.Registry) {
450+
reg.Register(
451+
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"),
452+
httpmock.JSONResponse(shared.Job{
453+
ID: 20,
454+
Status: shared.InProgress,
455+
RunID: 2,
456+
}))
457+
reg.Register(
458+
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"),
459+
httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, "")))
460+
},
461+
wantErr: true,
462+
errMsg: "job 20 is still in progress; logs will be available when it is complete",
463+
},
371464
{
372465
name: "noninteractive with job",
373466
opts: &ViewOptions{
@@ -502,6 +595,11 @@ func TestViewRun(t *testing.T) {
502595
return notnow
503596
}
504597

598+
fileBuff := bytes.Buffer{}
599+
tt.opts.CreateFile = func(fullPath string) (io.Writer, error) {
600+
return &fileBuff, nil
601+
}
602+
505603
io, _, stdout, _ := iostreams.Test()
506604
io.SetStdoutTTY(tt.tty)
507605
tt.opts.IO = io
@@ -522,6 +620,9 @@ func TestViewRun(t *testing.T) {
522620
err := runView(tt.opts)
523621
if tt.wantErr {
524622
assert.Error(t, err)
623+
if tt.errMsg != "" {
624+
assert.Equal(t, tt.errMsg, err.Error())
625+
}
525626
if !tt.opts.ExitStatus {
526627
return
527628
}
@@ -533,6 +634,9 @@ func TestViewRun(t *testing.T) {
533634
if tt.browsedURL != "" {
534635
assert.Equal(t, tt.browsedURL, browser.BrowsedURL())
535636
}
637+
if tt.wantWrite != "" {
638+
assert.Equal(t, tt.wantWrite, fileBuff.String())
639+
}
536640
reg.Verify(t)
537641
})
538642
}

0 commit comments

Comments
 (0)