Skip to content

Commit 0d0ec84

Browse files
vilmibmvilmibm
authored andcommitted
absorb gh job view into gh run view
1 parent 6bbebcd commit 0d0ec84

File tree

5 files changed

+367
-33
lines changed

5 files changed

+367
-33
lines changed

pkg/cmd/run/shared/presentation.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ func RenderRunHeader(cs *iostreams.ColorScheme, run Run, ago, prNumber string) s
2323
func RenderJobs(cs *iostreams.ColorScheme, jobs []Job, verbose bool) string {
2424
lines := []string{}
2525
for _, job := range jobs {
26+
elapsed := job.CompletedAt.Sub(job.StartedAt)
27+
elapsedStr := fmt.Sprintf(" in %s", elapsed)
28+
if elapsed < 0 {
29+
elapsedStr = ""
30+
}
2631
symbol, symbolColor := Symbol(cs, job.Status, job.Conclusion)
2732
id := cs.Cyanf("%d", job.ID)
28-
lines = append(lines, fmt.Sprintf("%s %s (ID %s)", symbolColor(symbol), job.Name, id))
33+
lines = append(lines, fmt.Sprintf("%s %s%s (ID %s)", symbolColor(symbol), cs.Bold(job.Name), elapsedStr, id))
2934
if verbose || IsFailureState(job.Conclusion) {
3035
for _, step := range job.Steps {
3136
stepSymbol, stepSymColor := Symbol(cs, step.Status, step.Conclusion)

pkg/cmd/run/shared/shared.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type Job struct {
8585
StartedAt time.Time `json:"started_at"`
8686
CompletedAt time.Time `json:"completed_at"`
8787
URL string `json:"html_url"`
88+
RunID int `json:"run_id"`
8889
}
8990

9091
type Step struct {
@@ -226,6 +227,7 @@ func PromptForRun(cs *iostreams.ColorScheme, runs []Run) (string, error) {
226227
for _, run := range runs {
227228
symbol, _ := Symbol(cs, run.Status, run.Conclusion)
228229
candidates = append(candidates,
230+
// TODO truncate commit message, long ones look terrible
229231
fmt.Sprintf("%s %s, %s (%s)", symbol, run.CommitMsg(), run.Name, run.HeadBranch))
230232
}
231233

pkg/cmd/run/shared/test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ var SuccessfulJob Job = Job{
6969
StartedAt: created(),
7070
CompletedAt: updated(),
7171
URL: "jobs/10",
72+
RunID: 3,
7273
Steps: []Step{
7374
{
7475
Name: "fob the barz",
@@ -93,6 +94,7 @@ var FailedJob Job = Job{
9394
StartedAt: created(),
9495
CompletedAt: updated(),
9596
URL: "jobs/20",
97+
RunID: 1234,
9698
Steps: []Step{
9799
{
98100
Name: "barf the quux",

pkg/cmd/run/view/view.go

Lines changed: 188 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,19 @@ package view
33
import (
44
"errors"
55
"fmt"
6+
"io"
67
"net/http"
78
"time"
89

10+
"github.com/AlecAivazis/survey/v2"
911
"github.com/MakeNowJust/heredoc"
1012
"github.com/cli/cli/api"
13+
"github.com/cli/cli/internal/ghinstance"
1114
"github.com/cli/cli/internal/ghrepo"
1215
"github.com/cli/cli/pkg/cmd/run/shared"
1316
"github.com/cli/cli/pkg/cmdutil"
1417
"github.com/cli/cli/pkg/iostreams"
18+
"github.com/cli/cli/pkg/prompt"
1519
"github.com/cli/cli/utils"
1620
"github.com/spf13/cobra"
1721
)
@@ -22,8 +26,10 @@ type ViewOptions struct {
2226
BaseRepo func() (ghrepo.Interface, error)
2327

2428
RunID string
29+
JobID string
2530
Verbose bool
2631
ExitStatus bool
32+
Log bool
2733

2834
Prompt bool
2935

@@ -42,25 +48,42 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
4248
Args: cobra.MaximumNArgs(1),
4349
Hidden: true,
4450
Example: heredoc.Doc(`
45-
# Interactively select a run to view
51+
# Interactively select a run to view, optionally drilling down to a job
4652
$ gh run view
4753
4854
# View a specific run
49-
$ gh run view 0451
55+
$ gh run view 12345
56+
57+
# View a specific job within a run
58+
$ gh run view --job 456789
59+
60+
# View the full log for a specific job
61+
$ gh run view --log --job 456789
5062
5163
# Exit non-zero if a run failed
52-
$ gh run view 0451 -e && echo "job pending or passed"
64+
$ gh run view 0451 -e && echo "run pending or passed"
5365
`),
66+
// TODO should exit status respect only a selected job if --job is passed?
5467
RunE: func(cmd *cobra.Command, args []string) error {
5568
// support `-R, --repo` override
5669
opts.BaseRepo = f.BaseRepo
5770

58-
if len(args) > 0 {
71+
if len(args) == 0 && opts.JobID == "" {
72+
if !opts.IO.CanPrompt() {
73+
return &cmdutil.FlagError{Err: errors.New("run or job ID required when not running interactively")}
74+
} else {
75+
opts.Prompt = true
76+
}
77+
} else if len(args) > 0 {
5978
opts.RunID = args[0]
60-
} else if !opts.IO.CanPrompt() {
61-
return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")}
62-
} else {
63-
opts.Prompt = true
79+
}
80+
81+
if opts.RunID != "" && opts.JobID != "" {
82+
opts.RunID = ""
83+
if opts.IO.CanPrompt() {
84+
cs := opts.IO.ColorScheme()
85+
fmt.Fprintf(opts.IO.ErrOut, "%s both run and job IDs specified; ignoring run ID\n", cs.WarningIcon())
86+
}
6487
}
6588

6689
if runF != nil {
@@ -72,28 +95,50 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
7295
cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show job steps")
7396
// TODO should we try and expose pending via another exit code?
7497
cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if run failed")
98+
cmd.Flags().StringVarP(&opts.JobID, "job", "j", "", "View a specific job ID from a run")
99+
cmd.Flags().BoolVar(&opts.Log, "log", false, "View full log for either a run or specific job")
75100

76101
return cmd
77102
}
78103

79104
func runView(opts *ViewOptions) error {
80-
c, err := opts.HttpClient()
105+
httpClient, err := opts.HttpClient()
81106
if err != nil {
82107
return fmt.Errorf("failed to create http client: %w", err)
83108
}
84-
client := api.NewClientFromHTTP(c)
109+
client := api.NewClientFromHTTP(httpClient)
85110

86111
repo, err := opts.BaseRepo()
87112
if err != nil {
88113
return fmt.Errorf("failed to determine base repo: %w", err)
89114
}
90115

116+
jobID := opts.JobID
91117
runID := opts.RunID
118+
var selectedJob *shared.Job
119+
var run *shared.Run
120+
var jobs []shared.Job
121+
122+
defer opts.IO.StopProgressIndicator()
123+
124+
if jobID != "" {
125+
opts.IO.StartProgressIndicator()
126+
selectedJob, err = getJob(client, repo, jobID)
127+
opts.IO.StopProgressIndicator()
128+
if err != nil {
129+
return fmt.Errorf("failed to get job: %w", err)
130+
}
131+
// TODO once more stuff is merged, standardize on using ints
132+
runID = fmt.Sprintf("%d", selectedJob.RunID)
133+
}
134+
135+
cs := opts.IO.ColorScheme()
92136

93137
if opts.Prompt {
94-
cs := opts.IO.ColorScheme()
95138
// TODO arbitrary limit
139+
opts.IO.StartProgressIndicator()
96140
runs, err := shared.GetRuns(client, repo, 10)
141+
opts.IO.StopProgressIndicator()
97142
if err != nil {
98143
return fmt.Errorf("failed to get runs: %w", err)
99144
}
@@ -104,24 +149,71 @@ func runView(opts *ViewOptions) error {
104149
}
105150

106151
opts.IO.StartProgressIndicator()
107-
defer opts.IO.StopProgressIndicator()
108-
109-
run, err := shared.GetRun(client, repo, runID)
152+
run, err = shared.GetRun(client, repo, runID)
153+
opts.IO.StopProgressIndicator()
110154
if err != nil {
111155
return fmt.Errorf("failed to get run: %w", err)
112156
}
113157

158+
if opts.Prompt {
159+
opts.IO.StartProgressIndicator()
160+
jobs, err = shared.GetJobs(client, repo, *run)
161+
opts.IO.StopProgressIndicator()
162+
if err != nil {
163+
return err
164+
}
165+
if len(jobs) > 1 {
166+
selectedJob, err = promptForJob(cs, jobs)
167+
if err != nil {
168+
return err
169+
}
170+
}
171+
}
172+
173+
opts.IO.StartProgressIndicator()
174+
175+
if opts.Log && selectedJob != nil {
176+
r, err := jobLog(httpClient, repo, selectedJob.ID)
177+
if err != nil {
178+
return err
179+
}
180+
opts.IO.StopProgressIndicator()
181+
182+
err = opts.IO.StartPager()
183+
if err != nil {
184+
return err
185+
}
186+
defer opts.IO.StopPager()
187+
188+
if _, err := io.Copy(opts.IO.Out, r); err != nil {
189+
return fmt.Errorf("failed to read log: %w", err)
190+
}
191+
192+
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
193+
return cmdutil.SilentError
194+
}
195+
196+
return nil
197+
}
198+
199+
// TODO support --log without selectedJob
200+
201+
if selectedJob == nil && len(jobs) == 0 {
202+
jobs, err = shared.GetJobs(client, repo, *run)
203+
opts.IO.StopProgressIndicator()
204+
if err != nil {
205+
return fmt.Errorf("failed to get jobs: %w", err)
206+
}
207+
} else if selectedJob != nil {
208+
jobs = []shared.Job{*selectedJob}
209+
}
210+
114211
prNumber := ""
115212
number, err := shared.PullRequestForRun(client, repo, *run)
116213
if err == nil {
117214
prNumber = fmt.Sprintf(" #%d", number)
118215
}
119216

120-
jobs, err := shared.GetJobs(client, repo, *run)
121-
if err != nil {
122-
return fmt.Errorf("failed to get jobs: %w", err)
123-
}
124-
125217
var annotations []shared.Annotation
126218

127219
var annotationErr error
@@ -135,12 +227,12 @@ func runView(opts *ViewOptions) error {
135227
}
136228

137229
opts.IO.StopProgressIndicator()
230+
138231
if annotationErr != nil {
139232
return fmt.Errorf("failed to get annotations: %w", annotationErr)
140233
}
141234

142235
out := opts.IO.Out
143-
cs := opts.IO.ColorScheme()
144236

145237
ago := opts.Now().Sub(run.CreatedAt)
146238

@@ -162,23 +254,93 @@ func runView(opts *ViewOptions) error {
162254
return nil
163255
}
164256

165-
fmt.Fprintln(out, cs.Bold("JOBS"))
166-
167-
fmt.Fprintln(out, shared.RenderJobs(cs, jobs, opts.Verbose))
257+
if selectedJob == nil {
258+
fmt.Fprintln(out, cs.Bold("JOBS"))
259+
fmt.Fprintln(out, shared.RenderJobs(cs, jobs, opts.Verbose))
260+
} else {
261+
fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true))
262+
}
168263

169264
if len(annotations) > 0 {
170265
fmt.Fprintln(out)
171266
fmt.Fprintln(out, cs.Bold("ANNOTATIONS"))
172267
fmt.Fprintln(out, shared.RenderAnnotations(cs, annotations))
173268
}
174269

175-
fmt.Fprintln(out)
176-
fmt.Fprintln(out, "For more information about a job, try: gh job view <job-id>")
177-
fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL)
270+
if selectedJob == nil {
271+
fmt.Fprintln(out)
272+
fmt.Fprintln(out, "For more information about a job, try: gh run view --job=<job-id>")
273+
// TODO note about run view --log when that exists
274+
fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL)
275+
} else {
276+
fmt.Fprintln(out)
277+
// TODO this does not exist yet
278+
fmt.Fprintf(out, "To see the full job log, try: gh run view --log --job=%d\n", selectedJob.ID)
279+
fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL)
280+
}
178281

179282
if opts.ExitStatus && shared.IsFailureState(run.Conclusion) {
180283
return cmdutil.SilentError
181284
}
182285

183286
return nil
184287
}
288+
289+
func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Job, error) {
290+
path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID)
291+
292+
var result shared.Job
293+
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
294+
if err != nil {
295+
return nil, err
296+
}
297+
298+
return &result, nil
299+
}
300+
301+
func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID int) (io.ReadCloser, error) {
302+
url := fmt.Sprintf("%srepos/%s/actions/jobs/%d/logs",
303+
ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), jobID)
304+
req, err := http.NewRequest("GET", url, nil)
305+
if err != nil {
306+
return nil, err
307+
}
308+
309+
resp, err := httpClient.Do(req)
310+
if err != nil {
311+
return nil, err
312+
}
313+
314+
if resp.StatusCode == 404 {
315+
return nil, errors.New("job not found")
316+
} else if resp.StatusCode != 200 {
317+
return nil, api.HandleHTTPError(resp)
318+
}
319+
320+
return resp.Body, nil
321+
}
322+
323+
func promptForJob(cs *iostreams.ColorScheme, jobs []shared.Job) (*shared.Job, error) {
324+
candidates := []string{"View all jobs in this run"}
325+
for _, job := range jobs {
326+
symbol, _ := shared.Symbol(cs, job.Status, job.Conclusion)
327+
candidates = append(candidates, fmt.Sprintf("%s %s", symbol, job.Name))
328+
}
329+
330+
var selected int
331+
err := prompt.SurveyAskOne(&survey.Select{
332+
Message: "View a specific job in this run?",
333+
Options: candidates,
334+
PageSize: 12,
335+
}, &selected)
336+
if err != nil {
337+
return nil, err
338+
}
339+
340+
if selected > 0 {
341+
return &jobs[selected-1], nil
342+
}
343+
344+
// User wants to see all jobs
345+
return nil, nil
346+
}

0 commit comments

Comments
 (0)