279 lines
7.2 KiB
Go
279 lines
7.2 KiB
Go
package view
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/MakeNowJust/heredoc"
|
|
"github.com/cli/cli/v2/api"
|
|
"github.com/cli/cli/v2/internal/browser"
|
|
"github.com/cli/cli/v2/internal/ghrepo"
|
|
"github.com/cli/cli/v2/internal/tableprinter"
|
|
"github.com/cli/cli/v2/internal/text"
|
|
runShared "github.com/cli/cli/v2/pkg/cmd/run/shared"
|
|
"github.com/cli/cli/v2/pkg/cmd/workflow/shared"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/cli/cli/v2/pkg/markdown"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
type ViewOptions struct {
|
|
HttpClient func() (*http.Client, error)
|
|
IO *iostreams.IOStreams
|
|
BaseRepo func() (ghrepo.Interface, error)
|
|
Browser browser.Browser
|
|
Prompter iprompter
|
|
|
|
Selector string
|
|
Ref string
|
|
Web bool
|
|
Prompt bool
|
|
Raw bool
|
|
YAML bool
|
|
|
|
now time.Time
|
|
}
|
|
|
|
type iprompter interface {
|
|
Select(string, string, []string) (int, error)
|
|
}
|
|
|
|
func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command {
|
|
opts := &ViewOptions{
|
|
IO: f.IOStreams,
|
|
HttpClient: f.HttpClient,
|
|
Browser: f.Browser,
|
|
Prompter: f.Prompter,
|
|
now: time.Now(),
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "view [<workflow-id> | <workflow-name> | <filename>]",
|
|
Short: "View the summary of a workflow",
|
|
Args: cobra.MaximumNArgs(1),
|
|
Example: heredoc.Doc(`
|
|
# Interactively select a workflow to view
|
|
$ gh workflow view
|
|
|
|
# View a specific workflow
|
|
$ gh workflow view 0451
|
|
`),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// support `-R, --repo` override
|
|
opts.BaseRepo = f.BaseRepo
|
|
|
|
opts.Raw = !opts.IO.IsStdoutTTY()
|
|
|
|
if len(args) > 0 {
|
|
opts.Selector = args[0]
|
|
} else if !opts.IO.CanPrompt() {
|
|
return cmdutil.FlagErrorf("workflow argument required when not running interactively")
|
|
} else {
|
|
opts.Prompt = true
|
|
}
|
|
|
|
if !opts.YAML && opts.Ref != "" {
|
|
return cmdutil.FlagErrorf("`--yaml` required when specifying `--ref`")
|
|
}
|
|
|
|
if runF != nil {
|
|
return runF(opts)
|
|
}
|
|
return runView(opts)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open workflow in the browser")
|
|
cmd.Flags().BoolVarP(&opts.YAML, "yaml", "y", false, "View the workflow yaml file")
|
|
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")
|
|
|
|
_ = cmdutil.RegisterBranchCompletionFlags(f.GitClient, cmd, "ref")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func runView(opts *ViewOptions) error {
|
|
c, err := opts.HttpClient()
|
|
if err != nil {
|
|
return fmt.Errorf("could not build http client: %w", err)
|
|
}
|
|
client := api.NewClientFromHTTP(c)
|
|
|
|
repo, err := opts.BaseRepo()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var workflow *shared.Workflow
|
|
states := []shared.WorkflowState{shared.Active}
|
|
workflow, err = shared.ResolveWorkflow(opts.Prompter, opts.IO, client, repo, opts.Prompt, opts.Selector, states)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Web {
|
|
var address string
|
|
if opts.YAML {
|
|
ref := opts.Ref
|
|
if ref == "" {
|
|
opts.IO.StartProgressIndicator()
|
|
ref, err = api.RepoDefaultBranch(client, repo)
|
|
opts.IO.StopProgressIndicator()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
address = ghrepo.GenerateRepoURL(repo, "blob/%s/%s", url.QueryEscape(ref), url.QueryEscape(workflow.Path))
|
|
} else {
|
|
address = ghrepo.GenerateRepoURL(repo, "actions/workflows/%s", url.QueryEscape(workflow.Base()))
|
|
}
|
|
if opts.IO.IsStdoutTTY() {
|
|
fmt.Fprintf(opts.IO.Out, "Opening %s in your browser.\n", text.DisplayURL(address))
|
|
}
|
|
return opts.Browser.Browse(address)
|
|
}
|
|
|
|
opts.IO.DetectTerminalTheme()
|
|
if err := opts.IO.StartPager(); err == nil {
|
|
defer opts.IO.StopPager()
|
|
} else {
|
|
fmt.Fprintf(opts.IO.ErrOut, "failed to start pager: %v\n", err)
|
|
}
|
|
|
|
if opts.YAML {
|
|
err = viewWorkflowContent(opts, client, repo, workflow, opts.Ref)
|
|
} else {
|
|
err = viewWorkflowInfo(opts, client, repo, workflow)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func viewWorkflowContent(opts *ViewOptions, client *api.Client, repo ghrepo.Interface, workflow *shared.Workflow, ref string) error {
|
|
yamlBytes, err := shared.GetWorkflowContent(client, repo, *workflow, ref)
|
|
if err != nil {
|
|
if s, ok := err.(api.HTTPError); ok && s.StatusCode == 404 {
|
|
if ref != "" {
|
|
return fmt.Errorf("could not find workflow file %s on %s, try specifying a different ref", workflow.Base(), ref)
|
|
}
|
|
return fmt.Errorf("could not find workflow file %s, try specifying a branch or tag using `--ref`", workflow.Base())
|
|
}
|
|
return fmt.Errorf("could not get workflow file content: %w", err)
|
|
}
|
|
|
|
yaml := string(yamlBytes)
|
|
|
|
if !opts.Raw {
|
|
cs := opts.IO.ColorScheme()
|
|
out := opts.IO.Out
|
|
|
|
fileName := workflow.Base()
|
|
fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Gray(fileName))
|
|
fmt.Fprintf(out, "ID: %s", cs.Cyanf("%d", workflow.ID))
|
|
|
|
codeBlock := fmt.Sprintf("```yaml\n%s\n```", yaml)
|
|
rendered, err := markdown.Render(codeBlock,
|
|
markdown.WithTheme(opts.IO.TerminalTheme()),
|
|
markdown.WithoutIndentation(),
|
|
markdown.WithWrap(0))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprint(opts.IO.Out, rendered)
|
|
return err
|
|
}
|
|
|
|
if _, err := fmt.Fprint(opts.IO.Out, yaml); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !strings.HasSuffix(yaml, "\n") {
|
|
_, err := fmt.Fprint(opts.IO.Out, "\n")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func viewWorkflowInfo(opts *ViewOptions, client *api.Client, repo ghrepo.Interface, workflow *shared.Workflow) error {
|
|
wr, err := runShared.GetRuns(client, repo, &runShared.FilterOptions{
|
|
WorkflowID: workflow.ID,
|
|
WorkflowName: workflow.Name,
|
|
}, 5)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get runs: %w", err)
|
|
}
|
|
|
|
out := opts.IO.Out
|
|
cs := opts.IO.ColorScheme()
|
|
|
|
// Header
|
|
filename := workflow.Base()
|
|
fmt.Fprintf(out, "%s - %s\n", cs.Bold(workflow.Name), cs.Cyan(filename))
|
|
fmt.Fprintf(out, "ID: %s\n\n", cs.Cyanf("%d", workflow.ID))
|
|
|
|
// Runs
|
|
fmt.Fprintf(out, "Total runs %d\n", wr.TotalCount)
|
|
|
|
if wr.TotalCount != 0 {
|
|
fmt.Fprintln(out, "Recent runs")
|
|
}
|
|
|
|
headers := make([]string, 0, 8)
|
|
if opts.Raw {
|
|
headers = append(headers, "STATUS", "CONCLUSION")
|
|
} else {
|
|
headers = append(headers, "")
|
|
}
|
|
headers = append(headers, "TITLE", "WORKFLOW", "BRANCH", "EVENT")
|
|
if opts.Raw {
|
|
headers = append(headers, "ELAPSED")
|
|
}
|
|
headers = append(headers, "ID")
|
|
|
|
tp := tableprinter.New(opts.IO, tableprinter.WithHeader(headers...))
|
|
for _, run := range wr.WorkflowRuns {
|
|
if opts.Raw {
|
|
tp.AddField(string(run.Status))
|
|
tp.AddField(string(run.Conclusion))
|
|
} else {
|
|
symbol, symbolColor := runShared.Symbol(cs, run.Status, run.Conclusion)
|
|
tp.AddField(symbol, tableprinter.WithColor(symbolColor))
|
|
}
|
|
|
|
tp.AddField(run.Title(), tableprinter.WithColor(cs.Bold))
|
|
|
|
tp.AddField(run.WorkflowName())
|
|
tp.AddField(run.HeadBranch, tableprinter.WithColor(cs.Bold))
|
|
tp.AddField(string(run.Event))
|
|
|
|
if opts.Raw {
|
|
tp.AddField(run.Duration(opts.now).String())
|
|
}
|
|
|
|
tp.AddField(fmt.Sprintf("%d", run.ID), tableprinter.WithColor(cs.Cyan))
|
|
|
|
tp.EndRow()
|
|
}
|
|
|
|
err = tp.Render()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintln(out)
|
|
|
|
// Footer
|
|
if wr.TotalCount != 0 {
|
|
fmt.Fprintf(out, "To see more runs for this workflow, try: gh run list --workflow %s\n", filename)
|
|
}
|
|
fmt.Fprintf(out, "To see the YAML for this workflow, try: gh workflow view %s --yaml\n", filename)
|
|
return nil
|
|
}
|