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

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