Repository: cli/cli
Pull request #7846 (2023-08-16 18:47:52)

codespace: Handle HTTP request retry interruption
<!--
  Thank you for contributing to GitHub CLI!
  To reference an open issue, please write this in your description: `Fixes #NUMBER`
-->

ref: https://github.com/cli/cli/issues/7819

## About

Fix the problem that the process of changing the error message depending on the cause of the HTTP request failure in `ConnectToLiveshare` did not work well.

## Problem

The current `ConnectToLiveshare` implementation always shows a timeout error message even if the GitHub API returns a 5XX error. This confuses users.

## Changes

The process of changing the error message depending on the cause of the HTTP request failure in `ConnectToLiveshare` was  implemented by determining if the error was wrapped with `backoff.PermanentError`. However, `backoff.Retry` unwraps it and returns the unwrapped error. Therefore, it cannot be determined by this method.
https://github.com/cenkalti/backoff/blob/a04a6fe64ffb0e3fd0816460529d300be5f252df/retry.go#L93-L96

In order to be able to determine it, define a new error type `TimeoutError` that represents a timeout, and use this type to determine the cause of the HTTP request failure.

## Behavior

With this change, the error message when a request in `ConnectToCodespace` function fails with a GitHub API's 5XX error will change as follows:
  - before: `failed to connect to Live Share: timed out while waiting for the codespace to start`
  - after: `failed to connect to Live Share: error getting codespace: error making request: retry`


Patch:
diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go
index c55b92400..d6b21c304 100644
--- a/internal/codespaces/api/api.go
+++ b/internal/codespaces/api/api.go
@@ -1159,6 +1159,6 @@ func (a *API) withRetry(f func() (*http.Response, error)) (*http.Response, error
 		if resp.StatusCode < 500 {
 			return resp, nil
 		}
-		return nil, errors.New("retry")
+		return nil, fmt.Errorf("received response with status code %d", resp.StatusCode)
 	}, backoff.WithMaxRetries(bo, 3))
 }
diff --git a/internal/codespaces/codespaces.go b/internal/codespaces/codespaces.go
index 9db12f01e..8834f0e6c 100644
--- a/internal/codespaces/codespaces.go
+++ b/internal/codespaces/codespaces.go
@@ -35,6 +35,14 @@ type logger interface {
 	Printf(f string, v ...interface{})
 }
 
+type TimeoutError struct {
+	message string
+}
+
+func (e *TimeoutError) Error() string {
+	return e.message
+}
+
 // ConnectToLiveshare waits for a Codespace to become running,
 // and connects to it using a Live Share session.
 func ConnectToLiveshare(ctx context.Context, progress progressIndicator, sessionLogger logger, apiClient apiClient, codespace *api.Codespace) (*liveshare.Session, error) {
@@ -63,15 +71,15 @@ func ConnectToLiveshare(ctx context.Context, progress progressIndicator, session
 				return nil
 			}
 
-			return errors.New("codespace not ready yet")
+			return &TimeoutError{message: "codespace not ready yet"}
 		}, backoff.WithContext(expBackoff, ctx))
 		if err != nil {
-			var permErr *backoff.PermanentError
-			if errors.As(err, &permErr) {
-				return nil, err
+			var timeoutErr *TimeoutError
+			if errors.As(err, &timeoutErr) {
+				return nil, errors.New("timed out while waiting for the codespace to start")
 			}
 
-			return nil, errors.New("timed out while waiting for the codespace to start")
+			return nil, err
 		}
 	}
 
diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go
index a4db9d3a1..577f08728 100644
--- a/pkg/cmd/issue/create/create.go
+++ b/pkg/cmd/issue/create/create.go
@@ -268,7 +268,7 @@ func createRun(opts *CreateOptions) (err error) {
 				Repo:      baseRepo,
 				State:     &tb,
 			}
-			err = prShared.MetadataSurvey(opts.IO, baseRepo, fetcher, &tb)
+			err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb)
 			if err != nil {
 				return
 			}
diff --git a/pkg/cmd/issue/delete/delete.go b/pkg/cmd/issue/delete/delete.go
index f0c68de42..cee367cb8 100644
--- a/pkg/cmd/issue/delete/delete.go
+++ b/pkg/cmd/issue/delete/delete.go
@@ -3,16 +3,13 @@ package delete
 import (
 	"fmt"
 	"net/http"
-	"strconv"
 
-	"github.com/AlecAivazis/survey/v2"
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/cmd/issue/shared"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
-	"github.com/cli/cli/v2/pkg/prompt"
 	"github.com/shurcooL/githubv4"
 	"github.com/spf13/cobra"
 )
@@ -22,16 +19,22 @@ type DeleteOptions struct {
 	Config     func() (config.Config, error)
 	IO         *iostreams.IOStreams
 	BaseRepo   func() (ghrepo.Interface, error)
+	Prompter   iprompter
 
 	SelectorArg string
 	Confirmed   bool
 }
 
