Merge pull request #7847 from cli/mas-prompts

switch to prompter in workflow commands
This commit is contained in:
Nate Smith 2023-08-17 17:30:21 -05:00 committed by GitHub
commit e3a152c246
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 168 additions and 114 deletions

View file

@ -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 {

View file

@ -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) {

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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)

View file

@ -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) {

View file

@ -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
}