diff --git a/pkg/cmd/run/list/list.go b/pkg/cmd/run/list/list.go index af84b089e..394af5dcb 100644 --- a/pkg/cmd/run/list/list.go +++ b/pkg/cmd/run/list/list.go @@ -24,6 +24,7 @@ type ListOptions struct { IO *iostreams.IOStreams HttpClient func() (*http.Client, error) BaseRepo func() (ghrepo.Interface, error) + Prompter iprompter Exporter cmdutil.Exporter @@ -38,10 +39,15 @@ type ListOptions struct { now time.Time } +type iprompter interface { + Select(string, string, []string) (int, error) +} + func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command { opts := &ListOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, + Prompter: f.Prompter, now: time.Now(), } @@ -103,7 +109,9 @@ func listRun(opts *ListOptions) error { opts.IO.StartProgressIndicator() if opts.WorkflowSelector != "" { states := []workflowShared.WorkflowState{workflowShared.Active} - if workflow, err := workflowShared.ResolveWorkflow(opts.IO, client, baseRepo, false, opts.WorkflowSelector, states); err == nil { + if workflow, err := workflowShared.ResolveWorkflow( + opts.Prompter, opts.IO, client, baseRepo, false, opts.WorkflowSelector, + states); err == nil { filters.WorkflowID = workflow.ID filters.WorkflowName = workflow.Name } else { diff --git a/pkg/cmd/workflow/disable/disable.go b/pkg/cmd/workflow/disable/disable.go index ed3155fbd..8b2fb62d3 100644 --- a/pkg/cmd/workflow/disable/disable.go +++ b/pkg/cmd/workflow/disable/disable.go @@ -17,15 +17,21 @@ type DisableOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) + Prompter iprompter Selector string Prompt bool } +type iprompter interface { + Select(string, string, []string) (int, error) +} + func NewCmdDisable(f *cmdutil.Factory, runF func(*DisableOptions) error) *cobra.Command { opts := &DisableOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, + Prompter: f.Prompter, } cmd := &cobra.Command{ @@ -69,7 +75,7 @@ func runDisable(opts *DisableOptions) error { states := []shared.WorkflowState{shared.Active} workflow, err := shared.ResolveWorkflow( - opts.IO, client, repo, opts.Prompt, opts.Selector, states) + opts.Prompter, opts.IO, client, repo, opts.Prompt, opts.Selector, states) if err != nil { var fae shared.FilteredAllError if errors.As(err, &fae) { diff --git a/pkg/cmd/workflow/disable/disable_test.go b/pkg/cmd/workflow/disable/disable_test.go index 4bbf766e7..d8ba3fcd9 100644 --- a/pkg/cmd/workflow/disable/disable_test.go +++ b/pkg/cmd/workflow/disable/disable_test.go @@ -7,11 +7,11 @@ import ( "testing" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -91,14 +91,14 @@ func TestNewCmdDisable(t *testing.T) { func TestDisableRun(t *testing.T) { tests := []struct { - name string - opts *DisableOptions - httpStubs func(*httpmock.Registry) - askStubs func(*prompt.AskStubber) - tty bool - wantOut string - wantErrOut string - wantErr bool + name string + opts *DisableOptions + httpStubs func(*httpmock.Registry) + promptStubs func(*prompter.MockPrompter) + tty bool + wantOut string + wantErrOut string + wantErr bool }{ { name: "tty no arg", @@ -120,8 +120,10 @@ func TestDisableRun(t *testing.T) { httpmock.REST("PUT", "repos/OWNER/REPO/actions/workflows/789/disable"), httpmock.StatusStringResponse(204, "{}")) }, - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Select a workflow").AnswerWith("another workflow (another.yml)") + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow", []string{"a workflow (flow.yml)", "another workflow (another.yml)"}, func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "another workflow (another.yml)") + }) }, wantOut: "✓ Disabled another workflow\n", }, @@ -169,8 +171,10 @@ func TestDisableRun(t *testing.T) { httpmock.REST("PUT", "repos/OWNER/REPO/actions/workflows/1011/disable"), httpmock.StatusStringResponse(204, "{}")) }, - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Which workflow do you mean?").AnswerWith("another workflow (yetanother.yml)") + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Which workflow do you mean?", []string{"another workflow (another.yml)", "another workflow (yetanother.yml)"}, func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "another workflow (yetanother.yml)") + }) }, wantOut: "✓ Disabled another workflow\n", }, @@ -269,10 +273,10 @@ func TestDisableRun(t *testing.T) { } t.Run(tt.name, func(t *testing.T) { - //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock - as := prompt.NewAskStubber(t) - if tt.askStubs != nil { - tt.askStubs(as) + pm := prompter.NewMockPrompter(t) + tt.opts.Prompter = pm + if tt.promptStubs != nil { + tt.promptStubs(pm) } err := runDisable(tt.opts) diff --git a/pkg/cmd/workflow/enable/enable.go b/pkg/cmd/workflow/enable/enable.go index 06922a46f..93e8ac007 100644 --- a/pkg/cmd/workflow/enable/enable.go +++ b/pkg/cmd/workflow/enable/enable.go @@ -17,15 +17,21 @@ type EnableOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) + Prompter iprompter Selector string Prompt bool } +type iprompter interface { + Select(string, string, []string) (int, error) +} + func NewCmdEnable(f *cmdutil.Factory, runF func(*EnableOptions) error) *cobra.Command { opts := &EnableOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, + Prompter: f.Prompter, } cmd := &cobra.Command{ @@ -68,7 +74,7 @@ func runEnable(opts *EnableOptions) error { } states := []shared.WorkflowState{shared.DisabledManually, shared.DisabledInactivity} - workflow, err := shared.ResolveWorkflow( + workflow, err := shared.ResolveWorkflow(opts.Prompter, opts.IO, client, repo, opts.Prompt, opts.Selector, states) if err != nil { var fae shared.FilteredAllError diff --git a/pkg/cmd/workflow/enable/enable_test.go b/pkg/cmd/workflow/enable/enable_test.go index b626f794d..905965087 100644 --- a/pkg/cmd/workflow/enable/enable_test.go +++ b/pkg/cmd/workflow/enable/enable_test.go @@ -7,11 +7,11 @@ import ( "testing" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -91,14 +91,14 @@ func TestNewCmdEnable(t *testing.T) { func TestEnableRun(t *testing.T) { tests := []struct { - name string - opts *EnableOptions - httpStubs func(*httpmock.Registry) - askStubs func(*prompt.AskStubber) - tty bool - wantOut string - wantErrOut string - wantErr bool + name string + opts *EnableOptions + httpStubs func(*httpmock.Registry) + promptStubs func(*prompter.MockPrompter) + tty bool + wantOut string + wantErrOut string + wantErr bool }{ { name: "tty no arg", @@ -120,8 +120,10 @@ func TestEnableRun(t *testing.T) { httpmock.REST("PUT", "repos/OWNER/REPO/actions/workflows/456/enable"), httpmock.StatusStringResponse(204, "{}")) }, - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Select a workflow").AnswerWith("a disabled workflow (disabled.yml)") + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow", []string{"a disabled workflow (disabled.yml)"}, func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "a disabled workflow (disabled.yml)") + }) }, wantOut: "✓ Enabled a disabled workflow\n", }, @@ -169,8 +171,10 @@ func TestEnableRun(t *testing.T) { httpmock.REST("PUT", "repos/OWNER/REPO/actions/workflows/1213/enable"), httpmock.StatusStringResponse(204, "{}")) }, - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Which workflow do you mean?").AnswerWith("a disabled workflow (anotherDisabled.yml)") + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Which workflow do you mean?", []string{"a disabled workflow (disabled.yml)", "a disabled workflow (anotherDisabled.yml)"}, func(_, _ string, opts []string) (int, error) { + return prompter.IndexFor(opts, "a disabled workflow (anotherDisabled.yml)") + }) }, wantOut: "✓ Enabled a disabled workflow\n", }, @@ -309,10 +313,10 @@ func TestEnableRun(t *testing.T) { } t.Run(tt.name, func(t *testing.T) { - //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock - as := prompt.NewAskStubber(t) - if tt.askStubs != nil { - tt.askStubs(as) + pm := prompter.NewMockPrompter(t) + tt.opts.Prompter = pm + if tt.promptStubs != nil { + tt.promptStubs(pm) } err := runEnable(tt.opts) diff --git a/pkg/cmd/workflow/run/run.go b/pkg/cmd/workflow/run/run.go index 564b41985..009c48182 100644 --- a/pkg/cmd/workflow/run/run.go +++ b/pkg/cmd/workflow/run/run.go @@ -11,14 +11,12 @@ import ( "sort" "strings" - "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" "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/prompt" "github.com/spf13/cobra" "gopkg.in/yaml.v3" ) @@ -27,6 +25,7 @@ type RunOptions struct { HttpClient func() (*http.Client, error) IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) + Prompter iprompter Selector string Ref string @@ -39,10 +38,16 @@ type RunOptions struct { Prompt bool } +type iprompter interface { + Input(string, string) (string, error) + Select(string, string, []string) (int, error) +} + func NewCmdRun(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command { opts := &RunOptions{ IO: f.IOStreams, HttpClient: f.HttpClient, + Prompter: f.Prompter, } cmd := &cobra.Command{ @@ -198,7 +203,7 @@ func (ia *InputAnswer) WriteAnswer(name string, value interface{}) error { return fmt.Errorf("unexpected value type: %v", value) } -func collectInputs(yamlContent []byte) (map[string]string, error) { +func collectInputs(p iprompter, yamlContent []byte) (map[string]string, error) { inputs, err := findInputs(yamlContent) if err != nil { return nil, err @@ -210,32 +215,24 @@ func collectInputs(yamlContent []byte) (map[string]string, error) { return providedInputs, nil } - qs := []*survey.Question{} - for inputName, input := range inputs { - q := &survey.Question{ - Name: inputName, - Prompt: &survey.Input{ - Message: inputName, - Default: input.Default, - }, - } + for _, input := range inputs { + var answer string if input.Required { - q.Validate = survey.Required + for answer == "" { + answer, err = p.Input(input.Name+" (required)", input.Default) + if err != nil { + break + } + } + } else { + answer, err = p.Input(input.Name, input.Default) } - qs = append(qs, q) - } - sort.Slice(qs, func(i, j int) bool { - return qs[i].Name < qs[j].Name - }) + if err != nil { + return nil, err + } - inputAnswer := InputAnswer{ - providedInputs: providedInputs, - } - //nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter - err = prompt.SurveyAsk(qs, &inputAnswer) - if err != nil { - return nil, err + providedInputs[input.Name] = answer } return providedInputs, nil @@ -263,7 +260,7 @@ func runRun(opts *RunOptions) error { } states := []shared.WorkflowState{shared.Active} - workflow, err := shared.ResolveWorkflow( + workflow, err := shared.ResolveWorkflow(opts.Prompter, opts.IO, client, repo, opts.Prompt, opts.Selector, states) if err != nil { var fae shared.FilteredAllError @@ -290,7 +287,7 @@ func runRun(opts *RunOptions) error { if err != nil { return fmt.Errorf("unable to fetch workflow file content: %w", err) } - providedInputs, err = collectInputs(yamlContent) + providedInputs, err = collectInputs(opts.Prompter, yamlContent) if err != nil { return err } @@ -330,12 +327,13 @@ func runRun(opts *RunOptions) error { } type WorkflowInput struct { + Name string Required bool Default string Description string } -func findInputs(yamlContent []byte) (map[string]WorkflowInput, error) { +func findInputs(yamlContent []byte) ([]WorkflowInput, error) { var rootNode yaml.Node err := yaml.Unmarshal(yamlContent, &rootNode) if err != nil { @@ -400,16 +398,31 @@ func findInputs(yamlContent []byte) (map[string]WorkflowInput, error) { return nil, errors.New("unable to manually run a workflow without a workflow_dispatch event") } - out := map[string]WorkflowInput{} + out := []WorkflowInput{} + + m := map[string]WorkflowInput{} if inputsKeyNode == nil || inputsMapNode == nil { return out, nil } - err = inputsMapNode.Decode(&out) + err = inputsMapNode.Decode(&m) if err != nil { return nil, fmt.Errorf("could not decode workflow inputs: %w", err) } + for name, input := range m { + out = append(out, WorkflowInput{ + Name: name, + Default: input.Default, + Description: input.Description, + Required: input.Required, + }) + } + + sort.Slice(out, func(i, j int) bool { + return out[i].Name < out[j].Name + }) + return out, nil } diff --git a/pkg/cmd/workflow/run/run_test.go b/pkg/cmd/workflow/run/run_test.go index c90c9a09e..70fea8aa7 100644 --- a/pkg/cmd/workflow/run/run_test.go +++ b/pkg/cmd/workflow/run/run_test.go @@ -12,11 +12,11 @@ import ( "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" + "github.com/cli/cli/v2/internal/prompter" "github.com/cli/cli/v2/pkg/cmd/workflow/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/httpmock" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" "github.com/google/shlex" "github.com/stretchr/testify/assert" ) @@ -215,7 +215,7 @@ func Test_findInputs(t *testing.T) { YAML []byte wantErr bool errMsg string - wantOut map[string]WorkflowInput + wantOut []WorkflowInput }{ { name: "blank", @@ -244,12 +244,12 @@ func Test_findInputs(t *testing.T) { { name: "short syntax", YAML: []byte("name: workflow\non: workflow_dispatch"), - wantOut: map[string]WorkflowInput{}, + wantOut: []WorkflowInput{}, }, { name: "array of events", YAML: []byte("name: workflow\non: [pull_request, workflow_dispatch]\n"), - wantOut: map[string]WorkflowInput{}, + wantOut: []WorkflowInput{}, }, { name: "inputs", @@ -274,18 +274,22 @@ jobs: - name: echo run: | echo "echo"`), - wantOut: map[string]WorkflowInput{ - "foo": { + wantOut: []WorkflowInput{ + { + Name: "bar", + Default: "boo", + }, + { + Name: "baz", + Description: "it's baz", + }, + { + Name: "foo", Required: true, Description: "good foo", }, - "bar": { - Default: "boo", - }, - "baz": { - Description: "it's baz", - }, - "quux": { + { + Name: "quux", Required: true, Default: "cool", }, @@ -359,15 +363,15 @@ jobs: } tests := []struct { - name string - opts *RunOptions - tty bool - wantErr bool - errOut string - wantOut string - wantBody map[string]interface{} - httpStubs func(*httpmock.Registry) - askStubs func(*prompt.AskStubber) + name string + opts *RunOptions + tty bool + wantErr bool + errOut string + wantOut string + wantBody map[string]interface{} + httpStubs func(*httpmock.Registry) + promptStubs func(*prompter.MockPrompter) }{ { name: "bad JSON", @@ -577,8 +581,10 @@ jobs: httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/1/dispatches"), httpmock.StatusStringResponse(204, "cool")) }, - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Select a workflow").AnswerDefault() + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow", []string{"minimal workflow (minimal.yml)"}, func(_, _ string, opts []string) (int, error) { + return 0, nil + }) }, wantBody: map[string]interface{}{ "inputs": map[string]interface{}{}, @@ -614,10 +620,16 @@ jobs: httpmock.REST("POST", "repos/OWNER/REPO/actions/workflows/12345/dispatches"), httpmock.StatusStringResponse(204, "cool")) }, - askStubs: func(as *prompt.AskStubber) { - as.StubPrompt("Select a workflow").AnswerDefault() - as.StubPrompt("greeting").AnswerWith("hi") - as.StubPrompt("name").AnswerWith("scully") + promptStubs: func(pm *prompter.MockPrompter) { + pm.RegisterSelect("Select a workflow", []string{"a workflow (workflow.yml)"}, func(_, _ string, opts []string) (int, error) { + return 0, nil + }) + pm.RegisterInput("greeting", func(_, _ string) (string, error) { + return "hi", nil + }) + pm.RegisterInput("name (required)", func(_, _ string) (string, error) { + return "scully", nil + }) }, wantBody: map[string]interface{}{ "inputs": map[string]interface{}{ @@ -652,10 +664,10 @@ jobs: } t.Run(tt.name, func(t *testing.T) { - //nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock - as := prompt.NewAskStubber(t) - if tt.askStubs != nil { - tt.askStubs(as) + pm := prompter.NewMockPrompter(t) + tt.opts.Prompter = pm + if tt.promptStubs != nil { + tt.promptStubs(pm) } err := runRun(tt.opts) diff --git a/pkg/cmd/workflow/shared/shared.go b/pkg/cmd/workflow/shared/shared.go index cfb1ff86e..6d887f927 100644 --- a/pkg/cmd/workflow/shared/shared.go +++ b/pkg/cmd/workflow/shared/shared.go @@ -11,11 +11,9 @@ import ( "strconv" "strings" - "github.com/AlecAivazis/survey/v2" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/iostreams" - "github.com/cli/cli/v2/pkg/prompt" "github.com/cli/go-gh/v2/pkg/asciisanitizer" "golang.org/x/text/transform" ) @@ -26,6 +24,10 @@ const ( DisabledInactivity WorkflowState = "disabled_inactivity" ) +type iprompter interface { + Select(string, string, []string) (int, error) +} + type WorkflowState string type Workflow struct { @@ -90,7 +92,7 @@ type FilteredAllError struct { error } -func SelectWorkflow(workflows []Workflow, promptMsg string, states []WorkflowState) (*Workflow, error) { +func selectWorkflow(p iprompter, workflows []Workflow, promptMsg string, states []WorkflowState) (*Workflow, error) { filtered := []Workflow{} candidates := []string{} for _, workflow := range workflows { @@ -107,14 +109,7 @@ func SelectWorkflow(workflows []Workflow, promptMsg string, states []WorkflowSta return nil, FilteredAllError{errors.New("")} } - var selected int - - //nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter - err := prompt.SurveyAskOne(&survey.Select{ - Message: promptMsg, - Options: candidates, - PageSize: 15, - }, &selected) + selected, err := p.Select(promptMsg, "", candidates) if err != nil { return nil, err } @@ -182,7 +177,7 @@ func getWorkflowsByName(client *api.Client, repo ghrepo.Interface, name string, return filtered, nil } -func ResolveWorkflow(io *iostreams.IOStreams, client *api.Client, repo ghrepo.Interface, prompt bool, workflowSelector string, states []WorkflowState) (*Workflow, error) { +func ResolveWorkflow(p iprompter, io *iostreams.IOStreams, client *api.Client, repo ghrepo.Interface, prompt bool, workflowSelector string, states []WorkflowState) (*Workflow, error) { if prompt { workflows, err := GetWorkflows(client, repo, 0) if len(workflows) == 0 { @@ -198,7 +193,7 @@ func ResolveWorkflow(io *iostreams.IOStreams, client *api.Client, repo ghrepo.In return nil, fmt.Errorf("could not fetch workflows for %s: %w", ghrepo.FullName(repo), err) } - return SelectWorkflow(workflows, "Select a workflow", states) + return selectWorkflow(p, workflows, "Select a workflow", states) } workflows, err := FindWorkflow(client, repo, workflowSelector, states) @@ -222,7 +217,7 @@ func ResolveWorkflow(io *iostreams.IOStreams, client *api.Client, repo ghrepo.In return nil, errors.New(errMsg) } - return SelectWorkflow(workflows, "Which workflow do you mean?", states) + return selectWorkflow(p, workflows, "Which workflow do you mean?", states) } func GetWorkflowContent(client *api.Client, repo ghrepo.Interface, workflow Workflow, ref string) ([]byte, error) { diff --git a/pkg/cmd/workflow/view/view.go b/pkg/cmd/workflow/view/view.go index f45024828..23b148942 100644 --- a/pkg/cmd/workflow/view/view.go +++ b/pkg/cmd/workflow/view/view.go @@ -26,6 +26,7 @@ type ViewOptions struct { IO *iostreams.IOStreams BaseRepo func() (ghrepo.Interface, error) Browser browser.Browser + Prompter iprompter Selector string Ref string @@ -37,11 +38,16 @@ type ViewOptions struct { 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(), } @@ -104,7 +110,7 @@ func runView(opts *ViewOptions) error { var workflow *shared.Workflow states := []shared.WorkflowState{shared.Active} - workflow, err = shared.ResolveWorkflow(opts.IO, client, repo, opts.Prompt, opts.Selector, states) + workflow, err = shared.ResolveWorkflow(opts.Prompter, opts.IO, client, repo, opts.Prompt, opts.Selector, states) if err != nil { return err }