Skip to content

Commit ab2c38e

Browse files
author
Nate Smith
authored
Merge pull request cli#1563 from cli/checks
gh pr checks
2 parents 72e9747 + eb132a1 commit ab2c38e

File tree

9 files changed

+671
-6
lines changed

9 files changed

+671
-6
lines changed

api/queries_pr.go

Lines changed: 62 additions & 6 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,11 +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 {
278287
status
279-
conclusion
280288
}
281289
}
282290
}
@@ -418,8 +426,32 @@ func PullRequestByNumber(client *Client, repo ghrepo.Interface, number int) (*Pu
418426
author {
419427
login
420428
}
421-
commits {
429+
commits(last: 1) {
422430
totalCount
431+
nodes {
432+
commit {
433+
oid
434+
statusCheckRollup {
435+
contexts(last: 100) {
436+
nodes {
437+
...on StatusContext {
438+
context
439+
state
440+
targetUrl
441+
}
442+
...on CheckRun {
443+
name
444+
status
445+
conclusion
446+
startedAt
447+
completedAt
448+
detailsUrl
449+
}
450+
}
451+
}
452+
}
453+
}
454+
}
423455
}
424456
baseRefName
425457
headRefName
@@ -524,8 +556,32 @@ func PullRequestForBranch(client *Client, repo ghrepo.Interface, baseBranch, hea
524556
author {
525557
login
526558
}
527-
commits {
559+
commits(last: 1) {
528560
totalCount
561+
nodes {
562+
commit {
563+
oid
564+
statusCheckRollup {
565+
contexts(last: 100) {
566+
nodes {
567+
...on StatusContext {
568+
context
569+
state
570+
targetUrl
571+
}
572+
...on CheckRun {
573+
name
574+
status
575+
conclusion
576+
startedAt
577+
completedAt
578+
detailsUrl
579+
}
580+
}
581+
}
582+
}
583+
}
584+
}
529585
}
530586
url
531587
baseRefName

pkg/cmd/pr/checks/checks.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
markColor func(string) string
98+
}
99+
100+
outputs := []output{}
101+
102+
for _, c := range pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
103+
mark := "✓"
104+
bucket := "pass"
105+
state := c.State
106+
markColor := utils.Green
107+
if state == "" {
108+
if c.Status == "COMPLETED" {
109+
state = c.Conclusion
110+
} else {
111+
state = c.Status
112+
}
113+
}
114+
switch state {
115+
case "SUCCESS", "NEUTRAL", "SKIPPED":
116+
passing++
117+
case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
118+
mark = "X"
119+
markColor = utils.Red
120+
failing++
121+
bucket = "fail"
122+
case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE":
123+
mark = "-"
124+
markColor = utils.Yellow
125+
pending++
126+
bucket = "pending"
127+
default:
128+
panic(fmt.Errorf("unsupported status: %q", state))
129+
}
130+
131+
elapsed := ""
132+
zeroTime := time.Time{}
133+
134+
if c.StartedAt != zeroTime && c.CompletedAt != zeroTime {
135+
e := c.CompletedAt.Sub(c.StartedAt)
136+
if e > 0 {
137+
elapsed = e.String()
138+
}
139+
}
140+
141+
link := c.DetailsURL
142+
if link == "" {
143+
link = c.TargetURL
144+
}
145+
146+
name := c.Name
147+
if name == "" {
148+
name = c.Context
149+
}
150+
151+
outputs = append(outputs, output{mark, bucket, name, elapsed, link, markColor})
152+
}
153+
154+
sort.Slice(outputs, func(i, j int) bool {
155+
b0 := outputs[i].bucket
156+
n0 := outputs[i].name
157+
l0 := outputs[i].link
158+
b1 := outputs[j].bucket
159+
n1 := outputs[j].name
160+
l1 := outputs[j].link
161+
162+
if b0 == b1 {
163+
if n0 == n1 {
164+
return l0 < l1
165+
} else {
166+
return n0 < n1
167+
}
168+
}
169+
170+
return (b0 == "fail") || (b0 == "pending" && b1 == "success")
171+
})
172+
173+
tp := utils.NewTablePrinter(opts.IO)
174+
175+
for _, o := range outputs {
176+
if opts.IO.IsStdoutTTY() {
177+
tp.AddField(o.mark, nil, o.markColor)
178+
tp.AddField(o.name, nil, nil)
179+
tp.AddField(o.elapsed, nil, nil)
180+
tp.AddField(o.link, nil, nil)
181+
} else {
182+
tp.AddField(o.name, nil, nil)
183+
tp.AddField(o.bucket, nil, nil)
184+
if o.elapsed == "" {
185+
tp.AddField("0", nil, nil)
186+
} else {
187+
tp.AddField(o.elapsed, nil, nil)
188+
}
189+
tp.AddField(o.link, nil, nil)
190+
}
191+
192+
tp.EndRow()
193+
}
194+
195+
summary := ""
196+
if failing+passing+pending > 0 {
197+
if failing > 0 {
198+
summary = "Some checks were not successful"
199+
} else if pending > 0 {
200+
summary = "Some checks are still pending"
201+
} else {
202+
summary = "All checks were successful"
203+
}
204+
205+
tallies := fmt.Sprintf(
206+
"%d failing, %d successful, and %d pending checks",
207+
failing, passing, pending)
208+
209+
summary = fmt.Sprintf("%s\n%s", utils.Bold(summary), tallies)
210+
}
211+
212+
if opts.IO.IsStdoutTTY() {
213+
fmt.Fprintln(opts.IO.Out, summary)
214+
fmt.Fprintln(opts.IO.Out)
215+
}
216+
217+
err = tp.Render()
218+
if err != nil {
219+
return err
220+
}
221+
222+
if failing+pending > 0 {
223+
return cmdutil.SilentError
224+
}
225+
226+
return nil
227+
}

0 commit comments

Comments
 (0)