-
Notifications
You must be signed in to change notification settings - Fork 7.3k
gh pr checks #1563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh pr checks #1563
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,227 @@ | ||
| package checks | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
| "sort" | ||
| "time" | ||
|
|
||
| "github.com/cli/cli/api" | ||
| "github.com/cli/cli/context" | ||
| "github.com/cli/cli/internal/ghrepo" | ||
| "github.com/cli/cli/pkg/cmd/pr/shared" | ||
| "github.com/cli/cli/pkg/cmdutil" | ||
| "github.com/cli/cli/pkg/iostreams" | ||
| "github.com/cli/cli/utils" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| type ChecksOptions struct { | ||
| HttpClient func() (*http.Client, error) | ||
| IO *iostreams.IOStreams | ||
| BaseRepo func() (ghrepo.Interface, error) | ||
| Branch func() (string, error) | ||
| Remotes func() (context.Remotes, error) | ||
|
|
||
| SelectorArg string | ||
| } | ||
|
|
||
| func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Command { | ||
| opts := &ChecksOptions{ | ||
| IO: f.IOStreams, | ||
| HttpClient: f.HttpClient, | ||
| Branch: f.Branch, | ||
| Remotes: f.Remotes, | ||
| BaseRepo: f.BaseRepo, | ||
| } | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "checks", | ||
| Short: "Show CI status for a single pull request", | ||
| Args: cobra.MaximumNArgs(1), | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| // support `-R, --repo` override | ||
| opts.BaseRepo = f.BaseRepo | ||
|
|
||
| if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 { | ||
| return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")} | ||
| } | ||
|
|
||
| if len(args) > 0 { | ||
| opts.SelectorArg = args[0] | ||
| } | ||
|
|
||
| if runF != nil { | ||
| return runF(opts) | ||
| } | ||
|
|
||
| return checksRun(opts) | ||
| }, | ||
| } | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func checksRun(opts *ChecksOptions) error { | ||
| httpClient, err := opts.HttpClient() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| apiClient := api.NewClientFromHTTP(httpClient) | ||
|
|
||
| pr, _, err := shared.PRFromArgs(apiClient, opts.BaseRepo, opts.Branch, opts.Remotes, opts.SelectorArg) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if len(pr.Commits.Nodes) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| rollup := pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes | ||
| if len(rollup) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| passing := 0 | ||
| failing := 0 | ||
| pending := 0 | ||
|
|
||
| type output struct { | ||
| mark string | ||
| bucket string | ||
| name string | ||
| elapsed string | ||
| link string | ||
| markColor func(string) string | ||
| } | ||
|
|
||
| outputs := []output{} | ||
|
|
||
| for _, c := range pr.Commits.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes { | ||
| mark := "✓" | ||
| bucket := "pass" | ||
| state := c.State | ||
| markColor := utils.Green | ||
| if state == "" { | ||
| if c.Status == "COMPLETED" { | ||
| state = c.Conclusion | ||
| } else { | ||
| state = c.Status | ||
| } | ||
| } | ||
| switch state { | ||
| case "SUCCESS", "NEUTRAL", "SKIPPED": | ||
| passing++ | ||
| case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED": | ||
| mark = "X" | ||
| markColor = utils.Red | ||
| failing++ | ||
| bucket = "fail" | ||
| case "EXPECTED", "REQUESTED", "QUEUED", "PENDING", "IN_PROGRESS", "STALE": | ||
| mark = "-" | ||
| markColor = utils.Yellow | ||
| pending++ | ||
| bucket = "pending" | ||
| default: | ||
| panic(fmt.Errorf("unsupported status: %q", state)) | ||
| } | ||
|
|
||
| elapsed := "" | ||
| zeroTime := time.Time{} | ||
|
|
||
| if c.StartedAt != zeroTime && c.CompletedAt != zeroTime { | ||
| e := c.CompletedAt.Sub(c.StartedAt) | ||
| if e > 0 { | ||
| elapsed = e.String() | ||
| } | ||
| } | ||
|
|
||
| link := c.DetailsURL | ||
| if link == "" { | ||
| link = c.TargetURL | ||
| } | ||
|
|
||
| name := c.Name | ||
| if name == "" { | ||
| name = c.Context | ||
| } | ||
|
|
||
| outputs = append(outputs, output{mark, bucket, name, elapsed, link, markColor}) | ||
| } | ||
|
|
||
| sort.Slice(outputs, func(i, j int) bool { | ||
vilmibm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| b0 := outputs[i].bucket | ||
| n0 := outputs[i].name | ||
| l0 := outputs[i].link | ||
| b1 := outputs[j].bucket | ||
| n1 := outputs[j].name | ||
| l1 := outputs[j].link | ||
|
|
||
| if b0 == b1 { | ||
| if n0 == n1 { | ||
| return l0 < l1 | ||
| } else { | ||
| return n0 < n1 | ||
| } | ||
| } | ||
|
|
||
| return (b0 == "fail") || (b0 == "pending" && b1 == "success") | ||
| }) | ||
|
|
||
| tp := utils.NewTablePrinter(opts.IO) | ||
|
|
||
| for _, o := range outputs { | ||
| if opts.IO.IsStdoutTTY() { | ||
| tp.AddField(o.mark, nil, o.markColor) | ||
| tp.AddField(o.name, nil, nil) | ||
| tp.AddField(o.elapsed, nil, nil) | ||
| tp.AddField(o.link, nil, nil) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one is tricky! We render the URL in this column, but the way I designed TablePrinter is to automatically truncate long columns in narrow terminals. That means that the URL column will be the first one put up on the chopping block, and URLs become completely useless after they are truncated. A good followup candidate would be to add a feature to TablePrinter to mark some columns as exempt from elastic resizing/truncation. |
||
| } else { | ||
| tp.AddField(o.name, nil, nil) | ||
| tp.AddField(o.bucket, nil, nil) | ||
| if o.elapsed == "" { | ||
| tp.AddField("0", nil, nil) | ||
| } else { | ||
| tp.AddField(o.elapsed, nil, nil) | ||
| } | ||
| tp.AddField(o.link, nil, nil) | ||
| } | ||
|
|
||
| tp.EndRow() | ||
| } | ||
|
|
||
| summary := "" | ||
| if failing+passing+pending > 0 { | ||
| if failing > 0 { | ||
| summary = "Some checks were not successful" | ||
| } else if pending > 0 { | ||
| summary = "Some checks are still pending" | ||
| } else { | ||
| summary = "All checks were successful" | ||
| } | ||
|
|
||
| tallies := fmt.Sprintf( | ||
| "%d failing, %d successful, and %d pending checks", | ||
| failing, passing, pending) | ||
|
|
||
| summary = fmt.Sprintf("%s\n%s", utils.Bold(summary), tallies) | ||
| } | ||
|
|
||
| if opts.IO.IsStdoutTTY() { | ||
| fmt.Fprintln(opts.IO.Out, summary) | ||
| fmt.Fprintln(opts.IO.Out) | ||
| } | ||
|
|
||
| err = tp.Render() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if failing+pending > 0 { | ||
| return cmdutil.SilentError | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My slight concern with expanding the
PullRequestByNumberis that the Checks+Statuses data is unnecessarily fetched every time a user does any operation on a pull request, such asgh pr view 123 --web. However, I do understand that this approach is the most feasible to implement, since any other course of action would require us to either duplicate the logic ofshared.PRFromArgs()to fetch extra fields, or to come up with a shippable solution to #1081. I'm only bringing this up as a note that this is a good candidate to revisit in a few weeks 👍