From cc8d55c51866a00e17b60163de16757bb312b560 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 31 Mar 2021 13:16:16 -0500 Subject: [PATCH] remove job command, update gh actions text --- pkg/cmd/actions/actions.go | 36 +--- pkg/cmd/job/job.go | 22 --- pkg/cmd/job/view/http.go | 47 ----- pkg/cmd/job/view/view.go | 242 ----------------------- pkg/cmd/job/view/view_test.go | 351 ---------------------------------- pkg/cmd/root/root.go | 2 - 6 files changed, 9 insertions(+), 691 deletions(-) delete mode 100644 pkg/cmd/job/job.go delete mode 100644 pkg/cmd/job/view/http.go delete mode 100644 pkg/cmd/job/view/view.go delete mode 100644 pkg/cmd/job/view/view_test.go diff --git a/pkg/cmd/actions/actions.go b/pkg/cmd/actions/actions.go index 0bcd5118d..f5df06fb9 100644 --- a/pkg/cmd/actions/actions.go +++ b/pkg/cmd/actions/actions.go @@ -43,34 +43,16 @@ func actionsRun(opts ActionsOptions) { %s gh run list: List recent workflow runs - gh run view: View details for a given workflow run + gh run view: View details for a workflow run or one of its jobs + gh run watch: Watch a workflow run while it executes + gh run rerun: Rerun a failed workflow run %s - gh job view: View details for a given job + gh workflow list: List all the workflow files in your repository + gh workflow enable: Enable a workflow file + gh workflow disable: Disable a workflow file + gh workflow run: Trigger a workflow_dispatch run for a workflow file `, - cs.Bold("Working with runs"), - cs.Bold("Working with jobs within runs"))) - /* - fmt.Fprint(opts.IO.Out, heredoc.Docf(` - Welcome to GitHub Actions on the command line. - - %s - gh workflow list: List workflows in the current repository - gh workflow run: Kick off a workflow run - gh workflow init: Create a new workflow - gh workflow check: Check a workflow file for correctness - - %s - gh run list: List recent workflow runs - gh run view: View details for a given workflow run - gh run watch: Watch a streaming log for a workflow run - - %s - gh job view: View details for a given job - gh job run: Run a given job within a workflow - `, - cs.Bold("Working with workflows"), - cs.Bold("Working with runs"), - cs.Bold("Working with jobs within runs"))) - */ + cs.Bold("Interacting with workflow runs"), + cs.Bold("Interacting with workflow files"))) } diff --git a/pkg/cmd/job/job.go b/pkg/cmd/job/job.go deleted file mode 100644 index d420b610e..000000000 --- a/pkg/cmd/job/job.go +++ /dev/null @@ -1,22 +0,0 @@ -package job - -import ( - viewCmd "github.com/cli/cli/pkg/cmd/job/view" - "github.com/cli/cli/pkg/cmdutil" - "github.com/spf13/cobra" -) - -func NewCmdJob(f *cmdutil.Factory) *cobra.Command { - cmd := &cobra.Command{ - Use: "job ", - Short: "Interact with the individual jobs of a workflow run", - Hidden: true, - Long: "List and view the jobs of a workflow run including full logs", - // TODO action annotation - } - cmdutil.EnableRepoOverride(cmd, f) - - cmd.AddCommand(viewCmd.NewCmdView(f, nil)) - - return cmd -} diff --git a/pkg/cmd/job/view/http.go b/pkg/cmd/job/view/http.go deleted file mode 100644 index 0541da996..000000000 --- a/pkg/cmd/job/view/http.go +++ /dev/null @@ -1,47 +0,0 @@ -package view - -import ( - "errors" - "fmt" - "io" - "net/http" - - "github.com/cli/cli/api" - "github.com/cli/cli/internal/ghinstance" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/cmd/run/shared" -) - -func jobLog(httpClient *http.Client, repo ghrepo.Interface, jobID string) (io.ReadCloser, error) { - url := fmt.Sprintf("%srepos/%s/actions/jobs/%s/logs", - ghinstance.RESTPrefix(repo.RepoHost()), ghrepo.FullName(repo), jobID) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - resp, err := httpClient.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode == 404 { - return nil, errors.New("job not found") - } else if resp.StatusCode != 200 { - return nil, api.HandleHTTPError(resp) - } - - return resp.Body, nil -} - -func getJob(client *api.Client, repo ghrepo.Interface, jobID string) (*shared.Job, error) { - path := fmt.Sprintf("repos/%s/actions/jobs/%s", ghrepo.FullName(repo), jobID) - - var result shared.Job - err := client.REST(repo.RepoHost(), "GET", path, nil, &result) - if err != nil { - return nil, err - } - - return &result, nil -} diff --git a/pkg/cmd/job/view/view.go b/pkg/cmd/job/view/view.go deleted file mode 100644 index 099cd0ccf..000000000 --- a/pkg/cmd/job/view/view.go +++ /dev/null @@ -1,242 +0,0 @@ -package view - -import ( - "errors" - "fmt" - "io" - "net/http" - "time" - - "github.com/AlecAivazis/survey/v2" - "github.com/MakeNowJust/heredoc" - "github.com/cli/cli/api" - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/cmd/run/shared" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" - "github.com/cli/cli/utils" - "github.com/spf13/cobra" -) - -type ViewOptions struct { - HttpClient func() (*http.Client, error) - IO *iostreams.IOStreams - BaseRepo func() (ghrepo.Interface, error) - - JobID string - Log bool - ExitStatus bool - - Prompt bool - - Now func() time.Time -} - -func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { - opts := &ViewOptions{ - IO: f.IOStreams, - HttpClient: f.HttpClient, - Now: time.Now, - } - cmd := &cobra.Command{ - Use: "view []", - Short: "View the summary or full logs of a workflow run's job", - Args: cobra.MaximumNArgs(1), - Hidden: true, - Example: heredoc.Doc(` - # Interactively select a run then job - $ gh job view - - # Just view the logs for a job - $ gh job view 0451 --log - - # Exit non-zero if a job failed - $ gh job view 0451 -e && echo "job pending or passed" - `), - RunE: func(cmd *cobra.Command, args []string) error { - // support `-R, --repo` override - opts.BaseRepo = f.BaseRepo - - if len(args) > 0 { - opts.JobID = args[0] - } else if !opts.IO.CanPrompt() { - return &cmdutil.FlagError{Err: errors.New("job ID required when not running interactively")} - } else { - opts.Prompt = true - } - - if runF != nil { - return runF(opts) - } - return runView(opts) - }, - } - cmd.Flags().BoolVarP(&opts.Log, "log", "l", false, "Print full logs for job") - // TODO should we try and expose pending via another exit code? - cmd.Flags().BoolVar(&opts.ExitStatus, "exit-status", false, "Exit with non-zero status if job failed") - - return cmd -} - -func runView(opts *ViewOptions) error { - httpClient, err := opts.HttpClient() - if err != nil { - return fmt.Errorf("failed to create http client: %w", err) - } - client := api.NewClientFromHTTP(httpClient) - - repo, err := opts.BaseRepo() - if err != nil { - return fmt.Errorf("failed to determine base repo: %w", err) - } - - out := opts.IO.Out - cs := opts.IO.ColorScheme() - - jobID := opts.JobID - if opts.Prompt { - // TODO arbitrary limit - runs, err := shared.GetRuns(client, repo, 10) - if err != nil { - return fmt.Errorf("failed to get runs: %w", err) - } - runID, err := shared.PromptForRun(cs, runs) - if err != nil { - return err - } - // TODO I'd love to overwrite the result of the prompt since it adds visual noise but I'm not sure - // the cleanest way to do that. - fmt.Fprintln(out) - - opts.IO.StartProgressIndicator() - defer opts.IO.StopProgressIndicator() - - run, err := shared.GetRun(client, repo, runID) - if err != nil { - return fmt.Errorf("failed to get run: %w", err) - } - - opts.IO.StopProgressIndicator() - jobID, err = promptForJob(*opts, client, repo, *run) - if err != nil { - return err - } - - fmt.Fprintln(out) - } - - opts.IO.StartProgressIndicator() - job, err := getJob(client, repo, jobID) - if err != nil { - return fmt.Errorf("failed to get job: %w", err) - } - - if opts.Log { - r, err := jobLog(httpClient, repo, jobID) - if err != nil { - return err - } - - opts.IO.StopProgressIndicator() - - err = opts.IO.StartPager() - if err != nil { - return err - } - defer opts.IO.StopPager() - - if _, err := io.Copy(opts.IO.Out, r); err != nil { - return fmt.Errorf("failed to read log: %w", err) - } - - if opts.ExitStatus && shared.IsFailureState(job.Conclusion) { - return cmdutil.SilentError - } - - return nil - } - - annotations, err := shared.GetAnnotations(client, repo, *job) - opts.IO.StopProgressIndicator() - if err != nil { - return fmt.Errorf("failed to get annotations: %w", err) - } - - elapsed := job.CompletedAt.Sub(job.StartedAt) - elapsedStr := fmt.Sprintf(" in %s", elapsed) - if elapsed < 0 { - elapsedStr = "" - } - - symbol, symColor := shared.Symbol(cs, job.Status, job.Conclusion) - - fmt.Fprintf(out, "%s (ID %s)\n", cs.Bold(job.Name), cs.Cyanf("%d", job.ID)) - fmt.Fprintf(out, "%s %s ago%s\n", - symColor(symbol), - utils.FuzzyAgoAbbr(opts.Now(), job.StartedAt), - elapsedStr) - - fmt.Fprintln(out) - - for _, step := range job.Steps { - stepSym, stepSymColor := shared.Symbol(cs, step.Status, step.Conclusion) - fmt.Fprintf(out, "%s %s\n", - stepSymColor(stepSym), - step.Name) - } - - if len(annotations) > 0 { - fmt.Fprintln(out) - fmt.Fprintln(out, cs.Bold("ANNOTATIONS")) - - for _, a := range annotations { - fmt.Fprintf(out, "%s %s\n", shared.AnnotationSymbol(cs, a), a.Message) - fmt.Fprintln(out, cs.Grayf("%s#%d\n", a.Path, a.StartLine)) - } - } - - fmt.Fprintln(out) - fmt.Fprintf(out, "To see the full logs for this job, try: gh job view %s --log\n", jobID) - fmt.Fprintf(out, cs.Gray("View this job on GitHub: %s\n"), job.URL) - - if opts.ExitStatus && shared.IsFailureState(job.Conclusion) { - return cmdutil.SilentError - } - - return nil -} - -func promptForJob(opts ViewOptions, client *api.Client, repo ghrepo.Interface, run shared.Run) (string, error) { - cs := opts.IO.ColorScheme() - jobs, err := shared.GetJobs(client, repo, run) - if err != nil { - return "", err - } - - if len(jobs) == 1 { - return fmt.Sprintf("%d", jobs[0].ID), nil - } - - var selected int - - candidates := []string{} - - for _, job := range jobs { - symbol, symColor := shared.Symbol(cs, job.Status, job.Conclusion) - candidates = append(candidates, fmt.Sprintf("%s %s", symColor(symbol), job.Name)) - } - - // TODO consider custom filter so it's fuzzier. right now matches start anywhere in string but - // become contiguous - err = prompt.SurveyAskOne(&survey.Select{ - Message: "Select a job to view", - Options: candidates, - PageSize: 10, - }, &selected) - if err != nil { - return "", err - } - - return fmt.Sprintf("%d", jobs[selected].ID), nil -} diff --git a/pkg/cmd/job/view/view_test.go b/pkg/cmd/job/view/view_test.go deleted file mode 100644 index 5d134320d..000000000 --- a/pkg/cmd/job/view/view_test.go +++ /dev/null @@ -1,351 +0,0 @@ -package view - -import ( - "bytes" - "io/ioutil" - "net/http" - "testing" - "time" - - "github.com/cli/cli/internal/ghrepo" - "github.com/cli/cli/pkg/cmd/run/shared" - "github.com/cli/cli/pkg/cmdutil" - "github.com/cli/cli/pkg/httpmock" - "github.com/cli/cli/pkg/iostreams" - "github.com/cli/cli/pkg/prompt" - "github.com/google/shlex" - "github.com/stretchr/testify/assert" -) - -func TestNewCmdView(t *testing.T) { - tests := []struct { - name string - cli string - wants ViewOptions - wantsErr bool - tty bool - }{ - { - name: "blank tty", - tty: true, - wants: ViewOptions{ - Prompt: true, - }, - }, - { - name: "blank nontty", - wantsErr: true, - }, - { - name: "nontty jobID", - cli: "1234", - wants: ViewOptions{ - JobID: "1234", - }, - }, - { - name: "log tty", - tty: true, - cli: "--log", - wants: ViewOptions{ - Prompt: true, - Log: true, - }, - }, - { - name: "log nontty", - cli: "--log 1234", - wants: ViewOptions{ - JobID: "1234", - Log: true, - }, - }, - { - name: "exit status", - cli: "--exit-status 1234", - wants: ViewOptions{ - JobID: "1234", - ExitStatus: true, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - io, _, _, _ := iostreams.Test() - io.SetStdinTTY(tt.tty) - io.SetStdoutTTY(tt.tty) - - f := &cmdutil.Factory{ - IOStreams: io, - } - - argv, err := shlex.Split(tt.cli) - assert.NoError(t, err) - - var gotOpts *ViewOptions - cmd := NewCmdView(f, func(opts *ViewOptions) error { - gotOpts = opts - return nil - }) - cmd.SetArgs(argv) - cmd.SetIn(&bytes.Buffer{}) - cmd.SetOut(ioutil.Discard) - cmd.SetErr(ioutil.Discard) - - _, err = cmd.ExecuteC() - if tt.wantsErr { - assert.Error(t, err) - return - } - - assert.NoError(t, err) - - assert.Equal(t, tt.wants.JobID, gotOpts.JobID) - assert.Equal(t, tt.wants.Log, gotOpts.Log) - assert.Equal(t, tt.wants.Prompt, gotOpts.Prompt) - assert.Equal(t, tt.wants.ExitStatus, gotOpts.ExitStatus) - }) - } -} - -func TestRunView(t *testing.T) { - tests := []struct { - name string - opts *ViewOptions - httpStubs func(*httpmock.Registry) - askStubs func(*prompt.AskStubber) - tty bool - wantErr bool - wantOut string - }{ - { - name: "exit status respected with --log", - opts: &ViewOptions{ - JobID: "20", - ExitStatus: true, - Log: true, - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), - httpmock.JSONResponse(shared.FailedJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20/logs"), - httpmock.StringResponse("it's a log\nfor this job\nbeautiful log\n")) - }, - wantErr: true, - wantOut: "it's a log\nfor this job\nbeautiful log\n", - }, - { - name: "exit status respected", - opts: &ViewOptions{ - JobID: "20", - ExitStatus: true, - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), - httpmock.JSONResponse(shared.FailedJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), - httpmock.JSONResponse(shared.FailedJobAnnotations)) - }, - wantErr: true, - wantOut: "sad job (ID 20)\nX 59m ago in 4m34s\n\n✓ barf the quux\nX quux the barf\n\nANNOTATIONS\nX the job is sad\nblaze.py#420\n\n\nTo see the full logs for this job, try: gh job view 20 --log\nView this job on GitHub: jobs/20\n", - }, - { - name: "interactive flow, multi-job", - tty: true, - opts: &ViewOptions{ - Prompt: true, - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), - httpmock.JSONResponse(shared.RunsPayload{ - WorkflowRuns: shared.TestRuns, - })) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), - httpmock.JSONResponse(shared.SuccessfulRun)) - reg.Register( - httpmock.REST("GET", "runs/3/jobs"), - httpmock.JSONResponse(shared.JobsPayload{ - Jobs: []shared.Job{ - shared.SuccessfulJob, - shared.FailedJob, - }, - })) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), - httpmock.JSONResponse(shared.SuccessfulJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), - httpmock.JSONResponse([]shared.Annotation{})) - }, - askStubs: func(as *prompt.AskStubber) { - as.StubOne(2) - as.StubOne(0) - }, - wantOut: "\n\ncool job (ID 10)\n✓ 59m ago in 4m34s\n\n✓ fob the barz\n✓ barz the fob\n\nTo see the full logs for this job, try: gh job view 10 --log\nView this job on GitHub: jobs/10\n", - }, - { - name: "interactive, run has only one job", - tty: true, - opts: &ViewOptions{ - Prompt: true, - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), - httpmock.JSONResponse(shared.RunsPayload{ - WorkflowRuns: shared.TestRuns, - })) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), - httpmock.JSONResponse(shared.SuccessfulRun)) - reg.Register( - httpmock.REST("GET", "runs/3/jobs"), - httpmock.JSONResponse(shared.JobsPayload{ - Jobs: []shared.Job{ - shared.SuccessfulJob, - }, - })) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), - httpmock.JSONResponse(shared.SuccessfulJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), - httpmock.JSONResponse([]shared.Annotation{})) - }, - askStubs: func(as *prompt.AskStubber) { - as.StubOne(2) - }, - wantOut: "\n\ncool job (ID 10)\n✓ 59m ago in 4m34s\n\n✓ fob the barz\n✓ barz the fob\n\nTo see the full logs for this job, try: gh job view 10 --log\nView this job on GitHub: jobs/10\n", - }, - { - name: "interactive with log", - tty: true, - opts: &ViewOptions{ - Prompt: true, - Log: true, - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), - httpmock.JSONResponse(shared.RunsPayload{ - WorkflowRuns: shared.TestRuns, - })) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), - httpmock.JSONResponse(shared.SuccessfulRun)) - reg.Register( - httpmock.REST("GET", "runs/3/jobs"), - httpmock.JSONResponse(shared.JobsPayload{ - Jobs: []shared.Job{ - shared.SuccessfulJob, - }, - })) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), - httpmock.JSONResponse(shared.SuccessfulJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10/logs"), - httpmock.StringResponse("it's a log\nfor this job\nbeautiful log\n")) - }, - askStubs: func(as *prompt.AskStubber) { - as.StubOne(2) - }, - wantOut: "\n\nit's a log\nfor this job\nbeautiful log\n", - }, - { - name: "noninteractive with log", - opts: &ViewOptions{ - JobID: "10", - Prompt: false, - Log: true, - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), - httpmock.JSONResponse(shared.SuccessfulJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10/logs"), - httpmock.StringResponse("it's a log\nfor this job\nbeautiful log\n")) - }, - wantOut: "it's a log\nfor this job\nbeautiful log\n", - }, - { - name: "noninteractive", - opts: &ViewOptions{ - JobID: "10", - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), - httpmock.JSONResponse(shared.SuccessfulJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), - httpmock.JSONResponse([]shared.Annotation{})) - }, - wantOut: "cool job (ID 10)\n✓ 59m ago in 4m34s\n\n✓ fob the barz\n✓ barz the fob\n\nTo see the full logs for this job, try: gh job view 10 --log\nView this job on GitHub: jobs/10\n", - }, - { - name: "shows annotations for failed job", - opts: &ViewOptions{ - JobID: "20", - }, - httpStubs: func(reg *httpmock.Registry) { - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), - httpmock.JSONResponse(shared.FailedJob)) - reg.Register( - httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), - httpmock.JSONResponse(shared.FailedJobAnnotations)) - }, - wantOut: "sad job (ID 20)\nX 59m ago in 4m34s\n\n✓ barf the quux\nX quux the barf\n\nANNOTATIONS\nX the job is sad\nblaze.py#420\n\n\nTo see the full logs for this job, try: gh job view 20 --log\nView this job on GitHub: jobs/20\n", - }, - } - - for _, tt := range tests { - reg := &httpmock.Registry{} - tt.httpStubs(reg) - tt.opts.HttpClient = func() (*http.Client, error) { - return &http.Client{Transport: reg}, nil - } - - tt.opts.Now = func() time.Time { - notnow, _ := time.Parse("2006-01-02 15:04:05", "2021-02-23 05:50:00") - return notnow - } - - io, _, stdout, _ := iostreams.Test() - io.SetStdoutTTY(tt.tty) - tt.opts.IO = io - tt.opts.BaseRepo = func() (ghrepo.Interface, error) { - return ghrepo.FromFullName("OWNER/REPO") - } - - as, teardown := prompt.InitAskStubber() - defer teardown() - if tt.askStubs != nil { - tt.askStubs(as) - } - t.Run(tt.name, func(t *testing.T) { - err := runView(tt.opts) - if tt.wantErr { - assert.Error(t, err) - if !tt.opts.ExitStatus { - return - } - } - if !tt.opts.ExitStatus { - assert.NoError(t, err) - } - assert.Equal(t, tt.wantOut, stdout.String()) - reg.Verify(t) - }) - } - -} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 038fa57d7..180a5f8fa 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -16,7 +16,6 @@ import ( "github.com/cli/cli/pkg/cmd/factory" gistCmd "github.com/cli/cli/pkg/cmd/gist" issueCmd "github.com/cli/cli/pkg/cmd/issue" - jobCmd "github.com/cli/cli/pkg/cmd/job" prCmd "github.com/cli/cli/pkg/cmd/pr" releaseCmd "github.com/cli/cli/pkg/cmd/release" repoCmd "github.com/cli/cli/pkg/cmd/repo" @@ -85,7 +84,6 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(actionsCmd.NewCmdActions(f)) cmd.AddCommand(runCmd.NewCmdRun(f)) - cmd.AddCommand(jobCmd.NewCmdJob(f)) cmd.AddCommand(workflowCmd.NewCmdWorkflow(f)) // the `api` command should not inherit any extra HTTP headers