github.com/cli/cli@v1.14.1-0.20210902173923-1af6a669e342/pkg/cmd/workflow/view/view.go (about)

     1  package view
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/MakeNowJust/heredoc"
    10  	"github.com/cli/cli/api"
    11  	"github.com/cli/cli/internal/ghrepo"
    12  	runShared "github.com/cli/cli/pkg/cmd/run/shared"
    13  	"github.com/cli/cli/pkg/cmd/workflow/shared"
    14  	"github.com/cli/cli/pkg/cmdutil"
    15  	"github.com/cli/cli/pkg/iostreams"
    16  	"github.com/cli/cli/pkg/markdown"
    17  	"github.com/cli/cli/utils"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  type ViewOptions struct {
    22  	HttpClient func() (*http.Client, error)
    23  	IO         *iostreams.IOStreams
    24  	BaseRepo   func() (ghrepo.Interface, error)
    25  	Browser    cmdutil.Browser
    26  
    27  	Selector string
    28  	Ref      string
    29  	Web      bool
    30  	Prompt   bool
    31  	Raw      bool
    32  	YAML     bool
    33  }
    34  
    35  func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
    36  	opts := &ViewOptions{
    37  		IO:         f.IOStreams,
    38  		HttpClient: f.HttpClient,
    39  		Browser:    f.Browser,
    40  	}
    41  
    42  	cmd := &cobra.Command{
    43  		Use:   "view [<workflow-id> | <workflow-name> | <filename>]",
    44  		Short: "View the summary of a workflow",
    45  		Args:  cobra.MaximumNArgs(1),
    46  		Example: heredoc.Doc(`
    47  		  # Interactively select a workflow to view
    48  		  $ gh workflow view
    49  
    50  		  # View a specific workflow
    51  		  $ gh workflow view 0451
    52  		`),
    53  		RunE: func(cmd *cobra.Command, args []string) error {
    54  			// support `-R, --repo` override
    55  			opts.BaseRepo = f.BaseRepo
    56  
    57  			opts.Raw = !opts.IO.IsStdoutTTY()
    58  
    59  			if len(args) > 0 {
    60  				opts.Selector = args[0]
    61  			} else if !opts.IO.CanPrompt() {
    62  				return &cmdutil.FlagError{Err: errors.New("workflow argument required when not running interactively")}
    63  			} else {
    64  				opts.Prompt = true
    65  			}
    66  
    67  			if !opts.YAML && opts.Ref != "" {
    68  				return &cmdutil.FlagError{Err: errors.New("`--yaml` required when specifying `--ref`")}
    69  			}
    70  
    71  			if runF != nil {
    72  				return runF(opts)
    73  			}
    74  			return runView(opts)
    75  		},
    76  	}
    77  
    78  	cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open workflow in the browser")
    79  	cmd.Flags().BoolVarP(&opts.YAML, "yaml", "y", false, "View the workflow yaml file")
    80  	cmd.Flags().StringVarP(&opts.Ref, "ref", "r", "", "The branch or tag name which contains the version of the workflow file you'd like to view")
    81  
    82  	return cmd
    83  }
    84  
    85  func runView(opts *ViewOptions) error {
    86  	c, err := opts.HttpClient()
    87  	if err != nil {
    88  		return fmt.Errorf("could not build http client: %w", err)
    89  	}
    90  	client := api.NewClientFromHTTP(c)
    91  
    92  	repo, err := opts.BaseRepo()
    93  	if err != nil {
    94  		return fmt.Errorf("could not determine base repo: %w", err)
    95  	}
    96  
    97  	var workflow *shared.Workflow
    98  	states := []shared.WorkflowState{shared.Active}
    99  	workflow, err = shared.ResolveWorkflow(opts.IO, client, repo, opts.Prompt, opts.Selector, states)
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	if opts.Web {
   105  		var url string
   106  		if opts.YAML {
   107  			ref := opts.Ref
   108  			if ref == "" {
   109  				opts.IO.StartProgressIndicator()
   110  				ref, err = api.RepoDefaultBranch(client, repo)
   111  				opts.IO.StopProgressIndicator()
   112  				if err != nil {
   113  					return err
   114  				}
   115  			}
   116  			url = ghrepo.GenerateRepoURL(repo, "blob/%s/%s", ref, workflow.Path)
   117  		} else {
   118  			url = ghrepo.GenerateRepoURL(repo, "actions/workflows/%s", workflow.Base())
   119  		}
   120  		if opts.IO.IsStdoutTTY() {
   121  			fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", utils.DisplayURL(url))
   122  		}
   123  		return opts.Browser.Browse(url)
   124  	}
   125  
   126  	if opts.YAML {
   127  		err = viewWorkflowContent(opts, client, workflow)
   128  	} else {
   129  		err = viewWorkflowInfo(opts, client, workflow)
   130  	}
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  func viewWorkflowContent(opts *ViewOptions, client *api.Client, workflow *shared.Workflow) error {
   139  	repo, err := opts.BaseRepo()
   140  	if err != nil {
   141  		return fmt.Errorf("could not determine base repo: %w", err)
   142  	}
   143  
   144  	opts.IO.StartProgressIndicator()
   145  	yamlBytes, err := shared.GetWorkflowContent(client, repo, *workflow, opts.Ref)
   146  	opts.IO.StopProgressIndicator()
   147  	if err != nil {
   148  		if s, ok := err.(api.HTTPError); ok && s.StatusCode == 404 {
   149  			if opts.Ref != "" {
   150  				return fmt.Errorf("could not find workflow file %s on %s, try specifying a different ref", workflow.Base(), opts.Ref)
   151  			}
   152  			return fmt.Errorf("could not find workflow file %s, try specifying a branch or tag using `--ref`", workflow.Base())
   153  		}
   154  		return fmt.Errorf("could not get workflow file content: %w", err)
   155  	}
   156  
   157  	yaml := string(yamlBytes)
   158  
   159  	theme := opts.IO.DetectTerminalTheme()
   160  	markdownStyle := markdown.GetStyle(theme)
   161  	if err := opts.IO.StartPager(); err != nil {
   162  		fmt.Fprintf(opts.IO.ErrOut, "starting pager failed: %v\n", err)
   163  	}
   164  	defer opts.IO.StopPager()
   165  
   166  	if !opts.Raw {
   167  		cs := opts.IO.ColorScheme()
   168  		out := opts.IO.Out
   169  
   170  		fileName := workflow.Base()
   171  		fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Gray(fileName))
   172  		fmt.Fprintf(out, "ID: %s", cs.Cyanf("%d", workflow.ID))
   173  
   174  		codeBlock := fmt.Sprintf("```yaml\n%s\n```", yaml)
   175  		rendered, err := markdown.RenderWithOpts(codeBlock, markdownStyle,
   176  			markdown.RenderOpts{
   177  				markdown.WithoutIndentation(),
   178  				markdown.WithoutWrap(),
   179  			})
   180  		if err != nil {
   181  			return err
   182  		}
   183  		_, err = fmt.Fprint(opts.IO.Out, rendered)
   184  		return err
   185  	}
   186  
   187  	if _, err := fmt.Fprint(opts.IO.Out, yaml); err != nil {
   188  		return err
   189  	}
   190  
   191  	if !strings.HasSuffix(yaml, "\n") {
   192  		_, err := fmt.Fprint(opts.IO.Out, "\n")
   193  		return err
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func viewWorkflowInfo(opts *ViewOptions, client *api.Client, workflow *shared.Workflow) error {
   200  	repo, err := opts.BaseRepo()
   201  	if err != nil {
   202  		return fmt.Errorf("could not determine base repo: %w", err)
   203  	}
   204  
   205  	opts.IO.StartProgressIndicator()
   206  	wr, err := getWorkflowRuns(client, repo, workflow)
   207  	opts.IO.StopProgressIndicator()
   208  	if err != nil {
   209  		return fmt.Errorf("failed to get runs: %w", err)
   210  	}
   211  
   212  	out := opts.IO.Out
   213  	cs := opts.IO.ColorScheme()
   214  	tp := utils.NewTablePrinter(opts.IO)
   215  
   216  	// Header
   217  	filename := workflow.Base()
   218  	fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Cyan(filename))
   219  	fmt.Fprintf(out, "ID: %s\n\n", cs.Cyanf("%d", workflow.ID))
   220  
   221  	// Runs
   222  	fmt.Fprintf(out, "Total runs %d\n", wr.Total)
   223  
   224  	if wr.Total != 0 {
   225  		fmt.Fprintln(out, "Recent runs")
   226  	}
   227  
   228  	for _, run := range wr.Runs {
   229  		if opts.Raw {
   230  			tp.AddField(string(run.Status), nil, nil)
   231  			tp.AddField(string(run.Conclusion), nil, nil)
   232  		} else {
   233  			symbol, symbolColor := runShared.Symbol(cs, run.Status, run.Conclusion)
   234  			tp.AddField(symbol, nil, symbolColor)
   235  		}
   236  
   237  		tp.AddField(run.CommitMsg(), nil, cs.Bold)
   238  
   239  		tp.AddField(run.Name, nil, nil)
   240  		tp.AddField(run.HeadBranch, nil, cs.Bold)
   241  		tp.AddField(string(run.Event), nil, nil)
   242  
   243  		if opts.Raw {
   244  			elapsed := run.UpdatedAt.Sub(run.CreatedAt)
   245  			if elapsed < 0 {
   246  				elapsed = 0
   247  			}
   248  			tp.AddField(elapsed.String(), nil, nil)
   249  		}
   250  
   251  		tp.AddField(fmt.Sprintf("%d", run.ID), nil, cs.Cyan)
   252  
   253  		tp.EndRow()
   254  	}
   255  
   256  	err = tp.Render()
   257  	if err != nil {
   258  		return err
   259  	}
   260  
   261  	fmt.Fprintln(out)
   262  
   263  	// Footer
   264  	if wr.Total != 0 {
   265  		fmt.Fprintf(out, "To see more runs for this workflow, try: gh run list --workflow %s\n", filename)
   266  	}
   267  	fmt.Fprintf(out, "To see the YAML for this workflow, try: gh workflow view %s --yaml\n", filename)
   268  	return nil
   269  }