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 }