package view import ( "errors" "fmt" "net/http" "strconv" "time" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/pkg/cmd/run/download" "github.com/cli/cli/pkg/cmd/run/shared" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/utils" "github.com/spf13/cobra" ) type ViewOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) RunID string Verbose bool ExitStatus bool Prompt bool Now func() time.Time } func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { opts := &ViewOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, Now: time.Now, } cmd := &cobra.Command{ Use: "view []", Short: "View a summary of a workflow run", Args: cobra.MaximumNArgs(1), Hidden: true, Example: heredoc.Doc(` # Interactively select a run to view $ gh run view # View a specific run $ gh run view 0451 # Exit non-zero if a run failed $ gh run view 0451 -e && echo "job pending or passed" `), 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.FlagError{Err: errors.New("run ID required when not running interactively")} } else { opts.Prompt = true } if runF != nil { return runF(opts) } return runView(opts) }, } cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show job steps") // TODO should we try and expose pending via another exit code? cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if run failed") return cmd } func runView(opts *ViewOptions) 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) } runID := opts.RunID if opts.Prompt { cs := opts.IO.ColorScheme() // TODO arbitrary limit runs, err := shared.GetRuns(client, repo, 10) if err != nil { return fmt.Errorf("failed to get runs: %w", err) } runID, err = shared.PromptForRun(cs, runs) if err != nil { return err } } opts.IO.StartProgressIndicator() defer opts.IO.StopProgressIndicator() run, err := shared.GetRun(client, repo, runID) if err != nil { return fmt.Errorf("failed to get run: %w", err) } prNumber := "" number, err := shared.PullRequestForRun(client, repo, *run) if err == nil { prNumber = fmt.Sprintf(" #%d", number) } jobs, err := shared.GetJobs(client, repo, *run) if err != nil { return fmt.Errorf("failed to get jobs: %w", err) } artifacts, err := download.ListArtifacts(c, repo, strconv.Itoa(run.ID)) if err != nil { return fmt.Errorf("failed to get artifacts: %w", err) } var annotations []shared.Annotation var annotationErr error var as []shared.Annotation for _, job := range jobs { as, annotationErr = shared.GetAnnotations(client, repo, job) if annotationErr != nil { break } annotations = append(annotations, as...) } opts.IO.StopProgressIndicator() if annotationErr != nil { return fmt.Errorf("failed to get annotations: %w", annotationErr) } out := opts.IO.Out cs := opts.IO.ColorScheme() ago := opts.Now().Sub(run.CreatedAt) fmt.Fprintln(out) fmt.Fprintln(out, shared.RenderRunHeader(cs, *run, utils.FuzzyAgo(ago), prNumber)) fmt.Fprintln(out) if len(jobs) == 0 && run.Conclusion == shared.Failure { fmt.Fprintf(out, "%s %s\n", cs.FailureIcon(), cs.Bold("This run likely failed because of a workflow file issue.")) fmt.Fprintln(out) fmt.Fprintf(out, "For more information, see: %s\n", cs.Bold(run.URL)) if opts.ExitStatus { return cmdutil.SilentError } return nil } fmt.Fprintln(out, cs.Bold("JOBS")) fmt.Fprintln(out, shared.RenderJobs(cs, jobs, opts.Verbose)) if len(annotations) > 0 { fmt.Fprintln(out) fmt.Fprintln(out, cs.Bold("ANNOTATIONS")) fmt.Fprintln(out, shared.RenderAnnotations(cs, annotations)) } var validArtifacts []download.Artifact for _, a := range artifacts { if !a.Expired { validArtifacts = append(validArtifacts, a) } } if len(validArtifacts) > 0 { fmt.Fprintln(out) fmt.Fprintln(out, cs.Bold("ARTIFACTS")) for _, a := range validArtifacts { fmt.Fprintln(out, a.Name) } } fmt.Fprintln(out) fmt.Fprintln(out, "For more information about a job, try: gh job view ") fmt.Fprintf(out, cs.Gray("view this run on GitHub: %s\n"), run.URL) if opts.ExitStatus && shared.IsFailureState(run.Conclusion) { return cmdutil.SilentError } return nil }