* [gh run watch] Support `--compact` flag * [gh run watch] Support `--compact` flag * Add changes for updated AC * Incorporate review changes * docs(run watch): fill up the line to 80 chars Signed-off-by: Babak K. Shandiz <babakks@github.com> --------- Signed-off-by: Babak K. Shandiz <babakks@github.com> Co-authored-by: Babak K. Shandiz <babakks@github.com>
280 lines
7.2 KiB
Go
280 lines
7.2 KiB
Go
package watch
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/api"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/text"
|
|
"github.com/cli/cli/v2/pkg/cmd/run/shared"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const defaultInterval int = 3
|
|
|
|
type WatchOptions struct {
|
|
IO *iostreams.IOStreams
|
|
HttpClient func() (*http.Client, error)
|
|
BaseRepo func() (ghrepo.Interface, error)
|
|
Prompter shared.Prompter
|
|
|
|
RunID string
|
|
Interval int
|
|
ExitStatus bool
|
|
Compact bool
|
|
|
|
Prompt bool
|
|
|
|
Now func() time.Time
|
|
}
|
|
|
|
func NewCmdWatch(f *cmdutil.Factory, runF func(*WatchOptions) error) *cobra.Command {
|
|
opts := &WatchOptions{
|
|
IO: f.IOStreams,
|
|
HttpClient: f.HttpClient,
|
|
Prompter: f.Prompter,
|
|
Now: time.Now,
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "watch <run-id>",
|
|
Short: "Watch a run until it completes, showing its progress",
|
|
Long: heredoc.Docf(`
|
|
Watch a run until it completes, showing its progress.
|
|
|
|
By default, all steps are displayed. The %[1]s--compact%[1]s option can be used to only
|
|
show the relevant/failed steps.
|
|
|
|
This command does not support authenticating via fine grained PATs
|
|
as it is not currently possible to create a PAT with the %[1]schecks:read%[1]s permission.
|
|
`, "`"),
|
|
Example: heredoc.Doc(`
|
|
# Watch a run until it's done
|
|
$ gh run watch
|
|
|
|
# Watch a run in compact mode
|
|
$ gh run watch --compact
|
|
|
|
# Run some other command when the run is finished
|
|
$ gh run watch && notify-send 'run is done!'
|
|
`),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// support `-R, --repo` override
|
|
opts.BaseRepo = f.BaseRepo
|
|
|
|
if len(args) > 0 {
|
|
opts.RunID = args[0]
|
|
} else if !opts.IO.CanPrompt() {
|
|
return cmdutil.FlagErrorf("run ID required when not running interactively")
|
|
} else {
|
|
opts.Prompt = true
|
|
}
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
|
|
return watchRun(opts)
|
|
},
|
|
}
|
|
cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if run fails")
|
|
cmd.Flags().BoolVar(&opts.Compact, "compact", false, "Show only relevant/failed steps")
|
|
cmd.Flags().IntVarP(&opts.Interval, "interval", "i", defaultInterval, "Refresh interval in seconds")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func watchRun(opts *WatchOptions) error {
|
|
c, err := opts.HttpClient()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create http client: %w", err)
|
|
}
|
|
client := api.NewClientFromHTTP(c)
|
|
|
|
repo, err := opts.BaseRepo()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to determine base repo: %w", err)
|
|
}
|
|
|
|
cs := opts.IO.ColorScheme()
|
|
|
|
runID := opts.RunID
|
|
var run *shared.Run
|
|
|
|
if opts.Prompt {
|
|
runs, err := shared.GetRunsWithFilter(client, repo, nil, 10, func(run shared.Run) bool {
|
|
return run.Status != shared.Completed
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get runs: %w", err)
|
|
}
|
|
if len(runs) == 0 {
|
|
return fmt.Errorf("found no in progress runs to watch")
|
|
}
|
|
if runID, err = shared.SelectRun(opts.Prompter, cs, runs); err != nil {
|
|
return err
|
|
}
|
|
// TODO silly stopgap until dust settles and SelectRun can just return a run
|
|
for _, r := range runs {
|
|
if fmt.Sprintf("%d", r.ID) == runID {
|
|
run = &r
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
run, err = shared.GetRun(client, repo, runID, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get run: %w", err)
|
|
}
|
|
}
|
|
|
|
if run.Status == shared.Completed {
|
|
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)
|
|
if opts.ExitStatus && run.Conclusion != shared.Success {
|
|
return cmdutil.SilentError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
prNumber := ""
|
|
number, err := shared.PullRequestForRun(client, repo, *run)
|
|
if err == nil {
|
|
prNumber = fmt.Sprintf(" %s#%d", ghrepo.FullName(repo), number)
|
|
}
|
|
|
|
annotationCache := map[int64][]shared.Annotation{}
|
|
|
|
duration, err := time.ParseDuration(fmt.Sprintf("%ds", opts.Interval))
|
|
if err != nil {
|
|
return fmt.Errorf("could not parse interval: %w", err)
|
|
}
|
|
|
|
out := &bytes.Buffer{}
|
|
opts.IO.StartAlternateScreenBuffer()
|
|
for run.Status != shared.Completed {
|
|
// Write to a temporary buffer to reduce total number of fetches
|
|
run, err = renderRun(out, *opts, client, repo, run, prNumber, annotationCache)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
if run.Status == shared.Completed {
|
|
break
|
|
}
|
|
|
|
// If not completed, refresh the screen buffer and write the temporary buffer to stdout
|
|
opts.IO.RefreshScreen()
|
|
|
|
fmt.Fprintln(opts.IO.Out, cs.Boldf("Refreshing run status every %d seconds. Press Ctrl+C to quit.", opts.Interval))
|
|
fmt.Fprintln(opts.IO.Out)
|
|
|
|
_, err = io.Copy(opts.IO.Out, out)
|
|
out.Reset()
|
|
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
time.Sleep(duration)
|
|
}
|
|
opts.IO.StopAlternateScreenBuffer()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write the last temporary buffer one last time
|
|
_, err = io.Copy(opts.IO.Out, out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
symbol, symbolColor := shared.Symbol(cs, run.Status, run.Conclusion)
|
|
id := cs.Cyanf("%d", run.ID)
|
|
|
|
if opts.IO.IsStdoutTTY() {
|
|
fmt.Fprintln(opts.IO.Out)
|
|
fmt.Fprintf(opts.IO.Out, "%s Run %s (%s) completed with '%s'\n", symbolColor(symbol), cs.Bold(run.WorkflowName()), id, run.Conclusion)
|
|
}
|
|
|
|
if opts.ExitStatus && run.Conclusion != shared.Success {
|
|
return cmdutil.SilentError
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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) {
|
|
cs := opts.IO.ColorScheme()
|
|
|
|
var err error
|
|
|
|
run, err = shared.GetRun(client, repo, fmt.Sprintf("%d", run.ID), 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get run: %w", err)
|
|
}
|
|
|
|
jobs, err := shared.GetJobs(client, repo, run, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get jobs: %w", err)
|
|
}
|
|
|
|
var annotations []shared.Annotation
|
|
var missingAnnotationsPermissions bool
|
|
|
|
for _, job := range jobs {
|
|
if as, ok := annotationCache[job.ID]; ok {
|
|
annotations = as
|
|
continue
|
|
}
|
|
|
|
as, err := shared.GetAnnotations(client, repo, job)
|
|
if err != nil {
|
|
if err != shared.ErrMissingAnnotationsPermissions {
|
|
return nil, fmt.Errorf("failed to get annotations: %w", err)
|
|
}
|
|
|
|
missingAnnotationsPermissions = true
|
|
break
|
|
}
|
|
|
|
annotations = append(annotations, as...)
|
|
|
|
if job.Status != shared.InProgress {
|
|
annotationCache[job.ID] = annotations
|
|
}
|
|
}
|
|
|
|
fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, text.FuzzyAgo(opts.Now(), run.StartedTime()), prNumber, 0))
|
|
fmt.Fprintln(out)
|
|
|
|
if len(jobs) == 0 {
|
|
return run, nil
|
|
}
|
|
|
|
fmt.Fprintln(out, cs.Bold("JOBS"))
|
|
if opts.Compact {
|
|
fmt.Fprintln(out, shared.RenderJobsCompact(cs, jobs))
|
|
} else {
|
|
fmt.Fprintln(out, shared.RenderJobs(cs, jobs, true))
|
|
}
|
|
|
|
if missingAnnotationsPermissions {
|
|
fmt.Fprintln(out)
|
|
fmt.Fprintln(out, cs.Bold("ANNOTATIONS"))
|
|
fmt.Fprintf(out, "requesting annotations returned 403 Forbidden as the token does not have sufficient permissions. Note that it is not currently possible to create a fine-grained PAT with the `checks:read` permission.")
|
|
} else if len(annotations) > 0 {
|
|
fmt.Fprintln(out)
|
|
fmt.Fprintln(out, cs.Bold("ANNOTATIONS"))
|
|
fmt.Fprintln(out, shared.RenderAnnotations(cs, annotations))
|
|
}
|
|
|
|
return run, nil
|
|
}
|