Skip to content

Commit 8dd93e1

Browse files
committed
gh pr checks
A basic first pass on gh pr checks that shows all check runs for a given PR's latest commit.
1 parent bb65ca0 commit 8dd93e1

File tree

9 files changed

+645
-5
lines changed

9 files changed

+645
-5
lines changed

api/queries_pr.go

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"net/http"
99
"strings"
10+
"time"
1011

1112
"github.com/cli/cli/internal/ghinstance"
1213
"github.com/cli/cli/internal/ghrepo"
@@ -72,12 +73,19 @@ type PullRequest struct {
7273
TotalCount int
7374
Nodes []struct {
7475
Commit struct {
76+
Oid string
7577
StatusCheckRollup struct {
7678
Contexts struct {
7779
Nodes []struct {
78-
State string
79-
Status string
80-
Conclusion string
80+
Name string
81+
Context string
82+
State string
83+
Status string
84+
Conclusion string
85+
StartedAt time.Time
86+
CompletedAt time.Time
87+
DetailsURL string
88+
TargetURL string
8189
}
8290
}
8391
}
@@ -272,9 +280,11 @@ func PullRequests(client *Client, repo ghrepo.Interface, currentPRNumber int, cu
272280
contexts(last: 100) {
273281
nodes {
274282
...on StatusContext {
283+
context
275284
state
276285
}
277286
...on CheckRun {
287+
name
278288
status
279289
conclusion
280290
}
@@ -418,8 +428,32 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
418428
author {
419429
login
420430
}
421-
commits {
431+
commits(last: 1) {
422432
totalCount
433+
nodes {
434+
commit {
435+
oid
436+
statusCheckRollup {
437+
contexts(last: 100) {
438+
nodes {
439+
...on StatusContext {
440+
context
441+
state
442+
targetUrl
443+
}
444+
...on CheckRun {
445+
name
446+
status
447+
conclusion
448+
startedAt
449+
completedAt
450+
detailsUrl
451+
}
452+
}
453+
}
454+
}
455+
}
456+
}
423457
}
424458
baseRefName
425459
headRefName
@@ -524,8 +558,32 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
524558
author {
525559
login
526560
}
527-
commits {
561+
commits(last: 1) {
528562
totalCount
563+
nodes {
564+
commit {
565+
oid
566+
statusCheckRollup {
567+
contexts(last: 100) {
568+
nodes {
569+
...on StatusContext {
570+
context
571+
state
572+
targetUrl
573+
}
574+
...on CheckRun {
575+
name
576+
status
577+
conclusion
578+
startedAt
579+
completedAt
580+
detailsUrl
581+
}
582+
}
583+
}
584+
}
585+
}
586+
}
529587
}
530588
url
531589
baseRefName

pkg/cmd/pr/checks/checks.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package checks
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"sort"
8+
"time"
9+
10+
"github.com/cli/cli/api"
11+
"github.com/cli/cli/context"
12+
"github.com/cli/cli/internal/ghrepo"
13+
"github.com/cli/cli/pkg/cmd/pr/shared"
14+
"github.com/cli/cli/pkg/cmdutil"
15+
"github.com/cli/cli/pkg/iostreams"
16+
"github.com/cli/cli/utils"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
type ChecksOptions struct {
21+
HttpClient func() (*http.Client, error)
22+
IO *iostreams.IOStreams
23+
BaseRepo func() (ghrepo.Interface, error)
24+
Branch func() (string, error)
25+
Remotes func() (context.Remotes, error)
26+
27+
SelectorArg string
28+
}
29+
30+
func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Command {
31+
opts := &ChecksOptions{
32+
IO: f.IOStreams,
33+
HttpClient: f.HttpClient,
34+
Branch: f.Branch,
35+
Remotes: f.Remotes,
36+
BaseRepo: f.BaseRepo,
37+
}
38+
39+
cmd := &cobra.Command{
40+
Use: "checks",
41+
Short: "Show CI status for a single pull request",
42+
Args: cobra.MaximumNArgs(1),
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
// support `-R, --repo` override
45+
opts.BaseRepo = f.BaseRepo
46+
47+
if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
48+
return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
49+
}
50+
51+
if len(args) > 0 {
52+
opts.SelectorArg = args[0]
53+
}
54+
55+
if runF != nil {
56+
return runF(opts)
57+
}
58+
59+
return checksRun(opts)
60+
},
61+
}
62+
63+
return cmd
64+
}
65+
66+
func checksRun(opts *ChecksOptions) error {
67+
httpClient, err := opts.HttpClient()
68+
if err != nil {
69+
return err
70+
}
71+
apiClient := api.NewClientFromHTTP(httpClient)
72+
73+
pr, _, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg)
74+
if err != nil {
75+
return err
76+
}
77+
78+
if len(pr.Commits.Nodes) == 0 {
79+
return nil
80+
}
81+
82+
rollup := pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes
83+
if len(rollup) == 0 {
84+
return nil
85+
}
86+
87+
passing := 0
88+
failing := 0
89+
pending := 0
90+
91+
type output struct {
92+
mark string
93+
bucket string
94+
name string
95+
elapsed string
96+
link string
97+
}
98+
99+
outputs := []output{}
100+
101+
for _, c := range pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
102+
mark := ""
103+
bucket := ""
104+
state := c.State
105+
if state == "" {
106+
if c.Status == "COMPLETED" {
107+
state = c.Conclusion
108+
} else {
109+
state = c.Status
110+
}
111+
}
112+
switch state {
113+
case "SUCCESS", "NEUTRAL", "SKIPPED":
114+
mark = utils.GreenCheck()
115+
passing++
116+
bucket = "pass"
117+
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
118+
mark = utils.RedX()
119+
failing++
120+
bucket = "fail"
121+
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE":
122+
mark = utils.YellowDash()
123+
pending++
124+
bucket = "pending"
125+
default:
126+
panic(fmt.Errorf("unsupported status: %q", state))
127+
}
128+
129+
elapsed := ""
130+
zeroTime := time.Time{}
131+
132+
if c.StartedAt != zeroTime && c.CompletedAt != zeroTime {
133+
e := c.CompletedAt.Sub(c.StartedAt)
134+
if e > 0 {
135+
elapsed = e.String()
136+
}
137+
}
138+
139+
link := c.DetailsURL
140+
if link == "" {
141+
link = c.TargetURL
142+
}
143+
144+
name := c.Name
145+
if name == "" {
146+
name = c.Context
147+
}
148+
149+
outputs = append(outputs, output{mark, bucket, name, elapsed, link})
150+
}
151+
152+
sort.Slice(outputs, func(i, j int) bool {
153+
if outputs[i].bucket == outputs[j].bucket {
154+
return outputs[i].name < outputs[j].name
155+
} else {
156+
if outputs[i].bucket == "fail" {
157+
return true
158+
} else if outputs[i].bucket == "pending" && outputs[j].bucket == "success" {
159+
return true
160+
}
161+
}
162+
163+
return false
164+
})
165+
166+
tp := utils.NewTablePrinter(opts.IO)
167+
168+
for _, o := range outputs {
169+
if opts.IO.IsStdoutTTY() {
170+
tp.AddField(o.mark, nil, nil)
171+
tp.AddField(o.name, nil, nil)
172+
tp.AddField(o.elapsed, nil, nil)
173+
tp.AddField(o.link, nil, nil)
174+
} else {
175+
tp.AddField(o.name, nil, nil)
176+
tp.AddField(o.bucket, nil, nil)
177+
if o.elapsed == "" {
178+
tp.AddField("0", nil, nil)
179+
} else {
180+
tp.AddField(o.elapsed, nil, nil)
181+
}
182+
tp.AddField(o.link, nil, nil)
183+
}
184+
185+
tp.EndRow()
186+
}
187+
188+
summary := ""
189+
if failing+passing+pending > 0 {
190+
if failing > 0 {
191+
summary = "Some checks were not successful"
192+
} else if pending > 0 {
193+
summary = "Some checks are still pending"
194+
} else {
195+
summary = "All checks were successful"
196+
}
197+
198+
tallies := fmt.Sprintf(
199+
"%d failing, %d successful, and %d pending checks",
200+
failing, passing, pending)
201+
202+
summary = fmt.Sprintf("%s\n%s", utils.Bold(summary), tallies)
203+
}
204+
205+
if opts.IO.IsStdoutTTY() {
206+
fmt.Fprintln(opts.IO.Out, summary)
207+
fmt.Fprintln(opts.IO.Out)
208+
}
209+
210+
return tp.Render()
211+
}

0 commit comments

Comments
 (0)