+type iprompter interface {
+	ConfirmDeletion(string) error
+}
+
 func NewCmdDelete(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Command {
 	opts := &DeleteOptions{
 		IO:         f.IOStreams,
 		HttpClient: f.HttpClient,
 		Config:     f.Config,
+		Prompter:   f.Prompter,
 	}
 
 	cmd := &cobra.Command{
@@ -79,22 +82,12 @@ func deleteRun(opts *DeleteOptions) error {
 	// When executed in an interactive shell, require confirmation, unless
 	// already provided. Otherwise skip confirmation.
 	if opts.IO.CanPrompt() && !opts.Confirmed {
-		answer := ""
-		//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-		err = prompt.SurveyAskOne(
-			&survey.Input{
-				Message: fmt.Sprintf("You're going to delete issue #%d. This action cannot be reversed. To confirm, type the issue number:", issue.Number),
-			},
-			&answer,
-		)
+		cs := opts.IO.ColorScheme()
+		fmt.Printf("%s Deleted issues cannot be recovered.\n", cs.WarningIcon())
+		err := opts.Prompter.ConfirmDeletion(fmt.Sprintf("%d", issue.Number))
 		if err != nil {
 			return err
 		}
-		answerInt, err := strconv.Atoi(answer)
-		if err != nil || answerInt != issue.Number {
-			fmt.Fprintf(opts.IO.Out, "Issue #%d was not deleted.\n", issue.Number)
-			return nil
-		}
 	}
 
 	if err := apiDelete(httpClient, baseRepo, issue.ID); err != nil {
diff --git a/pkg/cmd/issue/edit/edit.go b/pkg/cmd/issue/edit/edit.go
index 2fe4c62b7..53400fb63 100644
--- a/pkg/cmd/issue/edit/edit.go
+++ b/pkg/cmd/issue/edit/edit.go
@@ -21,10 +21,11 @@ type EditOptions struct {
 	HttpClient func() (*http.Client, error)
 	IO         *iostreams.IOStreams
 	BaseRepo   func() (ghrepo.Interface, error)
+	Prompter   prShared.EditPrompter
 
 	DetermineEditor    func() (string, error)
-	FieldsToEditSurvey func(*prShared.Editable) error
-	EditFieldsSurvey   func(*prShared.Editable, string) error
+	FieldsToEditSurvey func(prShared.EditPrompter, *prShared.Editable) error
+	EditFieldsSurvey   func(prShared.EditPrompter, *prShared.Editable, string) error
 	FetchOptions       func(*api.Client, ghrepo.Interface, *prShared.Editable) error
 
 	SelectorArgs []string
@@ -41,6 +42,7 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
 		FieldsToEditSurvey: prShared.FieldsToEditSurvey,
 		EditFieldsSurvey:   prShared.EditFieldsSurvey,
 		FetchOptions:       prShared.FetchOptions,
+		Prompter:           f.Prompter,
 	}
 
 	var bodyFile string
@@ -152,7 +154,7 @@ func editRun(opts *EditOptions) error {
 	// Prompt the user which fields they'd like to edit.
 	editable := opts.Editable
 	if opts.Interactive {
-		err = opts.FieldsToEditSurvey(&editable)
+		err = opts.FieldsToEditSurvey(opts.Prompter, &editable)
 		if err != nil {
 			return err
 		}
@@ -222,7 +224,7 @@ func editRun(opts *EditOptions) error {
 			if err != nil {
 				return err
 			}
-			err = opts.EditFieldsSurvey(&editable, editorCommand)
+			err = opts.EditFieldsSurvey(opts.Prompter, &editable, editorCommand)
 			if err != nil {
 				return err
 			}
diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go
index 6809528b3..7321d4f38 100644
--- a/pkg/cmd/pr/create/create.go
+++ b/pkg/cmd/pr/create/create.go
@@ -361,7 +361,7 @@ func createRun(opts *CreateOptions) (err error) {
 			Repo:      ctx.BaseRepo,
 			State:     state,
 		}
-		err = shared.MetadataSurvey(opts.IO, ctx.BaseRepo, fetcher, state)
+		err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.BaseRepo, fetcher, state)
 		if err != nil {
 			return
 		}
diff --git a/pkg/cmd/pr/edit/edit.go b/pkg/cmd/pr/edit/edit.go
index bab7a26d4..c606ae6bb 100644
--- a/pkg/cmd/pr/edit/edit.go
+++ b/pkg/cmd/pr/edit/edit.go
@@ -24,6 +24,7 @@ type EditOptions struct {
 	Surveyor        Surveyor
 	Fetcher         EditableOptionsFetcher
 	EditorRetriever EditorRetriever
+	Prompter        shared.EditPrompter
 
 	SelectorArg string
 	Interactive bool
@@ -35,9 +36,10 @@ func NewCmdEdit(f *cmdutil.Factory, runF func(*EditOptions) error) *cobra.Comman
 	opts := &EditOptions{
 		IO:              f.IOStreams,
 		HttpClient:      f.HttpClient,
-		Surveyor:        surveyor{},
+		Surveyor:        surveyor{P: f.Prompter},
 		Fetcher:         fetcher{},
 		EditorRetriever: editorRetriever{config: f.Config},
+		Prompter:        f.Prompter,
 	}
 
 	var bodyFile string
@@ -280,14 +282,16 @@ type Surveyor interface {
 	EditFields(*shared.Editable, string) error
 }
 
-type surveyor struct{}
+type surveyor struct {
+	P shared.EditPrompter
+}
 
 func (s surveyor) FieldsToEdit(editable *shared.Editable) error {
-	return shared.FieldsToEditSurvey(editable)
+	return shared.FieldsToEditSurvey(s.P, editable)
 }
 
 func (s surveyor) EditFields(editable *shared.Editable, editorCmd string) error {
-	return shared.EditFieldsSurvey(editable, editorCmd)
+	return shared.EditFieldsSurvey(s.P, editable, editorCmd)
 }
 
 type EditableOptionsFetcher interface {
diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go
index 0fbe689fc..cec3bfe8c 100644
--- a/pkg/cmd/pr/shared/editable.go
+++ b/pkg/cmd/pr/shared/editable.go
@@ -4,12 +4,9 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/AlecAivazis/survey/v2"
 	"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/set"
-	"github.com/cli/cli/v2/pkg/surveyext"
 )
 
 type Editable struct {
@@ -255,34 +252,45 @@ func (ep *EditableProjects) clone() EditableProjects {
 	}
 }
 
-func EditFieldsSurvey(editable *Editable, editorCommand string) error {
+type EditPrompter interface {
+	Select(string, string, []string) (int, error)
+	Input(string, string) (string, error)
+	MarkdownEditor(string, string, bool) (string, error)
+	MultiSelect(string, []string, []string) ([]int, error)
+	Confirm(string, bool) (bool, error)
+}
+
+func EditFieldsSurvey(p EditPrompter, editable *Editable, editorCommand string) error {
 	var err error
 	if editable.Title.Edited {
-		editable.Title.Value, err = titleSurvey(editable.Title.Default)
+		editable.Title.Value, err = p.Input("Title", editable.Title.Default)
 		if err != nil {
 			return err
 		}
 	}
 	if editable.Body.Edited {
-		editable.Body.Value, err = bodySurvey(editable.Body.Default, editorCommand)
+		editable.Body.Value, err = p.MarkdownEditor("Body", editable.Body.Default, false)
 		if err != nil {
 			return err
 		}
 	}
 	if editable.Reviewers.Edited {
-		editable.Reviewers.Value, err = multiSelectSurvey("Reviewers", editable.Reviewers.Default, editable.Reviewers.Options)
+		editable.Reviewers.Value, err = multiSelectSurvey(
+			p, "Reviewers", editable.Reviewers.Default, editable.Reviewers.Options)
 		if err != nil {
 			return err
 		}
 	}
 	if editable.Assignees.Edited {
-		editable.Assignees.Value, err = multiSelectSurvey("Assignees", editable.Assignees.Default, editable.Assignees.Options)
+		editable.Assignees.Value, err = multiSelectSurvey(
+			p, "Assignees", editable.Assignees.Default, editable.Assignees.Options)
 		if err != nil {
 			return err
 		}
 	}
 	if editable.Labels.Edited {
-		editable.Labels.Add, err = multiSelectSurvey("Labels", editable.Labels.Default, editable.Labels.Options)
+		editable.Labels.Add, err = multiSelectSurvey(
+			p, "Labels", editable.Labels.Default, editable.Labels.Options)
 		if err != nil {
 			return err
 		}
@@ -300,18 +308,19 @@ func EditFieldsSurvey(editable *Editable, editorCommand string) error {
 		}
 	}
 	if editable.Projects.Edited {
-		editable.Projects.Value, err = multiSelectSurvey("Projects", editable.Projects.Default, editable.Projects.Options)
+		editable.Projects.Value, err = multiSelectSurvey(
+			p, "Projects", editable.Projects.Default, editable.Projects.Options)
 		if err != nil {
 			return err
 		}
 	}
 	if editable.Milestone.Edited {
-		editable.Milestone.Value, err = milestoneSurvey(editable.Milestone.Default, editable.Milestone.Options)
+		editable.Milestone.Value, err = milestoneSurvey(p, editable.Milestone.Default, editable.Milestone.Options)
 		if err != nil {
 			return err
 		}
 	}
-	confirm, err := confirmSurvey()
+	confirm, err := p.Confirm("Submit?", true)
 	if err != nil {
 		return err
 	}
@@ -322,7 +331,7 @@ func EditFieldsSurvey(editable *Editable, editorCommand string) error {
 	return nil
 }
 
-func FieldsToEditSurvey(editable *Editable) error {
+func FieldsToEditSurvey(p EditPrompter, editable *Editable) error {
 	contains := func(s []string, str string) bool {
 		for _, v := range s {
 			if v == str {
@@ -337,7 +346,7 @@ func FieldsToEditSurvey(editable *Editable) error {
 		opts = append(opts, "Reviewers")
 	}
 	opts = append(opts, "Assignees", "Labels", "Projects", "Milestone")
-	results, err := multiSelectSurvey("What would you like to edit?", []string{}, opts)
+	results, err := multiSelectSurvey(p, "What would you like to edit?", []string{}, opts)
 	if err != nil {
 		return err
 	}
@@ -414,67 +423,34 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable)
 	return nil
 }
 
-func titleSurvey(title string) (string, error) {
-	var result string
-	q := &survey.Input{
-		Message: "Title",
-		Default: title,
-	}
-	err := survey.AskOne(q, &result)
-	return result, err
-}
-
-func bodySurvey(body, editorCommand string) (string, error) {
-	var result string
-	q := &surveyext.GhEditor{
-		EditorCommand: editorCommand,
-		Editor: &survey.Editor{
-			Message:       "Body",
-			FileName:      "*.md",
-			Default:       body,
-			HideDefault:   true,
-			AppendDefault: true,
-		},
-	}
-	err := survey.AskOne(q, &result)
-	return result, err
-}
-
-func multiSelectSurvey(message string, defaults, options []string) ([]string, error) {
+func multiSelectSurvey(p EditPrompter, message string, defaults, options []string) (results []string, err error) {
 	if len(options) == 0 {
 		return nil, nil
 	}
-	var results []string
-	q := &survey.MultiSelect{
-		Message: message,
-		Options: options,
-		Default: defaults,
-		Filter:  prompter.LatinMatchingFilter,
+
+	var selected []int
+	selected, err = p.MultiSelect(message, defaults, options)
+	if err != nil {
+		return
+	}
+
+	for _, i := range selected {
+		results = append(results, options[i])
 	}
-	err := survey.AskOne(q, &results)
+
 	return results, err
 }
 
-func milestoneSurvey(title string, opts []string) (string, error) {
+func milestoneSurvey(p EditPrompter, title string, opts []string) (result string, err error) {
 	if len(opts) == 0 {
 		return "", nil
 	}
-	var result string
-	q := &survey.Select{
-		Message: "Milestone",
-		Options: opts,
-		Default: title,
+	var selected int
+	selected, err = p.Select("Milestone", title, opts)
+	if err != nil {
+		return
 	}
-	err := survey.AskOne(q, &result)
-	return result, err
-}
 
-func confirmSurvey() (bool, error) {
-	var result bool
-	q := &survey.Confirm{
-		Message: "Submit?",
-		Default: true,
-	}
-	err := survey.AskOne(q, &result)
-	return result, err
+	result = opts[selected]
+	return
 }
diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go
index 4e1d1a421..393b87757 100644
--- a/pkg/cmd/pr/shared/survey.go
+++ b/pkg/cmd/pr/shared/survey.go
@@ -4,12 +4,9 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/AlecAivazis/survey/v2"
 	"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/iostreams"
-	"github.com/cli/cli/v2/pkg/prompt"
 )
 
 type Action int
@@ -37,6 +34,7 @@ type Prompt interface {
 	Select(string, string, []string) (int, error)
 	MarkdownEditor(string, string, bool) (string, error)
 	Confirm(string, bool) (bool, error)
+	MultiSelect(string, []string, []string) ([]int, error)
 }
 
 func ConfirmIssueSubmission(p Prompt, allowPreview bool, allowMetadata bool) (Action, error) {
@@ -142,7 +140,7 @@ type RepoMetadataFetcher interface {
 	RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error)
 }
 
-func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState) error {
+func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher RepoMetadataFetcher, state *IssueMetadataState) error {
 	isChosen := func(m string) bool {
 		for _, c := range state.Metadata {
 			if m == c {
@@ -160,18 +158,12 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
 	}
 	extraFieldsOptions = append(extraFieldsOptions, "Assignees", "Labels", "Projects", "Milestone")
 
-	//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
-	err := prompt.SurveyAsk([]*survey.Question{
-		{
-			Name: "metadata",
-			Prompt: &survey.MultiSelect{
-				Message: "What would you like to add?",
-				Options: extraFieldsOptions,
-			},
-		},
-	}, state)
+	selected, err := p.MultiSelect("What would you like to add?", nil, extraFieldsOptions)
 	if err != nil {
-		return fmt.Errorf("could not prompt: %w", err)
+		return err
+	}
+	for _, i := range selected {
+		state.Metadata = append(state.Metadata, extraFieldsOptions[i])
 	}
 
 	metadataInput := api.RepoMetadataInput{
@@ -215,63 +207,62 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
 		milestones = append(milestones, m.Title)
 	}
 
-	var mqs []*survey.Question
+	values := struct {
+		Reviewers []string
+		Assignees []string
+		Labels    []string
+		Projects  []string
+		Milestone string
+	}{}
+
 	if isChosen("Reviewers") {
 		if len(reviewers) > 0 {
-			mqs = append(mqs, &survey.Question{
-				Name: "reviewers",
-				Prompt: &survey.MultiSelect{
-					Message: "Reviewers",
-					Options: reviewers,
-					Default: state.Reviewers,
-					Filter:  prompter.LatinMatchingFilter,
-				},
-			})
+			selected, err := p.MultiSelect("Reviewers", state.Reviewers, reviewers)
+			if err != nil {
+				return err
+			}
+			for _, i := range selected {
+				values.Reviewers = append(values.Reviewers, reviewers[i])
+			}
 		} else {
 			fmt.Fprintln(io.ErrOut, "warning: no available reviewers")
 		}
 	}
 	if isChosen("Assignees") {
 		if len(assignees) > 0 {
-			mqs = append(mqs, &survey.Question{
-				Name: "assignees",
-				Prompt: &survey.MultiSelect{
-					Message: "Assignees",
-					Options: assignees,
-					Default: state.Assignees,
-					Filter:  prompter.LatinMatchingFilter,
-				},
-			})
+			selected, err := p.MultiSelect("Assignees", state.Assignees, assignees)
+			if err != nil {
+				return err
+			}
+			for _, i := range selected {
+				values.Assignees = append(values.Assignees, assignees[i])
+			}
 		} else {
 			fmt.Fprintln(io.ErrOut, "warning: no assignable users")
 		}
 	}
 	if isChosen("Labels") {
 		if len(labels) > 0 {
-			mqs = append(mqs, &survey.Question{
-				Name: "labels",
-				Prompt: &survey.MultiSelect{
-					Message: "Labels",
-					Options: labels,
-					Default: state.Labels,
-					Filter:  prompter.LatinMatchingFilter,
-				},
-			})
+			selected, err := p.MultiSelect("Labels", state.Labels, labels)
+			if err != nil {
+				return err
+			}
+			for _, i := range selected {
+				values.Labels = append(values.Labels, labels[i])
+			}
 		} else {
 			fmt.Fprintln(io.ErrOut, "warning: no labels in the repository")
 		}
 	}
 	if isChosen("Projects") {
 		if len(projects) > 0 {
-			mqs = append(mqs, &survey.Question{
-				Name: "projects",
-				Prompt: &survey.MultiSelect{
-					Message: "Projects",
-					Options: projects,
-					Default: state.Projects,
-					Filter:  prompter.LatinMatchingFilter,
-				},
-			})
+			selected, err := p.MultiSelect("Projects", state.Projects, projects)
+			if err != nil {
+				return err
+			}
+			for _, i := range selected {
+				values.Projects = append(values.Projects, projects[i])
+			}
 		} else {
 			fmt.Fprintln(io.ErrOut, "warning: no projects to choose from")
 		}
@@ -284,33 +275,16 @@ func MetadataSurvey(io *iostreams.IOStreams, baseRepo ghrepo.Interface, fetcher
 			} else {
 				milestoneDefault = milestones[1]
 			}
-			mqs = append(mqs, &survey.Question{
-				Name: "milestone",
-				Prompt: &survey.Select{
-					Message: "Milestone",
-					Options: milestones,
-					Default: milestoneDefault,
-				},
-			})
+			selected, err := p.Select("Milestone", milestoneDefault, milestones)
+			if err != nil {
+				return err
+			}
+			values.Milestone = milestones[selected]
 		} else {
 			fmt.Fprintln(io.ErrOut, "warning: no milestones in the repository")
 		}
 	}
 
-	values := struct {
-		Reviewers []string
-		Assignees []string
-		Labels    []string
-		Projects  []string
-		Milestone string
-	}{}
-
-	//nolint:staticcheck // SA1019: prompt.SurveyAsk is deprecated: use Prompter
-	err = prompt.SurveyAsk(mqs, &values)
-	if err != nil {
-		return fmt.Errorf("could not prompt: %w", err)
-	}
-
 	if isChosen("Reviewers") {
 		var logins []string
 		for _, r := range values.Reviewers {
diff --git a/pkg/cmd/repo/fork/fork.go b/pkg/cmd/repo/fork/fork.go
index 087d7219e..0d2d1da69 100644
--- a/pkg/cmd/repo/fork/fork.go
+++ b/pkg/cmd/repo/fork/fork.go
@@ -19,13 +19,16 @@ import (
 	"github.com/cli/cli/v2/pkg/cmd/repo/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"
 	"github.com/spf13/pflag"
 )
 
 const defaultRemoteName = "origin"
 
+type iprompter interface {
+	Confirm(string, bool) (bool, error)
+}
+
 type ForkOptions struct {
 	HttpClient func() (*http.Client, error)
 	GitClient  *git.Client
@@ -35,6 +38,7 @@ type ForkOptions struct {
 	Remotes    func() (ghContext.Remotes, error)
 	Since      func(time.Time) time.Duration
 	BackOff    backoff.BackOff
+	Prompter   iprompter
 
 	GitArgs           []string
 	Repository        string
@@ -65,6 +69,7 @@ func NewCmdFork(f *cmdutil.Factory, runF func(*ForkOptions) error) *cobra.Comman
 		Config:     f.Config,
 		BaseRepo:   f.BaseRepo,
 		Remotes:    f.Remotes,
+		Prompter:   f.Prompter,
 		Since:      time.Since,
 	}
 
@@ -273,10 +278,9 @@ func forkRun(opts *ForkOptions) error {
 
 		remoteDesired := opts.Remote
 		if opts.PromptRemote {
-			//nolint:staticcheck // SA1019: prompt.Confirm is deprecated: use Prompter
-			err = prompt.Confirm("Would you like to add a remote for the fork?", &remoteDesired)
+			remoteDesired, err = opts.Prompter.Confirm("Would you like to add a remote for the fork?", false)
 			if err != nil {
-				return fmt.Errorf("failed to prompt: %w", err)
+				return err
 			}
 		}
 
@@ -317,10 +321,9 @@ func forkRun(opts *ForkOptions) error {
 	} else {
 		cloneDesired := opts.Clone
 		if opts.PromptClone {
-			//nolint:staticcheck // SA1019: prompt.Confirm is deprecated: use Prompter
-			err = prompt.Confirm("Would you like to clone the fork?", &cloneDesired)
+			cloneDesired, err = opts.Prompter.Confirm("Would you like to clone the fork?", false)
 			if err != nil {
-				return fmt.Errorf("failed to prompt: %w", err)
+				return err
 			}
 		}
 		if cloneDesired {
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/secret/set/set.go b/pkg/cmd/secret/set/set.go
index 86dd35258..95f80f8b1 100644
--- a/pkg/cmd/secret/set/set.go
+++ b/pkg/cmd/secret/set/set.go
@@ -9,7 +9,6 @@ import (
 	"os"
 	"strings"
 
-	"github.com/AlecAivazis/survey/v2"
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/config"
@@ -17,7 +16,6 @@ import (
 	"github.com/cli/cli/v2/pkg/cmd/secret/shared"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
-	"github.com/cli/cli/v2/pkg/prompt"
 	"github.com/hashicorp/go-multierror"
 	"github.com/joho/godotenv"
 	"github.com/spf13/cobra"
@@ -29,6 +27,7 @@ type SetOptions struct {
 	IO         *iostreams.IOStreams
 	Config     func() (config.Config, error)
 	BaseRepo   func() (ghrepo.Interface, error)
+	Prompter   iprompter
 
 	RandomOverride func() io.Reader
 
@@ -44,11 +43,16 @@ type SetOptions struct {
 	Application     string
 }
 
+type iprompter interface {
+	Password(string) (string, error)
+}
+
 func NewCmdSet(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command {
 	opts := &SetOptions{
 		IO:         f.IOStreams,
 		Config:     f.Config,
 		HttpClient: f.HttpClient,
+		Prompter:   f.Prompter,
 	}
 
 	cmd := &cobra.Command{
@@ -375,11 +379,7 @@ func getBody(opts *SetOptions) ([]byte, error) {
 	}
 
 	if opts.IO.CanPrompt() {
-		var bodyInput string
-		//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
-		err := prompt.SurveyAskOne(&survey.Password{
-			Message: "Paste your secret",
-		}, &bodyInput)
+		bodyInput, err := opts.Prompter.Password("Paste your secret")
 		if err != nil {
 			return nil, err
 		}
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/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/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/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
 	}
diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go
deleted file mode 100644
index 683fad913..000000000
--- a/pkg/prompt/prompt.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package prompt
-
-import "github.com/AlecAivazis/survey/v2"
-
-// Deprecated: use PrompterMock
-func StubConfirm(result bool) func() {
-	orig := Confirm
-	Confirm = func(_ string, r *bool) error {
-		*r = result
-		return nil
-	}
-	return func() {
-		Confirm = orig
-	}
-}
-
-// Deprecated: use Prompter
-var Confirm = func(prompt string, result *bool) error {
-	p := &survey.Confirm{
-		Message: prompt,
-		Default: true,
-	}
-	return SurveyAskOne(p, result)
-}
-
-// Deprecated: use Prompter
-var SurveyAskOne = func(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
-	return survey.AskOne(p, response, opts...)
-}
-
-// Deprecated: use Prompter
-var SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
-	return survey.Ask(qs, response, opts...)
-}
diff --git a/pkg/prompt/stubber.go b/pkg/prompt/stubber.go
deleted file mode 100644
index 8011c5924..000000000
--- a/pkg/prompt/stubber.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package prompt
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/AlecAivazis/survey/v2"
-	"github.com/AlecAivazis/survey/v2/core"
-	"github.com/cli/cli/v2/pkg/surveyext"
-)
-
-type AskStubber struct {
-	stubs []*QuestionStub
-}
-
-type testing interface {
-	Errorf(format string, args ...interface{})
-	Cleanup(func())
-}
-
-// Deprecated: use PrompterMock
-func NewAskStubber(t testing) *AskStubber {
-	as, teardown := InitAskStubber()
-	t.Cleanup(func() {
-		teardown()
-		for _, s := range as.stubs {
-			if !s.matched {
-				t.Errorf("unmatched prompt stub: %+v", s)
-			}
-		}
-	})
-	return as
-}
-
-// Deprecated: use NewAskStubber
-func InitAskStubber() (*AskStubber, func()) {
-	origSurveyAsk := SurveyAsk
-	origSurveyAskOne := SurveyAskOne
-	as := AskStubber{}
-
-	answerFromStub := func(p survey.Prompt, fieldName string, response interface{}) error {
-		var message string
-		var defaultValue interface{}
-		var options []string
-		switch pt := p.(type) {
-		case *survey.Confirm:
-			message = pt.Message
-			defaultValue = pt.Default
-		case *survey.Input:
-			message = pt.Message
-			defaultValue = pt.Default
-		case *survey.Select:
-			message = pt.Message
-			options = pt.Options
-		case *survey.MultiSelect:
-			message = pt.Message
-			options = pt.Options
-		case *survey.Password:
-			message = pt.Message
-		case *surveyext.GhEditor:
-			message = pt.Message
-			defaultValue = pt.Default
-		default:
-			return fmt.Errorf("prompt type %T is not supported by the stubber", pt)
-		}
-
-		var stub *QuestionStub
-		for _, s := range as.stubs {
-			if !s.matched && (s.message == "" && strings.EqualFold(s.Name, fieldName) || s.message == message) {
-				stub = s
-				stub.matched = true
-				break
-			}
-		}
-		if stub == nil {
-			return fmt.Errorf("no prompt stub for %q", message)
-		}
-
-		if len(stub.options) > 0 {
-			if err := compareOptions(stub.options, options); err != nil {
-				return fmt.Errorf("stubbed options mismatch for %q: %v", message, err)
-			}
-		}
-
-		userValue := stub.Value
-
-		if stringValue, ok := stub.Value.(string); ok && len(options) > 0 {
-			foundIndex := -1
-			for i, o := range options {
-				if o == stringValue {
-					foundIndex = i
-					break
-				}
-			}
-			if foundIndex < 0 {
-				return fmt.Errorf("answer %q not found in options for %q: %v", stringValue, message, options)
-			}
-			userValue = core.OptionAnswer{
-				Value: stringValue,
-				Index: foundIndex,
-			}
-		}
-
-		if stub.Default {
-			if defaultIndex, ok := defaultValue.(int); ok && len(options) > 0 {
-				userValue = core.OptionAnswer{
-					Value: options[defaultIndex],
-					Index: defaultIndex,
-				}
-			} else if defaultValue == nil && len(options) > 0 {
-				userValue = core.OptionAnswer{
-					Value: options[0],
-					Index: 0,
-				}
-			} else {
-				userValue = defaultValue
-			}
-		}
-
-		if err := core.WriteAnswer(response, fieldName, userValue); err != nil {
-			topic := fmt.Sprintf("field %q", fieldName)
-			if fieldName == "" {
-				topic = fmt.Sprintf("%q", message)
-			}
-			return fmt.Errorf("AskStubber failed writing the answer for %s: %w", topic, err)
-		}
-		return nil
-	}
-
-	SurveyAskOne = func(p survey.Prompt, response interface{}, opts ...survey.AskOpt) error {
-		return answerFromStub(p, "", response)
-	}
-
-	SurveyAsk = func(qs []*survey.Question, response interface{}, opts ...survey.AskOpt) error {
-		for _, q := range qs {
-			if err := answerFromStub(q.Prompt, q.Name, response); err != nil {
-				return err
-			}
-		}
-		return nil
-	}
-
-	teardown := func() {
-		SurveyAsk = origSurveyAsk
-		SurveyAskOne = origSurveyAskOne
-	}
-	return &as, teardown
-}
-
-type QuestionStub struct {
-	Name    string
-	Value   interface{}
-	Default bool
-
-	matched bool
-	message string
-	options []string
-}
-
-// AssertOptions asserts the options presented to the user in Selects and MultiSelects.
-func (s *QuestionStub) AssertOptions(opts []string) *QuestionStub {
-	s.options = opts
-	return s
-}
-
-// AnswerWith defines an answer for the given stub.
-func (s *QuestionStub) AnswerWith(v interface{}) *QuestionStub {
-	s.Value = v
-	return s
-}
-
-// AnswerDefault marks the current stub to be answered with the default value for the prompt question.
-func (s *QuestionStub) AnswerDefault() *QuestionStub {
-	s.Default = true
-	return s
-}
-
-// Deprecated: use StubPrompt
-func (as *AskStubber) StubOne(value interface{}) {
-	as.Stub([]*QuestionStub{{Value: value}})
-}
-
-// Deprecated: use StubPrompt
-func (as *AskStubber) Stub(stubbedQuestions []*QuestionStub) {
-	as.stubs = append(as.stubs, stubbedQuestions...)
-}
-
-// StubPrompt records a stub for an interactive prompt matched by its message.
-func (as *AskStubber) StubPrompt(msg string) *QuestionStub {
-	stub := &QuestionStub{message: msg}
-	as.stubs = append(as.stubs, stub)
-	return stub
-}
-
-func compareOptions(expected, got []string) error {
-	if len(expected) != len(got) {
-		return fmt.Errorf("expected %v, got %v (length mismatch)", expected, got)
-	}
-	for i, v := range expected {
-		if v != got[i] {
-			return fmt.Errorf("expected %v, got %v", expected, got)
-		}
-	}
-	return nil
-}


Test patch:
diff --git a/internal/prompter/test.go b/internal/prompter/test.go
index c376e9c83..0dea18e5a 100644
--- a/internal/prompter/test.go
+++ b/internal/prompter/test.go
@@ -258,7 +258,7 @@ func (m *MockPrompter) RegisterInputHostname(stub func() (string, error)) {
 }
 
 func (m *MockPrompter) RegisterPassword(prompt string, stub func(string) (string, error)) {
-	m.PasswordStubs = append(m.PasswordStubs, PasswordStub{Fn: stub})
+	m.PasswordStubs = append(m.PasswordStubs, PasswordStub{Prompt: prompt, Fn: stub})
 }
 
 func (m *MockPrompter) RegisterConfirmDeletion(prompt string, stub func(string) error) {
diff --git a/pkg/cmd/issue/delete/delete_test.go b/pkg/cmd/issue/delete/delete_test.go
index e3b2613ce..acc52af65 100644
--- a/pkg/cmd/issue/delete/delete_test.go
+++ b/pkg/cmd/issue/delete/delete_test.go
@@ -2,6 +2,7 @@ package delete
 
 import (
 	"bytes"
+	"errors"
 	"io"
 	"net/http"
 	"regexp"
@@ -9,16 +10,16 @@ import (
 
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
 	"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/cli/cli/v2/test"
 	"github.com/google/shlex"
 	"github.com/stretchr/testify/assert"
 )
 
-func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, error) {
+func runCommand(rt http.RoundTripper, pm *prompter.MockPrompter, isTTY bool, cli string) (*test.CmdOut, error) {
 	ios, _, stdout, stderr := iostreams.Test()
 	ios.SetStdoutTTY(isTTY)
 	ios.SetStdinTTY(isTTY)
@@ -26,6 +27,7 @@ func runCommand(rt http.RoundTripper, isTTY bool, cli string) (*test.CmdOut, err
 
 	factory := &cmdutil.Factory{
 		IOStreams: ios,
+		Prompter:  pm,
 		HttpClient: func() (*http.Client, error) {
 			return &http.Client{Transport: rt}, nil
 		},
@@ -76,11 +78,10 @@ func TestIssueDelete(t *testing.T) {
 			}),
 	)
 
-	//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
-	as := prompt.NewAskStubber(t)
-	as.StubPrompt("You're going to delete issue #13. This action cannot be reversed. To confirm, type the issue number:").AnswerWith("13")
+	pm := prompter.NewMockPrompter(t)
+	pm.RegisterConfirmDeletion("13", func(_ string) error { return nil })
 
-	output, err := runCommand(httpRegistry, true, "13")
+	output, err := runCommand(httpRegistry, pm, true, "13")
 	if err != nil {
 		t.Fatalf("error running command `issue delete`: %v", err)
 	}
@@ -112,7 +113,7 @@ func TestIssueDelete_confirm(t *testing.T) {
 			}),
 	)
 
-	output, err := runCommand(httpRegistry, true, "13 --confirm")
+	output, err := runCommand(httpRegistry, nil, true, "13 --confirm")
 	if err != nil {
 		t.Fatalf("error running command `issue delete`: %v", err)
 	}
@@ -137,19 +138,17 @@ func TestIssueDelete_cancel(t *testing.T) {
 			} } }`),
 	)
 
-	//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
-	as := prompt.NewAskStubber(t)
-	as.StubPrompt("You're going to delete issue #13. This action cannot be reversed. To confirm, type the issue number:").AnswerWith("14")
+	pm := prompter.NewMockPrompter(t)
+	pm.RegisterConfirmDeletion("13", func(_ string) error {
+		return errors.New("You entered 14")
+	})
 
-	output, err := runCommand(httpRegistry, true, "13")
-	if err != nil {
-		t.Fatalf("error running command `issue delete`: %v", err)
+	_, err := runCommand(httpRegistry, pm, true, "13")
+	if err == nil {
+		t.Fatalf("expected error")
 	}
-
-	r := regexp.MustCompile(`Issue #13 was not deleted`)
-
-	if !r.MatchString(output.String()) {
-		t.Fatalf("output did not match regexp /%s/\n> output\n%q\n", r, output.String())
+	if err.Error() != "You entered 14" {
+		t.Fatalf("got unexpected error '%s'", err)
 	}
 }
 
@@ -166,7 +165,7 @@ func TestIssueDelete_doesNotExist(t *testing.T) {
 			`),
 	)
 
-	_, err := runCommand(httpRegistry, true, "13")
+	_, err := runCommand(httpRegistry, nil, true, "13")
 	if err == nil || err.Error() != "GraphQL: Could not resolve to an Issue with the number of 13." {
 		t.Errorf("error running command `issue delete`: %v", err)
 	}
@@ -199,7 +198,7 @@ func TestIssueDelete_issuesDisabled(t *testing.T) {
 		}`),
 	)
 
-	_, err := runCommand(httpRegistry, true, "13")
+	_, err := runCommand(httpRegistry, nil, true, "13")
 	if err == nil || err.Error() != "the 'OWNER/REPO' repository has disabled issues" {
 		t.Fatalf("got error: %v", err)
 	}
diff --git a/pkg/cmd/issue/edit/edit_test.go b/pkg/cmd/issue/edit/edit_test.go
index d73132f35..db0b09d2d 100644
--- a/pkg/cmd/issue/edit/edit_test.go
+++ b/pkg/cmd/issue/edit/edit_test.go
@@ -511,7 +511,7 @@ func Test_editRun(t *testing.T) {
 			input: &EditOptions{
 				SelectorArgs: []string{"123"},
 				Interactive:  true,
-				FieldsToEditSurvey: func(eo *prShared.Editable) error {
+				FieldsToEditSurvey: func(p prShared.EditPrompter, eo *prShared.Editable) error {
 					eo.Title.Edited = true
 					eo.Body.Edited = true
 					eo.Assignees.Edited = true
@@ -520,7 +520,7 @@ func Test_editRun(t *testing.T) {
 					eo.Milestone.Edited = true
 					return nil
 				},
-				EditFieldsSurvey: func(eo *prShared.Editable, _ string) error {
+				EditFieldsSurvey: func(p prShared.EditPrompter, eo *prShared.Editable, _ string) error {
 					eo.Title.Value = "new title"
 					eo.Body.Value = "new body"
 					eo.Assignees.Value = []string{"monalisa", "hubot"}
diff --git a/pkg/cmd/pr/shared/survey_test.go b/pkg/cmd/pr/shared/survey_test.go
index 0bc27e8d6..4653ce08b 100644
--- a/pkg/cmd/pr/shared/survey_test.go
+++ b/pkg/cmd/pr/shared/survey_test.go
@@ -5,8 +5,8 @@ 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/iostreams"
-	"github.com/cli/cli/v2/pkg/prompt"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -43,45 +43,32 @@ func TestMetadataSurvey_selectAll(t *testing.T) {
 		},
 	}
 
-	//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
-	as, restoreAsk := prompt.InitAskStubber()
-	defer restoreAsk()
-
-	//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
-	as.Stub([]*prompt.QuestionStub{
-		{
-			Name:  "metadata",
-			Value: []string{"Labels", "Projects", "Assignees", "Reviewers", "Milestone"},
-		},
+	pm := prompter.NewMockPrompter(t)
+	pm.RegisterMultiSelect("What would you like to add?",
+		[]string{}, []string{"Reviewers", "Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, _ []string) ([]int, error) {
+			return []int{0, 1, 2, 3, 4}, nil
+		})
+	pm.RegisterMultiSelect("Reviewers", []string{}, []string{"hubot", "monalisa"}, func(_ string, _, _ []string) ([]int, error) {
+		return []int{1}, nil
 	})
-	//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
-	as.Stub([]*prompt.QuestionStub{
-		{
-			Name:  "reviewers",
-			Value: []string{"monalisa"},
-		},
-		{
-			Name:  "assignees",
-			Value: []string{"hubot"},
-		},
-		{
-			Name:  "labels",
-			Value: []string{"good first issue"},
-		},
-		{
-			Name:  "projects",
-			Value: []string{"The road to 1.0"},
-		},
-		{
-			Name:  "milestone",
-			Value: "(none)",
-		},
+	pm.RegisterMultiSelect("Assignees", []string{}, []string{"hubot", "monalisa"}, func(_ string, _, _ []string) ([]int, error) {
+		return []int{0}, nil
+	})
+	pm.RegisterMultiSelect("Labels", []string{}, []string{"help wanted", "good first issue"}, func(_ string, _, _ []string) ([]int, error) {
+		return []int{1}, nil
+	})
+	pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring", "The road to 1.0"}, func(_ string, _, _ []string) ([]int, error) {
+		return []int{1}, nil
+	})
+	pm.RegisterSelect("Milestone", []string{"(none)", "1.2 patch release"}, func(_, _ string, _ []string) (int, error) {
+		return 0, nil
 	})
 
 	state := &IssueMetadataState{
 		Assignees: []string{"hubot"},
+		Type:      PRMetadata,
 	}
-	err := MetadataSurvey(ios, repo, fetcher, state)
+	err := MetadataSurvey(pm, ios, repo, fetcher, state)
 	assert.NoError(t, err)
 
 	assert.Equal(t, "", stdout.String())
@@ -112,33 +99,21 @@ func TestMetadataSurvey_keepExisting(t *testing.T) {
 		},
 	}
 
-	//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
-	as, restoreAsk := prompt.InitAskStubber()
-	defer restoreAsk()
-
-	//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
-	as.Stub([]*prompt.QuestionStub{
-		{
-			Name:  "metadata",
-			Value: []string{"Labels", "Projects"},
-		},
+	pm := prompter.NewMockPrompter(t)
+	pm.RegisterMultiSelect("What would you like to add?", []string{}, []string{"Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, _ []string) ([]int, error) {
+		return []int{1, 2}, nil
 	})
-	//nolint:staticcheck // SA1019: as.Stub is deprecated: use StubPrompt
-	as.Stub([]*prompt.QuestionStub{
-		{
-			Name:  "labels",
-			Value: []string{"good first issue"},
-		},
-		{
-			Name:  "projects",
-			Value: []string{"The road to 1.0"},
-		},
+	pm.RegisterMultiSelect("Labels", []string{}, []string{"help wanted", "good first issue"}, func(_ string, _, _ []string) ([]int, error) {
+		return []int{1}, nil
+	})
+	pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring", "The road to 1.0"}, func(_ string, _, _ []string) ([]int, error) {
+		return []int{1}, nil
 	})
 
 	state := &IssueMetadataState{
 		Assignees: []string{"hubot"},
 	}
-	err := MetadataSurvey(ios, repo, fetcher, state)
+	err := MetadataSurvey(pm, ios, repo, fetcher, state)
 	assert.NoError(t, err)
 
 	assert.Equal(t, "", stdout.String())
diff --git a/pkg/cmd/repo/fork/fork_test.go b/pkg/cmd/repo/fork/fork_test.go
index 5833acb5d..f59fba1b7 100644
--- a/pkg/cmd/repo/fork/fork_test.go
+++ b/pkg/cmd/repo/fork/fork_test.go
@@ -14,11 +14,11 @@ import (
 	"github.com/cli/cli/v2/git"
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
 	"github.com/cli/cli/v2/internal/run"
 	"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"
 )
@@ -205,18 +205,18 @@ func TestRepoFork(t *testing.T) {
 	}
 
 	tests := []struct {
-		name       string
-		opts       *ForkOptions
-		tty        bool
-		httpStubs  func(*httpmock.Registry)
-		execStubs  func(*run.CommandStubber)
-		askStubs   func(*prompt.AskStubber)
-		cfgStubs   func(*config.ConfigMock)
-		remotes    []*context.Remote
-		wantOut    string
-		wantErrOut string
-		wantErr    bool
-		errMsg     string
+		name        string
+		opts        *ForkOptions
+		tty         bool
+		httpStubs   func(*httpmock.Registry)
+		execStubs   func(*run.CommandStubber)
+		promptStubs func(*prompter.MockPrompter)
+		cfgStubs    func(*config.ConfigMock)
+		remotes     []*context.Remote
+		wantOut     string
+		wantErrOut  string
+		wantErr     bool
+		errMsg      string
 	}{
 		{
 			name: "implicit match, configured protocol overrides provided",
@@ -272,9 +272,10 @@ func TestRepoFork(t *testing.T) {
 				RemoteName:   defaultRemoteName,
 			},
 			httpStubs: forkPost,
-			askStubs: func(as *prompt.AskStubber) {
-				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
-				as.StubOne(false)
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterConfirm("Would you like to add a remote for the fork?", func(_ string, _ bool) (bool, error) {
+					return false, nil
+				})
 			},
 			wantErrOut: "✓ Created fork someone/REPO\n",
 		},
@@ -291,9 +292,10 @@ func TestRepoFork(t *testing.T) {
 				cs.Register("git remote rename origin upstream", 0, "")
 				cs.Register(`git remote add origin https://github.com/someone/REPO.git`, 0, "")
 			},
-			askStubs: func(as *prompt.AskStubber) {
-				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
-				as.StubOne(true)
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterConfirm("Would you like to add a remote for the fork?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
 			},
 			wantErrOut: "✓ Created fork someone/REPO\n✓ Added remote origin\n",
 		},
@@ -497,9 +499,10 @@ func TestRepoFork(t *testing.T) {
 				PromptClone: true,
 			},
 			httpStubs: forkPost,
-			askStubs: func(as *prompt.AskStubber) {
-				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
-				as.StubOne(false)
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterConfirm("Would you like to clone the fork?", func(_ string, _ bool) (bool, error) {
+					return false, nil
+				})
 			},
 			wantErrOut: "✓ Created fork someone/REPO\n",
 		},
@@ -511,9 +514,10 @@ func TestRepoFork(t *testing.T) {
 				PromptClone: true,
 			},
 			httpStubs: forkPost,
-			askStubs: func(as *prompt.AskStubber) {
-				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
-				as.StubOne(true)
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterConfirm("Would you like to clone the fork?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
 			},
 			execStubs: func(cs *run.CommandStubber) {
 				cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "")
@@ -533,9 +537,10 @@ func TestRepoFork(t *testing.T) {
 				},
 			},
 			httpStubs: forkPost,
-			askStubs: func(as *prompt.AskStubber) {
-				//nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt
-				as.StubOne(true)
+			promptStubs: func(pm *prompter.MockPrompter) {
+				pm.RegisterConfirm("Would you like to clone the fork?", func(_ string, _ bool) (bool, error) {
+					return true, nil
+				})
 			},
 			execStubs: func(cs *run.CommandStubber) {
 				cs.Register(`git clone https://github.com/someone/REPO\.git`, 0, "")
@@ -746,11 +751,10 @@ func TestRepoFork(t *testing.T) {
 				GitPath: "some/path/git",
 			}
 
-			//nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber
-			as, teardown := prompt.InitAskStubber()
-			defer teardown()
-			if tt.askStubs != nil {
-				tt.askStubs(as)
+			pm := prompter.NewMockPrompter(t)
+			tt.opts.Prompter = pm
+			if tt.promptStubs != nil {
+				tt.promptStubs(pm)
 			}
 
 			cs, restoreRun := run.Stub()
diff --git a/pkg/cmd/secret/set/set_test.go b/pkg/cmd/secret/set/set_test.go
index 592527c5f..376ef2d53 100644
--- a/pkg/cmd/secret/set/set_test.go
+++ b/pkg/cmd/secret/set/set_test.go
@@ -12,11 +12,11 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/internal/config"
 	"github.com/cli/cli/v2/internal/ghrepo"
+	"github.com/cli/cli/v2/internal/prompter"
 	"github.com/cli/cli/v2/pkg/cmd/secret/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"
 )
@@ -595,12 +595,14 @@ func Test_getBodyPrompt(t *testing.T) {
 	ios.SetStdinTTY(true)
 	ios.SetStdoutTTY(true)
 
-	//nolint:staticcheck // SA1019: prompt.NewAskStubber is deprecated: use PrompterMock
-	as := prompt.NewAskStubber(t)
-	as.StubPrompt("Paste your secret").AnswerWith("cool secret")
+	pm := prompter.NewMockPrompter(t)
+	pm.RegisterPassword("Paste your secret", func(_ string) (string, error) {
+		return "cool secret", nil
+	})
 
 	body, err := getBody(&SetOptions{
-		IO: ios,
+		IO:       ios,
+		Prompter: pm,
 	})
 	assert.NoError(t, err)
 	assert.Equal(t, string(body), "cool secret")
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_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_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": {
-					Required:    true,
-					Description: "good foo",
-				},
-				"bar": {
+			wantOut: []WorkflowInput{
+				{
+					Name:    "bar",
 					Default: "boo",
 				},
-				"baz": {
+				{
+					Name:        "baz",
 					Description: "it's baz",
 				},
-				"quux": {
+				{
+					Name:        "foo",
+					Required:    true,
+					Description: "good foo",
+				},
+				{
+					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)