support --log for runs
This commit is contained in:
parent
b705b3d6ba
commit
b5fc794b78
2 changed files with 164 additions and 7 deletions
|
|
@ -5,6 +5,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
|
|
@ -39,7 +41,8 @@ type ViewOptions struct {
|
|||
|
||||
Prompt bool
|
||||
|
||||
Now func() time.Time
|
||||
Now func() time.Time
|
||||
CreateFile func(string) (io.Writer, error)
|
||||
}
|
||||
|
||||
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
|
|||
HttpClient: f.HttpClient,
|
||||
Now: time.Now,
|
||||
Browser: f.Browser,
|
||||
CreateFile: func(fullPath string) (io.Writer, error) {
|
||||
return os.Create(fullPath)
|
||||
},
|
||||
}
|
||||
cmd := &cobra.Command{
|
||||
Use: "view [<run-id>]",
|
||||
|
|
@ -196,6 +202,10 @@ func runView(opts *ViewOptions) error {
|
|||
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 := jobLog(httpClient, repo, selectedJob.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -219,7 +229,40 @@ func runView(opts *ViewOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO support --log without selectedJob
|
||||
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)
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("gh-run-log-%d.zip", run.ID)
|
||||
dir := os.TempDir()
|
||||
fullpath := path.Join(dir, filename)
|
||||
f, err := opts.CreateFile(fullpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s: %w", fullpath, err)
|
||||
}
|
||||
|
||||
r, err := runLog(httpClient, repo, run.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get run log: %w", err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(f, r); err != nil {
|
||||
return fmt.Errorf("failed to download log: %w", err)
|
||||
}
|
||||
|
||||
opts.IO.StopProgressIndicator()
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.Out, "%s Downloaded logs to %s\n", cs.SuccessIcon(), fullpath)
|
||||
}
|
||||
|
||||
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if selectedJob == nil && len(jobs) == 0 {
|
||||
jobs, err = shared.GetJobs(client, repo, *run)
|
||||
|
|
@ -324,10 +367,8 @@ func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Jo
|
|||
return &result, nil
|
||||
}
|
||||
|
||||
func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadCloser, error) {
|
||||
url := fmt.Sprintf("%srepos/%s/actions/jobs/%d/logs",
|
||||
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), jobID)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
func getLog(httpClient *http.Client, logURL string) (io.ReadCloser, error) {
|
||||
req, err := http.NewRequest("GET", logURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -338,7 +379,7 @@ func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadC
|
|||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, errors.New("job not found")
|
||||
return nil, errors.New("log not found")
|
||||
} else if resp.StatusCode != 200 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
|
@ -346,6 +387,18 @@ func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadC
|
|||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func runLog(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 jobLog(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)
|
||||
}
|
||||
|
||||
func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, error) {
|
||||
candidates := []string{"View all jobs in this run"}
|
||||
for _, job := range jobs {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package view
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
|
@ -158,6 +159,8 @@ func TestViewRun(t *testing.T) {
|
|||
wantErr bool
|
||||
wantOut string
|
||||
browsedURL string
|
||||
wantWrite string
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "associate with PR",
|
||||
|
|
@ -368,6 +371,96 @@ func TestViewRun(t *testing.T) {
|
|||
},
|
||||
wantOut: "it's a log\nfor this job\nbeautiful log\n",
|
||||
},
|
||||
{
|
||||
name: "interactive with run log",
|
||||
tty: true,
|
||||
opts: &ViewOptions{
|
||||
Prompt: true,
|
||||
Log: 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/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.StringResponse("pretend these bytes constitute a zip file"))
|
||||
},
|
||||
askStubs: func(as *prompt.AskStubber) {
|
||||
as.StubOne(2)
|
||||
as.StubOne(0)
|
||||
},
|
||||
wantOut: "✓ Downloaded logs to /tmp/gh-run-log-3.zip\n",
|
||||
wantWrite: "pretend these bytes constitute a zip file",
|
||||
},
|
||||
{
|
||||
name: "noninteractive with run log",
|
||||
tty: true,
|
||||
opts: &ViewOptions{
|
||||
RunID: "3",
|
||||
Log: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"),
|
||||
httpmock.JSONResponse(shared.SuccessfulRun))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"),
|
||||
httpmock.StringResponse("pretend these bytes constitute a zip file"))
|
||||
},
|
||||
wantOut: "✓ Downloaded logs to /tmp/gh-run-log-3.zip\n",
|
||||
wantWrite: "pretend these bytes constitute a zip file",
|
||||
},
|
||||
{
|
||||
name: "run log but run is not done",
|
||||
tty: true,
|
||||
opts: &ViewOptions{
|
||||
RunID: "2",
|
||||
Log: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"),
|
||||
httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, "")))
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "run 2 is still in progress; logs will be available when it is complete",
|
||||
},
|
||||
{
|
||||
name: "job log but job is not done",
|
||||
tty: true,
|
||||
opts: &ViewOptions{
|
||||
JobID: "20",
|
||||
Log: true,
|
||||
},
|
||||
httpStubs: func(reg *httpmock.Registry) {
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"),
|
||||
httpmock.JSONResponse(shared.Job{
|
||||
ID: 20,
|
||||
Status: shared.InProgress,
|
||||
RunID: 2,
|
||||
}))
|
||||
reg.Register(
|
||||
httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"),
|
||||
httpmock.JSONResponse(shared.TestRun("in progress", 2, shared.InProgress, "")))
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "job 20 is still in progress; logs will be available when it is complete",
|
||||
},
|
||||
{
|
||||
name: "noninteractive with job",
|
||||
opts: &ViewOptions{
|
||||
|
|
@ -502,6 +595,11 @@ func TestViewRun(t *testing.T) {
|
|||
return notnow
|
||||
}
|
||||
|
||||
fileBuff := bytes.Buffer{}
|
||||
tt.opts.CreateFile = func(fullPath string) (io.Writer, error) {
|
||||
return &fileBuff, nil
|
||||
}
|
||||
|
||||
io, _, stdout, _ := iostreams.Test()
|
||||
io.SetStdoutTTY(tt.tty)
|
||||
tt.opts.IO = io
|
||||
|
|
@ -522,6 +620,9 @@ func TestViewRun(t *testing.T) {
|
|||
err := runView(tt.opts)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if tt.errMsg != "" {
|
||||
assert.Equal(t, tt.errMsg, err.Error())
|
||||
}
|
||||
if !tt.opts.ExitStatus {
|
||||
return
|
||||
}
|
||||
|
|
@ -533,6 +634,9 @@ func TestViewRun(t *testing.T) {
|
|||
if tt.browsedURL != "" {
|
||||
assert.Equal(t, tt.browsedURL, browser.BrowsedURL())
|
||||
}
|
||||
if tt.wantWrite != "" {
|
||||
assert.Equal(t, tt.wantWrite, fileBuff.String())
|
||||
}
|
||||
reg.Verify(t)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue