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 }