github.com/abdfnx/gh-api@v0.0.0-20210414084727-f5432eec23b8/pkg/cmd/run/watch/watch.go (about) 1 package watch 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "runtime" 8 "time" 9 10 "github.com/abdfnx/gh-api/api" 11 "github.com/abdfnx/gh-api/internal/ghrepo" 12 "github.com/abdfnx/gh-api/pkg/cmd/run/shared" 13 "github.com/abdfnx/gh-api/pkg/cmdutil" 14 "github.com/abdfnx/gh-api/pkg/iostreams" 15 "github.com/abdfnx/gh-api/utils" 16 "github.com/spf13/cobra" 17 ) 18 19 const defaultInterval int = 3 20 21 type WatchOptions struct { 22 IO *iostreams.IOStreams 23 HttpClient func() (*http.Client, error) 24 BaseRepo func() (ghrepo.Interface, error) 25 26 RunID string 27 Interval int 28 ExitStatus bool 29 30 Prompt bool 31 32 Now func() time.Time 33 } 34 35 func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Command { 36 opts := &WatchOptions{ 37 IO: f.IOStreams, 38 HttpClient: f.HttpClient, 39 Now: time.Now, 40 } 41 42 cmd := &cobra.Command{ 43 Use: "watch <run-selector>", 44 Short: "Watch a run until it completes, showing its progress", 45 Annotations: map[string]string{ 46 "IsActions": "true", 47 }, 48 RunE: func(cmd *cobra.Command, args []string) error { 49 // support `-R, --repo` override 50 opts.BaseRepo = f.BaseRepo 51 52 if len(args) > 0 { 53 opts.RunID = args[0] 54 } else if !opts.IO.CanPrompt() { 55 return &cmdutil.FlagError{Err: errors.New("run ID required when not running interactively")} 56 } else { 57 opts.Prompt = true 58 } 59 60 if runF != nil { 61 return runF(opts) 62 } 63 64 return watchRun(opts) 65 }, 66 } 67 cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if run fails") 68 cmd.Flags().IntVarP(&opts.Interval, "interval", "i", defaultInterval, "Refresh interval in seconds") 69 70 return cmd 71 } 72 73 func watchRun(opts *WatchOptions) error { 74 c, err := opts.HttpClient() 75 if err != nil { 76 return fmt.Errorf("failed to create http client: %w", err) 77 } 78 client := api.NewClientFromHTTP(c) 79 80 repo, err := opts.BaseRepo() 81 if err != nil { 82 return fmt.Errorf("failed to determine base repo: %w", err) 83 } 84 85 out := opts.IO.Out 86 cs := opts.IO.ColorScheme() 87 88 runID := opts.RunID 89 var run *shared.Run 90 91 if opts.Prompt { 92 runs, err := shared.GetRunsWithFilter(client, repo, 10, func(run shared.Run) bool { 93 return run.Status != shared.Completed 94 }) 95 if err != nil { 96 return fmt.Errorf("failed to get runs: %w", err) 97 } 98 if len(runs) == 0 { 99 return fmt.Errorf("found no in progress runs to watch") 100 } 101 runID, err = shared.PromptForRun(cs, runs) 102 if err != nil { 103 return err 104 } 105 // TODO silly stopgap until dust settles and PromptForRun can just return a run 106 for _, r := range runs { 107 if fmt.Sprintf("%d", r.ID) == runID { 108 run = &r 109 break 110 } 111 } 112 } else { 113 run, err = shared.GetRun(client, repo, runID) 114 if err != nil { 115 return fmt.Errorf("failed to get run: %w", err) 116 } 117 } 118 119 if run.Status == shared.Completed { 120 fmt.Fprintf(out, "Run %s (%s) has already completed with '%s'\n", cs.Bold(run.Name), cs.Cyanf("%d", run.ID), run.Conclusion) 121 return nil 122 } 123 124 prNumber := "" 125 number, err := shared.PullRequestForRun(client, repo, *run) 126 if err == nil { 127 prNumber = fmt.Sprintf(" #%d", number) 128 } 129 130 opts.IO.EnableVirtualTerminalProcessing() 131 // clear entire screen 132 fmt.Fprintf(out, "\x1b[2J") 133 134 annotationCache := map[int][]shared.Annotation{} 135 136 duration, err := time.ParseDuration(fmt.Sprintf("%ds", opts.Interval)) 137 if err != nil { 138 return fmt.Errorf("could not parse interval: %w", err) 139 } 140 141 for run.Status != shared.Completed { 142 run, err = renderRun(*opts, client, repo, run, prNumber, annotationCache) 143 if err != nil { 144 return err 145 } 146 time.Sleep(duration) 147 } 148 149 symbol, symbolColor := shared.Symbol(cs, run.Status, run.Conclusion) 150 id := cs.Cyanf("%d", run.ID) 151 152 if opts.IO.IsStdoutTTY() { 153 fmt.Fprintln(out) 154 fmt.Fprintf(out, "%s Run %s (%s) completed with '%s'\n", symbolColor(symbol), cs.Bold(run.Name), id, run.Conclusion) 155 } 156 157 if opts.ExitStatus && run.Conclusion != shared.Success { 158 return cmdutil.SilentError 159 } 160 161 return nil 162 } 163 164 func renderRun(opts WatchOptions, client *api.Client, repo ghrepo.Interface, run *shared.Run, prNumber string, annotationCache map[int][]shared.Annotation) (*shared.Run, error) { 165 out := opts.IO.Out 166 cs := opts.IO.ColorScheme() 167 168 var err error 169 170 run, err = shared.GetRun(client, repo, fmt.Sprintf("%d", run.ID)) 171 if err != nil { 172 return nil, fmt.Errorf("failed to get run: %w", err) 173 } 174 175 ago := opts.Now().Sub(run.CreatedAt) 176 177 jobs, err := shared.GetJobs(client, repo, *run) 178 if err != nil { 179 return nil, fmt.Errorf("failed to get jobs: %w", err) 180 } 181 182 var annotations []shared.Annotation 183 184 var annotationErr error 185 var as []shared.Annotation 186 for _, job := range jobs { 187 if as, ok := annotationCache[job.ID]; ok { 188 annotations = as 189 continue 190 } 191 192 as, annotationErr = shared.GetAnnotations(client, repo, job) 193 if annotationErr != nil { 194 break 195 } 196 annotations = append(annotations, as...) 197 198 if job.Status != shared.InProgress { 199 annotationCache[job.ID] = annotations 200 } 201 } 202 203 if annotationErr != nil { 204 return nil, fmt.Errorf("failed to get annotations: %w", annotationErr) 205 } 206 207 if runtime.GOOS == "windows" { 208 // Just clear whole screen; I wasn't able to get the nicer cursor movement thing working 209 fmt.Fprintf(opts.IO.Out, "\x1b[2J") 210 } else { 211 // Move cursor to 0,0 212 fmt.Fprint(opts.IO.Out, "\x1b[0;0H") 213 // Clear from cursor to bottom of screen 214 fmt.Fprint(opts.IO.Out, "\x1b[J") 215 } 216 217 fmt.Fprintln(out, cs.Boldf("Refreshing run status every %d seconds. Press Ctrl+C to quit.", opts.Interval)) 218 fmt.Fprintln(out) 219 fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, utils.FuzzyAgo(ago), prNumber)) 220 fmt.Fprintln(out) 221 222 if len(jobs) == 0 { 223 return run, nil 224 } 225 226 fmt.Fprintln(out, cs.Bold("JOBS")) 227 228 fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true)) 229 230 if len(annotations) > 0 { 231 fmt.Fprintln(out) 232 fmt.Fprintln(out, cs.Bold("ANNOTATIONS")) 233 fmt.Fprintln(out, shared.RenderAnnotations(cs, annotations)) 234 } 235 236 return run, nil 237 }