github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/pkg/cmd/pr/checks/checks.go (about)

     1  package checks
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"time"
     8  
     9  	"github.com/MakeNowJust/heredoc"
    10  	"github.com/andrewhsu/cli/v2/internal/ghrepo"
    11  	"github.com/andrewhsu/cli/v2/pkg/cmd/pr/shared"
    12  	"github.com/andrewhsu/cli/v2/pkg/cmdutil"
    13  	"github.com/andrewhsu/cli/v2/pkg/iostreams"
    14  	"github.com/andrewhsu/cli/v2/utils"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  type browser interface {
    19  	Browse(string) error
    20  }
    21  
    22  type ChecksOptions struct {
    23  	IO      *iostreams.IOStreams
    24  	Browser browser
    25  
    26  	Finder shared.PRFinder
    27  
    28  	SelectorArg string
    29  	WebMode     bool
    30  }
    31  
    32  func NewCmdChecks(f *cmdutil.Factory, runF func(*ChecksOptions) error) *cobra.Command {
    33  	opts := &ChecksOptions{
    34  		IO:      f.IOStreams,
    35  		Browser: f.Browser,
    36  	}
    37  
    38  	cmd := &cobra.Command{
    39  		Use:   "checks [<number> | <url> | <branch>]",
    40  		Short: "Show CI status for a single pull request",
    41  		Long: heredoc.Doc(`
    42  			Show CI status for a single pull request.
    43  
    44  			Without an argument, the pull request that belongs to the current branch
    45  			is selected.			
    46  		`),
    47  		Args: cobra.MaximumNArgs(1),
    48  		RunE: func(cmd *cobra.Command, args []string) error {
    49  			opts.Finder = shared.NewFinder(f)
    50  
    51  			if repoOverride, _ := cmd.Flags().GetString("repo"); repoOverride != "" && len(args) == 0 {
    52  				return &cmdutil.FlagError{Err: errors.New("argument required when using the --repo flag")}
    53  			}
    54  
    55  			if len(args) > 0 {
    56  				opts.SelectorArg = args[0]
    57  			}
    58  
    59  			if runF != nil {
    60  				return runF(opts)
    61  			}
    62  
    63  			return checksRun(opts)
    64  		},
    65  	}
    66  
    67  	cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the web browser to show details about checks")
    68  
    69  	return cmd
    70  }
    71  
    72  func checksRun(opts *ChecksOptions) error {
    73  	findOptions := shared.FindOptions{
    74  		Selector: opts.SelectorArg,
    75  		Fields:   []string{"number", "baseRefName", "statusCheckRollup"},
    76  	}
    77  	if opts.WebMode {
    78  		findOptions.Fields = []string{"number"}
    79  	}
    80  	pr, baseRepo, err := opts.Finder.Find(findOptions)
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	isTerminal := opts.IO.IsStdoutTTY()
    86  
    87  	if opts.WebMode {
    88  		openURL := ghrepo.GenerateRepoURL(baseRepo, "pull/%d/checks", pr.Number)
    89  		if isTerminal {
    90  			fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(openURL))
    91  		}
    92  		return opts.Browser.Browse(openURL)
    93  	}
    94  
    95  	if len(pr.StatusCheckRollup.Nodes) == 0 {
    96  		return fmt.Errorf("no commit found on the pull request")
    97  	}
    98  
    99  	rollup := pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes
   100  	if len(rollup) == 0 {
   101  		return fmt.Errorf("no checks reported on the '%s' branch", pr.BaseRefName)
   102  	}
   103  
   104  	passing := 0
   105  	failing := 0
   106  	pending := 0
   107  
   108  	type output struct {
   109  		mark      string
   110  		bucket    string
   111  		name      string
   112  		elapsed   string
   113  		link      string
   114  		markColor func(string) string
   115  	}
   116  
   117  	cs := opts.IO.ColorScheme()
   118  
   119  	outputs := []output{}
   120  
   121  	for _, c := range pr.StatusCheckRollup.Nodes[0].Commit.StatusCheckRollup.Contexts.Nodes {
   122  		mark := "✓"
   123  		bucket := "pass"
   124  		state := c.State
   125  		markColor := cs.Green
   126  		if state == "" {
   127  			if c.Status == "COMPLETED" {
   128  				state = c.Conclusion
   129  			} else {
   130  				state = c.Status
   131  			}
   132  		}
   133  		switch state {
   134  		case "SUCCESS", "NEUTRAL", "SKIPPED":
   135  			passing++
   136  		case "ERROR", "FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED":
   137  			mark = "X"
   138  			markColor = cs.Red
   139  			failing++
   140  			bucket = "fail"
   141  		default: // "EXPECTED", "REQUESTED", "WAITING", "QUEUED", "PENDING", "IN_PROGRESS", "STALE"
   142  			mark = "-"
   143  			markColor = cs.Yellow
   144  			pending++
   145  			bucket = "pending"
   146  		}
   147  
   148  		elapsed := ""
   149  		zeroTime := time.Time{}
   150  
   151  		if c.StartedAt != zeroTime && c.CompletedAt != zeroTime {
   152  			e := c.CompletedAt.Sub(c.StartedAt)
   153  			if e > 0 {
   154  				elapsed = e.String()
   155  			}
   156  		}
   157  
   158  		link := c.DetailsURL
   159  		if link == "" {
   160  			link = c.TargetURL
   161  		}
   162  
   163  		name := c.Name
   164  		if name == "" {
   165  			name = c.Context
   166  		}
   167  
   168  		outputs = append(outputs, output{mark, bucket, name, elapsed, link, markColor})
   169  	}
   170  
   171  	sort.Slice(outputs, func(i, j int) bool {
   172  		b0 := outputs[i].bucket
   173  		n0 := outputs[i].name
   174  		l0 := outputs[i].link
   175  		b1 := outputs[j].bucket
   176  		n1 := outputs[j].name
   177  		l1 := outputs[j].link
   178  
   179  		if b0 == b1 {
   180  			if n0 == n1 {
   181  				return l0 < l1
   182  			}
   183  			return n0 < n1
   184  		}
   185  
   186  		return (b0 == "fail") || (b0 == "pending" && b1 == "success")
   187  	})
   188  
   189  	tp := utils.NewTablePrinter(opts.IO)
   190  
   191  	for _, o := range outputs {
   192  		if isTerminal {
   193  			tp.AddField(o.mark, nil, o.markColor)
   194  			tp.AddField(o.name, nil, nil)
   195  			tp.AddField(o.elapsed, nil, nil)
   196  			tp.AddField(o.link, nil, nil)
   197  		} else {
   198  			tp.AddField(o.name, nil, nil)
   199  			tp.AddField(o.bucket, nil, nil)
   200  			if o.elapsed == "" {
   201  				tp.AddField("0", nil, nil)
   202  			} else {
   203  				tp.AddField(o.elapsed, nil, nil)
   204  			}
   205  			tp.AddField(o.link, nil, nil)
   206  		}
   207  
   208  		tp.EndRow()
   209  	}
   210  
   211  	summary := ""
   212  	if failing+passing+pending > 0 {
   213  		if failing > 0 {
   214  			summary = "Some checks were not successful"
   215  		} else if pending > 0 {
   216  			summary = "Some checks are still pending"
   217  		} else {
   218  			summary = "All checks were successful"
   219  		}
   220  
   221  		tallies := fmt.Sprintf(
   222  			"%d failing, %d successful, and %d pending checks",
   223  			failing, passing, pending)
   224  
   225  		summary = fmt.Sprintf("%s\n%s", cs.Bold(summary), tallies)
   226  	}
   227  
   228  	if isTerminal {
   229  		fmt.Fprintln(opts.IO.Out, summary)
   230  		fmt.Fprintln(opts.IO.Out)
   231  	}
   232  
   233  	err = tp.Render()
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	if failing+pending > 0 {
   239  		return cmdutil.SilentError
   240  	}
   241  
   242  	return nil
   243  }