github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/run/watch/watch.go (about)

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