Skip to content

Commit 211324d

Browse files
authored
Merge pull request cli#3337 from cli/workflow-view-2
Workflow view
2 parents 216cfb6 + e15189f commit 211324d

File tree

5 files changed

+767
-0
lines changed

5 files changed

+767
-0
lines changed

pkg/cmd/run/shared/shared.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ func IsFailureState(c Conclusion) bool {
148148
}
149149

150150
type RunsPayload struct {
151+
TotalCount int `json:"total_count"`
151152
WorkflowRuns []Run `json:"workflow_runs"`
152153
}
153154

pkg/cmd/workflow/view/http.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package view
2+
3+
import (
4+
"encoding/base64"
5+
"fmt"
6+
"net/url"
7+
8+
"github.com/cli/cli/api"
9+
"github.com/cli/cli/internal/ghrepo"
10+
runShared "github.com/cli/cli/pkg/cmd/run/shared"
11+
"github.com/cli/cli/pkg/cmd/workflow/shared"
12+
)
13+
14+
type workflowRuns struct {
15+
Total int
16+
Runs []runShared.Run
17+
}
18+
19+
func getWorkflowContent(client *api.Client, repo ghrepo.Interface, ref string, workflow *shared.Workflow) (string, error) {
20+
path := fmt.Sprintf("repos/%s/contents/%s", ghrepo.FullName(repo), workflow.Path)
21+
if ref != "" {
22+
q := fmt.Sprintf("?ref=%s", url.QueryEscape(ref))
23+
path = path + q
24+
}
25+
26+
type Result struct {
27+
Content string
28+
}
29+
30+
var result Result
31+
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
32+
if err != nil {
33+
return "", err
34+
}
35+
36+
decoded, err := base64.StdEncoding.DecodeString(result.Content)
37+
if err != nil {
38+
return "", fmt.Errorf("failed to decode workflow file: %w", err)
39+
}
40+
41+
return string(decoded), nil
42+
}
43+
44+
func getWorkflowRuns(client *api.Client, repo ghrepo.Interface, workflow *shared.Workflow) (workflowRuns, error) {
45+
var wr workflowRuns
46+
var result runShared.RunsPayload
47+
path := fmt.Sprintf("repos/%s/actions/workflows/%d/runs?per_page=%d&page=%d", ghrepo.FullName(repo), workflow.ID, 5, 1)
48+
49+
err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
50+
if err != nil {
51+
return wr, err
52+
}
53+
54+
wr.Total = result.TotalCount
55+
wr.Runs = append(wr.Runs, result.WorkflowRuns...)
56+
57+
return wr, nil
58+
}

pkg/cmd/workflow/view/view.go

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

0 commit comments

Comments
 (0)