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 }