Repository: cli/cli
Pull request #10818 (2025-04-17 19:08:00)

Feature detect v1 projects on web mode issue create
## Description

Relates to: https://github.com/cli/cli/issues/10714
Builds on: https://github.com/cli/cli/pull/10815

This PR tackles projectv1 deprecation on issue creation for `--web` mode.

### Reviewer Notes

**DO NOT MERGE** into base branch, wait for base branch to be merged into trunk.

You can verify the presence or absence of `projects` by running:

```
GH_DEBUG=api ./bin/gh issue create --title foo --body bar --project "non-existent" --web 2>&1 | grep "projects("
```

This exists for queries `RepositoryProjectList` and `OrganizationProjectList`


Patch:
diff --git a/api/queries_repo.go b/api/queries_repo.go
index 7c33825b0..27e21eb32 100644
--- a/api/queries_repo.go
+++ b/api/queries_repo.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghinstance"
 	"golang.org/x/sync/errgroup"
 
@@ -782,35 +783,54 @@ func (m *RepoMetadataResult) projectV2TitleToID(projectTitle string) (string, bo
 	return "", false
 }
 
-func ProjectsToPaths(projects []RepoProject, projectsV2 []ProjectV2, names []string) ([]string, error) {
-	var paths []string
-	for _, projectName := range names {
-		found := false
-		for _, p := range projects {
-			if strings.EqualFold(projectName, p.Name) {
-				// format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER
-				// required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER
-				var path string
-				pathParts := strings.Split(p.ResourcePath, "/")
-				if pathParts[1] == "orgs" || pathParts[1] == "users" {
-					path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
-				} else {
-					path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
+func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string, projectsV1Support gh.ProjectsV1Support) ([]string, error) {
+	paths := make([]string, 0, len(projectNames))
+	matchedPaths := map[string]struct{}{}
+
+	// TODO: ProjectsV1Cleanup
+	// At this point, we only know the names that the user has provided, so we can't push this conditional up the stack.
+	// First we'll try to match against v1 projects, if supported
+	if projectsV1Support == gh.ProjectsV1Supported {
+		v1Projects, err := v1Projects(client, repo)
+		if err != nil {
+			return nil, err
+		}
+
+		for _, projectName := range projectNames {
+			for _, p := range v1Projects {
+				if strings.EqualFold(projectName, p.Name) {
+					pathParts := strings.Split(p.ResourcePath, "/")
+					var path string
+					if pathParts[1] == "orgs" || pathParts[1] == "users" {
+						path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
+					} else {
+						path = fmt.Sprintf("%s/%s/%s", pathParts[1], pathParts[2], pathParts[4])
+					}
+					paths = append(paths, path)
+					matchedPaths[projectName] = struct{}{}
+					break
 				}
-				paths = append(paths, path)
-				found = true
-				break
 			}
 		}
-		if found {
+	}
+
+	// Then we'll try to match against v2 projects
+	v2Projects, err := v2Projects(client, repo)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, projectName := range projectNames {
+		// If we already found a v1 project with this name, skip it
+		if _, ok := matchedPaths[projectName]; ok {
 			continue
 		}
-		for _, p := range projectsV2 {
+
+		found := false
+		for _, p := range v2Projects {
 			if strings.EqualFold(projectName, p.Title) {
-				// format of ResourcePath: /OWNER/REPO/projects/PROJECT_NUMBER or /orgs/ORG/projects/PROJECT_NUMBER or /users/USER/projects/PROJECT_NUBER
-				// required format of path: OWNER/REPO/PROJECT_NUMBER or ORG/PROJECT_NUMBER or USER/PROJECT_NUMBER
-				var path string
 				pathParts := strings.Split(p.ResourcePath, "/")
+				var path string
 				if pathParts[1] == "orgs" || pathParts[1] == "users" {
 					path = fmt.Sprintf("%s/%s", pathParts[2], pathParts[4])
 				} else {
@@ -821,10 +841,12 @@ func ProjectsToPaths(projects []RepoProject, projectsV2 []ProjectV2, names []str
 				break
 			}
 		}
+
 		if !found {
 			return nil, fmt.Errorf("'%s' not found", projectName)
 		}
 	}
+
 	return paths, nil
 }
 
@@ -1253,14 +1275,6 @@ func RepoMilestones(client *Client, repo ghrepo.Interface, state string) ([]Repo
 	return milestones, nil
 }
 
-func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []string) ([]string, error) {
-	projects, projectsV2, err := relevantProjects(client, repo)
-	if err != nil {
-		return nil, err
-	}
-	return ProjectsToPaths(projects, projectsV2, projectNames)
-}
-
 // v1Projects retrieves set of RepoProjects relevant to given repository:
 // - Projects for repository
 // - Projects for repository organization, if it belongs to one
@@ -1366,26 +1380,6 @@ func v2Projects(client *Client, repo ghrepo.Interface) ([]ProjectV2, error) {
 	return projectsV2, nil
 }
 
-// relevantProjects retrieves set of Project or ProjectV2 relevant to given repository:
-// - Projects for repository
-// - Projects for repository organization, if it belongs to one
-// - ProjectsV2 owned by current user
-// - ProjectsV2 linked to repository
-// - ProjectsV2 owned by repository organization, if it belongs to one
-func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []ProjectV2, error) {
-	v1Projects, err := v1Projects(client, repo)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	v2Projects, err := v2Projects(client, repo)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	return v1Projects, v2Projects, nil
-}
-
 func CreateRepoTransformToV4(apiClient *Client, hostname string, method string, path string, body io.Reader) (*Repository, error) {
 	var responsev3 repositoryV3
 	err := apiClient.REST(hostname, method, path, body, &responsev3)
diff --git a/go.mod b/go.mod
index 31b07f2cf..8fe69cc84 100644
--- a/go.mod
+++ b/go.mod
@@ -53,7 +53,7 @@ require (
 	golang.org/x/sync v0.13.0
 	golang.org/x/term v0.31.0
 	golang.org/x/text v0.24.0
-	google.golang.org/grpc v1.71.1
+	google.golang.org/grpc v1.71.0
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/h2non/gock.v1 v1.1.2
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/go.sum b/go.sum
index b312bcf6c..63a1cdd77 100644
--- a/go.sum
+++ b/go.sum
@@ -601,8 +601,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:
 google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
-google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/config/config.go b/internal/config/config.go
index c78dac15c..e7534dfdb 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -14,18 +14,15 @@ import (
 	ghConfig "github.com/cli/go-gh/v2/pkg/config"
 )
 
-// Important: some of the following configuration settings are used outside of `cli/cli`,
-// they are defined here to avoid `cli/cli` being changed unexpectedly.
 const (
-	accessibleColorsKey   = "accessible_colors" // used by cli/go-gh to enable the use of customizable, accessible 4-bit colors.
 	aliasesKey            = "aliases"
-	browserKey            = "browser" // used by cli/go-gh to open URLs in web browsers
+	browserKey            = "browser"
 	colorLabelsKey        = "color_labels"
-	editorKey             = "editor" // used by cli/go-gh to open interactive text editor
+	editorKey             = "editor"
 	gitProtocolKey        = "git_protocol"
-	hostsKey              = "hosts" // used by cli/go-gh to locate authenticated host tokens
+	hostsKey              = "hosts"
 	httpUnixSocketKey     = "http_unix_socket"
-	oauthTokenKey         = "oauth_token" // used by cli/go-gh to locate authenticated host tokens
+	oauthTokenKey         = "oauth_token"
 	pagerKey              = "pager"
 	promptKey             = "prompt"
 	preferEditorPromptKey = "prefer_editor_prompt"
@@ -112,11 +109,6 @@ func (c *cfg) Authentication() gh.AuthConfig {
 	return &AuthConfig{cfg: c.cfg}
 }
 
-func (c *cfg) AccessibleColors(hostname string) gh.ConfigEntry {
-	// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
-	return c.GetOrDefault(hostname, accessibleColorsKey).Unwrap()
-}
-
 func (c *cfg) Browser(hostname string) gh.ConfigEntry {
 	// Intentionally panic if there is no user provided value or default value (which would be a programmer error)
 	return c.GetOrDefault(hostname, browserKey).Unwrap()
@@ -548,8 +540,6 @@ http_unix_socket:
 browser:
 # Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
 color_labels: disabled
-# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled
-accessible_colors: disabled
 `
 
 type ConfigOption struct {
@@ -629,15 +619,6 @@ var Options = []ConfigOption{
 			return c.ColorLabels(hostname).Value
 		},
 	},
-	{
-		Key:           accessibleColorsKey,
-		Description:   "whether customizable, 4-bit accessible colors should be used",
-		DefaultValue:  "disabled",
-		AllowedValues: []string{"enabled", "disabled"},
-		CurrentValue: func(c gh.Config, hostname string) string {
-			return c.AccessibleColors(hostname).Value
-		},
-	},
 }
 
 func HomeDirPath(subdir string) (string, error) {
diff --git a/internal/config/stub.go b/internal/config/stub.go
index 8b9f14290..78073da4a 100644
--- a/internal/config/stub.go
+++ b/internal/config/stub.go
@@ -52,9 +52,6 @@ func NewFromString(cfgStr string) *ghmock.ConfigMock {
 			},
 		}
 	}
-	mock.AccessibleColorsFunc = func(hostname string) gh.ConfigEntry {
-		return cfg.AccessibleColors(hostname)
-	}
 	mock.BrowserFunc = func(hostname string) gh.ConfigEntry {
 		return cfg.Browser(hostname)
 	}
diff --git a/internal/gh/gh.go b/internal/gh/gh.go
index 8f3e3cd5b..b17c6bd67 100644
--- a/internal/gh/gh.go
+++ b/internal/gh/gh.go
@@ -35,8 +35,6 @@ type Config interface {
 	// Set provides primitive access for setting configuration values, optionally scoped by host.
 	Set(hostname string, key string, value string)
 
-	// AccessibleColors returns the configured accessible_colors setting, optionally scoped by host.
-	AccessibleColors(hostname string) ConfigEntry
 	// Browser returns the configured browser, optionally scoped by host.
 	Browser(hostname string) ConfigEntry
 	// ColorLabels returns the configured color_label setting, optionally scoped by host.
diff --git a/internal/gh/mock/config.go b/internal/gh/mock/config.go
index 600eea5c1..b94cb084d 100644
--- a/internal/gh/mock/config.go
+++ b/internal/gh/mock/config.go
@@ -19,9 +19,6 @@ var _ gh.Config = &ConfigMock{}
 //
 //		// make and configure a mocked gh.Config
 //		mockedConfig := &ConfigMock{
-//			AccessibleColorsFunc: func(hostname string) gh.ConfigEntry {
-//				panic("mock out the AccessibleColors method")
-//			},
 //			AliasesFunc: func() gh.AliasConfig {
 //				panic("mock out the Aliases method")
 //			},
@@ -77,9 +74,6 @@ var _ gh.Config = &ConfigMock{}
 //
 //	}
 type ConfigMock struct {
-	// AccessibleColorsFunc mocks the AccessibleColors method.
-	AccessibleColorsFunc func(hostname string) gh.ConfigEntry
-
 	// AliasesFunc mocks the Aliases method.
 	AliasesFunc func() gh.AliasConfig
 
@@ -130,11 +124,6 @@ type ConfigMock struct {
 
 	// calls tracks calls to the methods.
 	calls struct {
-		// AccessibleColors holds details about calls to the AccessibleColors method.
-		AccessibleColors []struct {
-			// Hostname is the hostname argument value.
-			Hostname string
-		}
 		// Aliases holds details about calls to the Aliases method.
 		Aliases []struct {
 		}
@@ -212,7 +201,6 @@ type ConfigMock struct {
 		Write []struct {
 		}
 	}
-	lockAccessibleColors   sync.RWMutex
 	lockAliases            sync.RWMutex
 	lockAuthentication     sync.RWMutex
 	lockBrowser            sync.RWMutex
@@ -231,38 +219,6 @@ type ConfigMock struct {
 	lockWrite              sync.RWMutex
 }
 
-// AccessibleColors calls AccessibleColorsFunc.
-func (mock *ConfigMock) AccessibleColors(hostname string) gh.ConfigEntry {
-	if mock.AccessibleColorsFunc == nil {
-		panic("ConfigMock.AccessibleColorsFunc: method is nil but Config.AccessibleColors was just called")
-	}
-	callInfo := struct {
-		Hostname string
-	}{
-		Hostname: hostname,
-	}
-	mock.lockAccessibleColors.Lock()
-	mock.calls.AccessibleColors = append(mock.calls.AccessibleColors, callInfo)
-	mock.lockAccessibleColors.Unlock()
-	return mock.AccessibleColorsFunc(hostname)
-}
-
-// AccessibleColorsCalls gets all the calls that were made to AccessibleColors.
-// Check the length with:
-//
-//	len(mockedConfig.AccessibleColorsCalls())
-func (mock *ConfigMock) AccessibleColorsCalls() []struct {
-	Hostname string
-} {
-	var calls []struct {
-		Hostname string
-	}
-	mock.lockAccessibleColors.RLock()
-	calls = mock.calls.AccessibleColors
-	mock.lockAccessibleColors.RUnlock()
-	return calls
-}
-
 // Aliases calls AliasesFunc.
 func (mock *ConfigMock) Aliases() gh.AliasConfig {
 	if mock.AliasesFunc == nil {
diff --git a/internal/prompter/prompter_mock.go b/internal/prompter/prompter_mock.go
index b15f8bf96..b817a491f 100644
--- a/internal/prompter/prompter_mock.go
+++ b/internal/prompter/prompter_mock.go
@@ -20,28 +20,28 @@ var _ Prompter = &PrompterMock{}
 //			AuthTokenFunc: func() (string, error) {
 //				panic("mock out the AuthToken method")
 //			},
-//			ConfirmFunc: func(prompt string, defaultValue bool) (bool, error) {
+//			ConfirmFunc: func(s string, b bool) (bool, error) {
 //				panic("mock out the Confirm method")
 //			},
-//			ConfirmDeletionFunc: func(requiredValue string) error {
+//			ConfirmDeletionFunc: func(s string) error {
 //				panic("mock out the ConfirmDeletion method")
 //			},
-//			InputFunc: func(prompt string, defaultValue string) (string, error) {
+//			InputFunc: func(s1 string, s2 string) (string, error) {
 //				panic("mock out the Input method")
 //			},
 //			InputHostnameFunc: func() (string, error) {
 //				panic("mock out the InputHostname method")
 //			},
-//			MarkdownEditorFunc: func(prompt string, defaultValue string, blankAllowed bool) (string, error) {
+//			MarkdownEditorFunc: func(s1 string, s2 string, b bool) (string, error) {
 //				panic("mock out the MarkdownEditor method")
 //			},
 //			MultiSelectFunc: func(prompt string, defaults []string, options []string) ([]int, error) {
 //				panic("mock out the MultiSelect method")
 //			},
-//			PasswordFunc: func(prompt string) (string, error) {
+//			PasswordFunc: func(s string) (string, error) {
 //				panic("mock out the Password method")
 //			},
-//			SelectFunc: func(prompt string, defaultValue string, options []string) (int, error) {
+//			SelectFunc: func(s1 string, s2 string, strings []string) (int, error) {
 //				panic("mock out the Select method")
 //			},
 //		}
@@ -55,28 +55,28 @@ type PrompterMock struct {
 	AuthTokenFunc func() (string, error)
 
 	// ConfirmFunc mocks the Confirm method.
-	ConfirmFunc func(prompt string, defaultValue bool) (bool, error)
+	ConfirmFunc func(s string, b bool) (bool, error)
 
 	// ConfirmDeletionFunc mocks the ConfirmDeletion method.
-	ConfirmDeletionFunc func(requiredValue string) error
+	ConfirmDeletionFunc func(s string) error
 
 	// InputFunc mocks the Input method.
-	InputFunc func(prompt string, defaultValue string) (string, error)
+	InputFunc func(s1 string, s2 string) (string, error)
 
 	// InputHostnameFunc mocks the InputHostname method.
 	InputHostnameFunc func() (string, error)
 
 	// MarkdownEditorFunc mocks the MarkdownEditor method.
-	MarkdownEditorFunc func(prompt string, defaultValue string, blankAllowed bool) (string, error)
+	MarkdownEditorFunc func(s1 string, s2 string, b bool) (string, error)
 
 	// MultiSelectFunc mocks the MultiSelect method.
 	MultiSelectFunc func(prompt string, defaults []string, options []string) ([]int, error)
 
 	// PasswordFunc mocks the Password method.
-	PasswordFunc func(prompt string) (string, error)
+	PasswordFunc func(s string) (string, error)
 
 	// SelectFunc mocks the Select method.
-	SelectFunc func(prompt string, defaultValue string, options []string) (int, error)
+	SelectFunc func(s1 string, s2 string, strings []string) (int, error)
 
 	// calls tracks calls to the methods.
 	calls struct {
@@ -85,34 +85,34 @@ type PrompterMock struct {
 		}
 		// Confirm holds details about calls to the Confirm method.
 		Confirm []struct {
-			// Prompt is the prompt argument value.
-			Prompt string
-			// DefaultValue is the defaultValue argument value.
-			DefaultValue bool
+			// S is the s argument value.
+			S string
+			// B is the b argument value.
+			B bool
 		}
 		// ConfirmDeletion holds details about calls to the ConfirmDeletion method.
 		ConfirmDeletion []struct {
-			// RequiredValue is the requiredValue argument value.
-			RequiredValue string
+			// S is the s argument value.
+			S string
 		}
 		// Input holds details about calls to the Input method.
 		Input []struct {
-			// Prompt is the prompt argument value.
-			Prompt string
-			// DefaultValue is the defaultValue argument value.
-			DefaultValue string
+			// S1 is the s1 argument value.
+			S1 string
+			// S2 is the s2 argument value.
+			S2 string
 		}
 		// InputHostname holds details about calls to the InputHostname method.
 		InputHostname []struct {
 		}
 		// MarkdownEditor holds details about calls to the MarkdownEditor method.
 		MarkdownEditor []struct {
-			// Prompt is the prompt argument value.
-			Prompt string
-			// DefaultValue is the defaultValue argument value.
-			DefaultValue string
-			// BlankAllowed is the blankAllowed argument value.
-			BlankAllowed bool
+			// S1 is the s1 argument value.
+			S1 string
+			// S2 is the s2 argument value.
+			S2 string
+			// B is the b argument value.
+			B bool
 		}
 		// MultiSelect holds details about calls to the MultiSelect method.
 		MultiSelect []struct {
@@ -125,17 +125,17 @@ type PrompterMock struct {
 		}
 		// Password holds details about calls to the Password method.
 		Password []struct {
-			// Prompt is the prompt argument value.
-			Prompt string
+			// S is the s argument value.
+			S string
 		}
 		// Select holds details about calls to the Select method.
 		Select []struct {
-			// Prompt is the prompt argument value.
-			Prompt string
-			// DefaultValue is the defaultValue argument value.
-			DefaultValue string
-			// Options is the options argument value.
-			Options []string
+			// S1 is the s1 argument value.
+			S1 string
+			// S2 is the s2 argument value.
+			S2 string
+			// Strings is the strings argument value.
+			Strings []string
 		}
 	}
 	lockAuthToken       sync.RWMutex
@@ -177,21 +177,21 @@ func (mock *PrompterMock) AuthTokenCalls() []struct {
 }
 
 // Confirm calls ConfirmFunc.
-func (mock *PrompterMock) Confirm(prompt string, defaultValue bool) (bool, error) {
+func (mock *PrompterMock) Confirm(s string, b bool) (bool, error) {
 	if mock.ConfirmFunc == nil {
 		panic("PrompterMock.ConfirmFunc: method is nil but Prompter.Confirm was just called")
 	}
 	callInfo := struct {
-		Prompt       string
-		DefaultValue bool
+		S string
+		B bool
 	}{
-		Prompt:       prompt,
-		DefaultValue: defaultValue,
+		S: s,
+		B: b,
 	}
 	mock.lockConfirm.Lock()
 	mock.calls.Confirm = append(mock.calls.Confirm, callInfo)
 	mock.lockConfirm.Unlock()
-	return mock.ConfirmFunc(prompt, defaultValue)
+	return mock.ConfirmFunc(s, b)
 }
 
 // ConfirmCalls gets all the calls that were made to Confirm.
@@ -199,12 +199,12 @@ func (mock *PrompterMock) Confirm(prompt string, defaultValue bool) (bool, error
 //
 //	len(mockedPrompter.ConfirmCalls())
 func (mock *PrompterMock) ConfirmCalls() []struct {
-	Prompt       string
-	DefaultValue bool
+	S string
+	B bool
 } {
 	var calls []struct {
-		Prompt       string
-		DefaultValue bool
+		S string
+		B bool
 	}
 	mock.lockConfirm.RLock()
 	calls = mock.calls.Confirm
@@ -213,19 +213,19 @@ func (mock *PrompterMock) ConfirmCalls() []struct {
 }
 
 // ConfirmDeletion calls ConfirmDeletionFunc.
-func (mock *PrompterMock) ConfirmDeletion(requiredValue string) error {
+func (mock *PrompterMock) ConfirmDeletion(s string) error {
 	if mock.ConfirmDeletionFunc == nil {
 		panic("PrompterMock.ConfirmDeletionFunc: method is nil but Prompter.ConfirmDeletion was just called")
 	}
 	callInfo := struct {
-		RequiredValue string
+		S string
 	}{
-		RequiredValue: requiredValue,
+		S: s,
 	}
 	mock.lockConfirmDeletion.Lock()
 	mock.calls.ConfirmDeletion = append(mock.calls.ConfirmDeletion, callInfo)
 	mock.lockConfirmDeletion.Unlock()
-	return mock.ConfirmDeletionFunc(requiredValue)
+	return mock.ConfirmDeletionFunc(s)
 }
 
 // ConfirmDeletionCalls gets all the calls that were made to ConfirmDeletion.
@@ -233,10 +233,10 @@ func (mock *PrompterMock) ConfirmDeletion(requiredValue string) error {
 //
 //	len(mockedPrompter.ConfirmDeletionCalls())
 func (mock *PrompterMock) ConfirmDeletionCalls() []struct {
-	RequiredValue string
+	S string
 } {
 	var calls []struct {
-		RequiredValue string
+		S string
 	}
 	mock.lockConfirmDeletion.RLock()
 	calls = mock.calls.ConfirmDeletion
@@ -245,21 +245,21 @@ func (mock *PrompterMock) ConfirmDeletionCalls() []struct {
 }
 
 // Input calls InputFunc.
-func (mock *PrompterMock) Input(prompt string, defaultValue string) (string, error) {
+func (mock *PrompterMock) Input(s1 string, s2 string) (string, error) {
 	if mock.InputFunc == nil {
 		panic("PrompterMock.InputFunc: method is nil but Prompter.Input was just called")
 	}
 	callInfo := struct {
-		Prompt       string
-		DefaultValue string
+		S1 string
+		S2 string
 	}{
-		Prompt:       prompt,
-		DefaultValue: defaultValue,
+		S1: s1,
+		S2: s2,
 	}
 	mock.lockInput.Lock()
 	mock.calls.Input = append(mock.calls.Input, callInfo)
 	mock.lockInput.Unlock()
-	return mock.InputFunc(prompt, defaultValue)
+	return mock.InputFunc(s1, s2)
 }
 
 // InputCalls gets all the calls that were made to Input.
@@ -267,12 +267,12 @@ func (mock *PrompterMock) Input(prompt string, defaultValue string) (string, err
 //
 //	len(mockedPrompter.InputCalls())
 func (mock *PrompterMock) InputCalls() []struct {
-	Prompt       string
-	DefaultValue string
+	S1 string
+	S2 string
 } {
 	var calls []struct {
-		Prompt       string
-		DefaultValue string
+		S1 string
+		S2 string
 	}
 	mock.lockInput.RLock()
 	calls = mock.calls.Input
@@ -308,23 +308,23 @@ func (mock *PrompterMock) InputHostnameCalls() []struct {
 }
 
 // MarkdownEditor calls MarkdownEditorFunc.
-func (mock *PrompterMock) MarkdownEditor(prompt string, defaultValue string, blankAllowed bool) (string, error) {
+func (mock *PrompterMock) MarkdownEditor(s1 string, s2 string, b bool) (string, error) {
 	if mock.MarkdownEditorFunc == nil {
 		panic("PrompterMock.MarkdownEditorFunc: method is nil but Prompter.MarkdownEditor was just called")
 	}
 	callInfo := struct {
-		Prompt       string
-		DefaultValue string
-		BlankAllowed bool
+		S1 string
+		S2 string
+		B  bool
 	}{
-		Prompt:       prompt,
-		DefaultValue: defaultValue,
-		BlankAllowed: blankAllowed,
+		S1: s1,
+		S2: s2,
+		B:  b,
 	}
 	mock.lockMarkdownEditor.Lock()
 	mock.calls.MarkdownEditor = append(mock.calls.MarkdownEditor, callInfo)
 	mock.lockMarkdownEditor.Unlock()
-	return mock.MarkdownEditorFunc(prompt, defaultValue, blankAllowed)
+	return mock.MarkdownEditorFunc(s1, s2, b)
 }
 
 // MarkdownEditorCalls gets all the calls that were made to MarkdownEditor.
@@ -332,14 +332,14 @@ func (mock *PrompterMock) MarkdownEditor(prompt string, defaultValue string, bla
 //
 //	len(mockedPrompter.MarkdownEditorCalls())
 func (mock *PrompterMock) MarkdownEditorCalls() []struct {
-	Prompt       string
-	DefaultValue string
-	BlankAllowed bool
+	S1 string
+	S2 string
+	B  bool
 } {
 	var calls []struct {
-		Prompt       string
-		DefaultValue string
-		BlankAllowed bool
+		S1 string
+		S2 string
+		B  bool
 	}
 	mock.lockMarkdownEditor.RLock()
 	calls = mock.calls.MarkdownEditor
@@ -388,19 +388,19 @@ func (mock *PrompterMock) MultiSelectCalls() []struct {
 }
 
 // Password calls PasswordFunc.
-func (mock *PrompterMock) Password(prompt string) (string, error) {
+func (mock *PrompterMock) Password(s string) (string, error) {
 	if mock.PasswordFunc == nil {
 		panic("PrompterMock.PasswordFunc: method is nil but Prompter.Password was just called")
 	}
 	callInfo := struct {
-		Prompt string
+		S string
 	}{
-		Prompt: prompt,
+		S: s,
 	}
 	mock.lockPassword.Lock()
 	mock.calls.Password = append(mock.calls.Password, callInfo)
 	mock.lockPassword.Unlock()
-	return mock.PasswordFunc(prompt)
+	return mock.PasswordFunc(s)
 }
 
 // PasswordCalls gets all the calls that were made to Password.
@@ -408,10 +408,10 @@ func (mock *PrompterMock) Password(prompt string) (string, error) {
 //
 //	len(mockedPrompter.PasswordCalls())
 func (mock *PrompterMock) PasswordCalls() []struct {
-	Prompt string
+	S string
 } {
 	var calls []struct {
-		Prompt string
+		S string
 	}
 	mock.lockPassword.RLock()
 	calls = mock.calls.Password
@@ -420,23 +420,23 @@ func (mock *PrompterMock) PasswordCalls() []struct {
 }
 
 // Select calls SelectFunc.
-func (mock *PrompterMock) Select(prompt string, defaultValue string, options []string) (int, error) {
+func (mock *PrompterMock) Select(s1 string, s2 string, strings []string) (int, error) {
 	if mock.SelectFunc == nil {
 		panic("PrompterMock.SelectFunc: method is nil but Prompter.Select was just called")
 	}
 	callInfo := struct {
-		Prompt       string
-		DefaultValue string
-		Options      []string
+		S1      string
+		S2      string
+		Strings []string
 	}{
-		Prompt:       prompt,
-		DefaultValue: defaultValue,
-		Options:      options,
+		S1:      s1,
+		S2:      s2,
+		Strings: strings,
 	}
 	mock.lockSelect.Lock()
 	mock.calls.Select = append(mock.calls.Select, callInfo)
 	mock.lockSelect.Unlock()
-	return mock.SelectFunc(prompt, defaultValue, options)
+	return mock.SelectFunc(s1, s2, strings)
 }
 
 // SelectCalls gets all the calls that were made to Select.
@@ -444,14 +444,14 @@ func (mock *PrompterMock) Select(prompt string, defaultValue string, options []s
 //
 //	len(mockedPrompter.SelectCalls())
 func (mock *PrompterMock) SelectCalls() []struct {
-	Prompt       string
-	DefaultValue string
-	Options      []string
+	S1      string
+	S2      string
+	Strings []string
 } {
 	var calls []struct {
-		Prompt       string
-		DefaultValue string
-		Options      []string
+		S1      string
+		S2      string
+		Strings []string
 	}
 	mock.lockSelect.RLock()
 	calls = mock.calls.Select
diff --git a/pkg/cmd/issue/create/create.go b/pkg/cmd/issue/create/create.go
index 0976fd94f..2978a21fc 100644
--- a/pkg/cmd/issue/create/create.go
+++ b/pkg/cmd/issue/create/create.go
@@ -173,13 +173,13 @@ func createRun(opts *CreateOptions) (err error) {
 	}
 
 	tb := prShared.IssueMetadataState{
-		Type:       prShared.IssueMetadata,
-		Assignees:  assignees,
-		Labels:     opts.Labels,
-		Projects:   opts.Projects,
-		Milestones: milestones,
-		Title:      opts.Title,
-		Body:       opts.Body,
+		Type:          prShared.IssueMetadata,
+		Assignees:     assignees,
+		Labels:        opts.Labels,
+		ProjectTitles: opts.Projects,
+		Milestones:    milestones,
+		Title:         opts.Title,
+		Body:          opts.Body,
 	}
 
 	if opts.RecoverFile != "" {
@@ -195,7 +195,7 @@ func createRun(opts *CreateOptions) (err error) {
 	if opts.WebMode {
 		var openURL string
 		if opts.Title != "" || opts.Body != "" || tb.HasMetadata() {
-			openURL, err = generatePreviewURL(apiClient, baseRepo, tb)
+			openURL, err = generatePreviewURL(apiClient, baseRepo, tb, projectsV1Support)
 			if err != nil {
 				return
 			}
@@ -273,7 +273,7 @@ func createRun(opts *CreateOptions) (err error) {
 			}
 		}
 
-		openURL, err = generatePreviewURL(apiClient, baseRepo, tb)
+		openURL, err = generatePreviewURL(apiClient, baseRepo, tb, projectsV1Support)
 		if err != nil {
 			return
 		}
@@ -367,7 +367,7 @@ func createRun(opts *CreateOptions) (err error) {
 	return
 }
 
-func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState) (string, error) {
+func generatePreviewURL(apiClient *api.Client, baseRepo ghrepo.Interface, tb prShared.IssueMetadataState, projectsV1Support gh.ProjectsV1Support) (string, error) {
 	openURL := ghrepo.GenerateRepoURL(baseRepo, "issues/new")
-	return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb)
+	return prShared.WithPrAndIssueQueryParams(apiClient, baseRepo, openURL, tb, projectsV1Support)
 }
diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go
index 27c929dc4..7f960bce4 100644
--- a/pkg/cmd/pr/create/create.go
+++ b/pkg/cmd/pr/create/create.go
@@ -625,13 +625,13 @@ func NewIssueState(ctx CreateContext, opts CreateOptions) (*shared.IssueMetadata
 	}
 
 	state := &shared.IssueMetadataState{
-		Type:       shared.PRMetadata,
-		Reviewers:  opts.Reviewers,
-		Assignees:  assignees,
-		Labels:     opts.Labels,
-		Projects:   opts.Projects,
-		Milestones: milestoneTitles,
-		Draft:      opts.IsDraft,
+		Type:          shared.PRMetadata,
+		Reviewers:     opts.Reviewers,
+		Assignees:     assignees,
+		Labels:        opts.Labels,
+		ProjectTitles: opts.Projects,
+		Milestones:    milestoneTitles,
+		Draft:         opts.IsDraft,
 	}
 
 	if opts.FillVerbose || opts.Autofill || opts.FillFirst || !opts.TitleProvided || !opts.BodyProvided {
@@ -1032,8 +1032,8 @@ func renderPullRequestPlain(w io.Writer, params map[string]interface{}, state *s
 	if len(state.Milestones) != 0 {
 		fmt.Fprintf(w, "milestones:\t%v\n", strings.Join(state.Milestones, ", "))
 	}
-	if len(state.Projects) != 0 {
-		fmt.Fprintf(w, "projects:\t%v\n", strings.Join(state.Projects, ", "))
+	if len(state.ProjectTitles) != 0 {
+		fmt.Fprintf(w, "projects:\t%v\n", strings.Join(state.ProjectTitles, ", "))
 	}
 	fmt.Fprintf(w, "maintainerCanModify:\t%t\n", params["maintainerCanModify"])
 	fmt.Fprint(w, "body:\n")
@@ -1064,8 +1064,8 @@ func renderPullRequestTTY(io *iostreams.IOStreams, params map[string]interface{}
 	if len(state.Milestones) != 0 {
 		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Milestones"), strings.Join(state.Milestones, ", "))
 	}
-	if len(state.Projects) != 0 {
-		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), strings.Join(state.Projects, ", "))
+	if len(state.ProjectTitles) != 0 {
+		fmt.Fprintf(out, "%s: %s\n", cs.Bold("Projects"), strings.Join(state.ProjectTitles, ", "))
 	}
 	fmt.Fprintf(out, "%s: %t\n", cs.Bold("MaintainerCanModify"), params["maintainerCanModify"])
 
@@ -1221,7 +1221,8 @@ func generateCompareURL(ctx CreateContext, state shared.IssueMetadataState) (str
 		ctx.PRRefs.BaseRepo(),
 		"compare/%s...%s?expand=1",
 		url.PathEscape(ctx.PRRefs.BaseRef()), url.PathEscape(ctx.PRRefs.QualifiedHeadRef()))
-	url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.PRRefs.BaseRepo(), u, state)
+	// TODO wm: revisit project support
+	url, err := shared.WithPrAndIssueQueryParams(ctx.Client, ctx.PRRefs.BaseRepo(), u, state, gh.ProjectsV1Supported)
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go
index 9688a2aca..4f36a80aa 100644
--- a/pkg/cmd/pr/shared/params.go
+++ b/pkg/cmd/pr/shared/params.go
@@ -12,7 +12,7 @@ import (
 	"github.com/google/shlex"
 )
 
-func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState) (string, error) {
+func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, baseURL string, state IssueMetadataState, projectsV1Support gh.ProjectsV1Support) (string, error) {
 	u, err := url.Parse(baseURL)
 	if err != nil {
 		return "", err
@@ -35,8 +35,8 @@ func WithPrAndIssueQueryParams(client *api.Client, baseRepo ghrepo.Interface, ba
 	if len(state.Labels) > 0 {
 		q.Set("labels", strings.Join(state.Labels, ","))
 	}
-	if len(state.Projects) > 0 {
-		projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.Projects)
+	if len(state.ProjectTitles) > 0 {
+		projectPaths, err := api.ProjectNamesToPaths(client, baseRepo, state.ProjectTitles, projectsV1Support)
 		if err != nil {
 			return "", fmt.Errorf("could not add to project: %w", err)
 		}
@@ -72,7 +72,7 @@ func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetada
 		resolveInput.Labels = tb.Labels
 	}
 
-	if len(tb.Projects) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) {
+	if len(tb.ProjectTitles) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) {
 		if projectV1Support == gh.ProjectsV1Supported {
 			resolveInput.ProjectsV1 = true
 		}
@@ -119,7 +119,7 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par
 	}
 	params["labelIds"] = labelIDs
 
-	projectIDs, projectV2IDs, err := tb.MetadataResult.ProjectsToIDs(tb.Projects)
+	projectIDs, projectV2IDs, err := tb.MetadataResult.ProjectsToIDs(tb.ProjectTitles)
 	if err != nil {
 		return fmt.Errorf("could not add to project: %w", err)
 	}
diff --git a/pkg/cmd/pr/shared/state.go b/pkg/cmd/pr/shared/state.go
index 143021cb6..7e7da436d 100644
--- a/pkg/cmd/pr/shared/state.go
+++ b/pkg/cmd/pr/shared/state.go
@@ -25,12 +25,12 @@ type IssueMetadataState struct {
 
 	Template string
 
-	Metadata   []string
-	Reviewers  []string
-	Assignees  []string
-	Labels     []string
-	Projects   []string
-	Milestones []string
+	Metadata      []string
+	Reviewers     []string
+	Assignees     []string
+	Labels        []string
+	ProjectTitles []string
+	Milestones    []string
 
 	MetadataResult *api.RepoMetadataResult
 
@@ -49,7 +49,7 @@ func (tb *IssueMetadataState) HasMetadata() bool {
 	return len(tb.Reviewers) > 0 ||
 		len(tb.Assignees) > 0 ||
 		len(tb.Labels) > 0 ||
-		len(tb.Projects) > 0 ||
+		len(tb.ProjectTitles) > 0 ||
 		len(tb.Milestones) > 0
 }
 
diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go
index 23079a391..bf4476ca1 100644
--- a/pkg/cmd/pr/shared/survey.go
+++ b/pkg/cmd/pr/shared/survey.go
@@ -268,7 +268,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
 	}
 	if isChosen("Projects") {
 		if len(projects) > 0 {
-			selected, err := p.MultiSelect("Projects", state.Projects, projects)
+			selected, err := p.MultiSelect("Projects", state.ProjectTitles, projects)
 			if err != nil {
 				return err
 			}
@@ -317,7 +317,7 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
 		state.Labels = values.Labels
 	}
 	if isChosen("Projects") {
-		state.Projects = values.Projects
+		state.ProjectTitles = values.Projects
 	}
 	if isChosen("Milestone") {
 		if values.Milestone != "" && values.Milestone != noMilestone {
diff --git a/pkg/cmd/root/help_topic.go b/pkg/cmd/root/help_topic.go
index 0c9534306..4b692777c 100644
--- a/pkg/cmd/root/help_topic.go
+++ b/pkg/cmd/root/help_topic.go
@@ -84,8 +84,6 @@ var HelpTopics = []helpTopic{
 			%[1]sGH_COLOR_LABELS%[1]s: set to any value to display labels using their RGB hex color codes in terminals that
 			support truecolor.
 
-			%[1]sGH_ACCESSIBLE_COLORS%[1]s (preview): set to a truthy value to use customizable, 4-bit accessible colors.
-
 			%[1]sGH_FORCE_TTY%[1]s: set to any value to force terminal-style output even when the output is
 			redirected. When the value is a number, it is interpreted as the number of columns
 			available in the viewport. When the value is a percentage, it will be applied against


Test patch:
diff --git a/api/queries_repo_test.go b/api/queries_repo_test.go
index 5f5f55b67..72ed35776 100644
--- a/api/queries_repo_test.go
+++ b/api/queries_repo_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/stretchr/testify/assert"
@@ -212,37 +213,16 @@ func Test_RepoMetadata(t *testing.T) {
 	}
 }
 
-func Test_ProjectsToPaths(t *testing.T) {
-	expectedProjectPaths := []string{"OWNER/REPO/PROJECT_NUMBER", "ORG/PROJECT_NUMBER", "OWNER/REPO/PROJECT_NUMBER_2"}
-	projects := []RepoProject{
-		{ID: "id1", Name: "My Project", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER"},
-		{ID: "id2", Name: "Org Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER"},
-		{ID: "id3", Name: "Project", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_2"},
-	}
-	projectsV2 := []ProjectV2{
-		{ID: "id4", Title: "My Project V2", ResourcePath: "/OWNER/REPO/projects/PROJECT_NUMBER_2"},
-		{ID: "id5", Title: "Org Project V2", ResourcePath: "/orgs/ORG/projects/PROJECT_NUMBER_3"},
-	}
-	projectNames := []string{"My Project", "Org Project", "My Project V2"}
-
-	projectPaths, err := ProjectsToPaths(projects, projectsV2, projectNames)
-	if err != nil {
-		t.Errorf("error resolving projects: %v", err)
-	}
-	if !sliceEqual(projectPaths, expectedProjectPaths) {
-		t.Errorf("expected projects %v, got %v", expectedProjectPaths, projectPaths)
-	}
-}
-
 func Test_ProjectNamesToPaths(t *testing.T) {
-	http := &httpmock.Registry{}
-	client := newTestClient(http)
+	t.Run("when projectsV1 is supported, requests them", func(t *testing.T) {
+		http := &httpmock.Registry{}
+		client := newTestClient(http)
 
-	repo, _ := ghrepo.FromFullName("OWNER/REPO")
+		repo, _ := ghrepo.FromFullName("OWNER/REPO")
 
-	http.Register(
-		httpmock.GraphQL(`query RepositoryProjectList\b`),
-		httpmock.StringResponse(`
+		http.Register(
+			httpmock.GraphQL(`query RepositoryProjectList\b`),
+			httpmock.StringResponse(`
 		{ "data": { "repository": { "projects": {
 			"nodes": [
 				{ "name": "Cleanup", "id": "CLEANUPID", "resourcePath": "/OWNER/REPO/projects/1" },
@@ -251,9 +231,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 			"pageInfo": { "hasNextPage": false }
 		} } } }
 		`))
-	http.Register(
-		httpmock.GraphQL(`query OrganizationProjectList\b`),
-		httpmock.StringResponse(`
+		http.Register(
+			httpmock.GraphQL(`query OrganizationProjectList\b`),
+			httpmock.StringResponse(`
 		{ "data": { "organization": { "projects": {
 			"nodes": [
 				{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1"  }
@@ -261,9 +241,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 			"pageInfo": { "hasNextPage": false }
 		} } } }
 		`))
-	http.Register(
-		httpmock.GraphQL(`query RepositoryProjectV2List\b`),
-		httpmock.StringResponse(`
+		http.Register(
+			httpmock.GraphQL(`query RepositoryProjectV2List\b`),
+			httpmock.StringResponse(`
 		{ "data": { "repository": { "projectsV2": {
 			"nodes": [
 				{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
@@ -272,9 +252,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 			"pageInfo": { "hasNextPage": false }
 		} } } }
 		`))
-	http.Register(
-		httpmock.GraphQL(`query OrganizationProjectV2List\b`),
-		httpmock.StringResponse(`
+		http.Register(
+			httpmock.GraphQL(`query OrganizationProjectV2List\b`),
+			httpmock.StringResponse(`
 		{ "data": { "organization": { "projectsV2": {
 			"nodes": [
 				{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2"  }
@@ -282,9 +262,9 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 			"pageInfo": { "hasNextPage": false }
 		} } } }
 		`))
-	http.Register(
-		httpmock.GraphQL(`query UserProjectV2List\b`),
-		httpmock.StringResponse(`
+		http.Register(
+			httpmock.GraphQL(`query UserProjectV2List\b`),
+			httpmock.StringResponse(`
 		{ "data": { "viewer": { "projectsV2": {
 			"nodes": [
 				{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/users/MONALISA/projects/5"  }
@@ -293,15 +273,110 @@ func Test_ProjectNamesToPaths(t *testing.T) {
 		} } } }
 		`))
 
-	projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"})
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
-	}
+		projectPaths, err := ProjectNamesToPaths(client, repo, []string{"Triage", "Roadmap", "TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Supported)
+		if err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
 
-	expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2", "ORG/2", "OWNER/REPO/4", "MONALISA/5"}
-	if !sliceEqual(projectPaths, expectedProjectPaths) {
-		t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
-	}
+		expectedProjectPaths := []string{"ORG/1", "OWNER/REPO/2", "ORG/2", "OWNER/REPO/4", "MONALISA/5"}
+		if !sliceEqual(projectPaths, expectedProjectPaths) {
+			t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
+		}
+	})
+
+	t.Run("when projectsV1 is not supported, does not request them", func(t *testing.T) {
+		http := &httpmock.Registry{}
+		client := newTestClient(http)
+
+		repo, _ := ghrepo.FromFullName("OWNER/REPO")
+
+		http.Exclude(
+			t,
+			httpmock.GraphQL(`query RepositoryProjectList\b`),
+		)
+		http.Exclude(
+			t,
+			httpmock.GraphQL(`query OrganizationProjectList\b`),
+		)
+
+		http.Register(
+			httpmock.GraphQL(`query RepositoryProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "repository": { "projectsV2": {
+			"nodes": [
+				{ "title": "CleanupV2", "id": "CLEANUPV2ID", "resourcePath": "/OWNER/REPO/projects/3" },
+				{ "title": "RoadmapV2", "id": "ROADMAPV2ID", "resourcePath": "/OWNER/REPO/projects/4" }
+			],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		http.Register(
+			httpmock.GraphQL(`query OrganizationProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "organization": { "projectsV2": {
+			"nodes": [
+				{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2"  }
+			],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		http.Register(
+			httpmock.GraphQL(`query UserProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "viewer": { "projectsV2": {
+			"nodes": [
+				{ "title": "MonalisaV2", "id": "MONALISAV2ID", "resourcePath": "/users/MONALISA/projects/5"  }
+			],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+
+		projectPaths, err := ProjectNamesToPaths(client, repo, []string{"TriageV2", "RoadmapV2", "MonalisaV2"}, gh.ProjectsV1Unsupported)
+		if err != nil {
+			t.Fatalf("unexpected error: %v", err)
+		}
+
+		expectedProjectPaths := []string{"ORG/2", "OWNER/REPO/4", "MONALISA/5"}
+		if !sliceEqual(projectPaths, expectedProjectPaths) {
+			t.Errorf("expected projects paths %v, got %v", expectedProjectPaths, projectPaths)
+		}
+	})
+
+	t.Run("when a project is not found, returns an error", func(t *testing.T) {
+		http := &httpmock.Registry{}
+		client := newTestClient(http)
+
+		repo, _ := ghrepo.FromFullName("OWNER/REPO")
+
+		// No projects found
+		http.Register(
+			httpmock.GraphQL(`query RepositoryProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "repository": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		http.Register(
+			httpmock.GraphQL(`query OrganizationProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "organization": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		http.Register(
+			httpmock.GraphQL(`query UserProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "viewer": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+
+		_, err := ProjectNamesToPaths(client, repo, []string{"TriageV2"}, gh.ProjectsV1Unsupported)
+		require.Equal(t, err, fmt.Errorf("'TriageV2' not found"))
+	})
 }
 
 func Test_RepoResolveMetadataIDs(t *testing.T) {
diff --git a/pkg/cmd/config/list/list_test.go b/pkg/cmd/config/list/list_test.go
index 2a1dd72ee..2184d0f16 100644
--- a/pkg/cmd/config/list/list_test.go
+++ b/pkg/cmd/config/list/list_test.go
@@ -101,7 +101,6 @@ func Test_listRun(t *testing.T) {
 				http_unix_socket=
 				browser=brave
 				color_labels=disabled
-				accessible_colors=disabled
 			`),
 		},
 	}
diff --git a/pkg/cmd/issue/create/create_test.go b/pkg/cmd/issue/create/create_test.go
index d8c1c5d92..1211c0c1d 100644
--- a/pkg/cmd/issue/create/create_test.go
+++ b/pkg/cmd/issue/create/create_test.go
@@ -474,6 +474,7 @@ func Test_createRun(t *testing.T) {
 			opts.BaseRepo = func() (ghrepo.Interface, error) {
 				return ghrepo.New("OWNER", "REPO"), nil
 			}
+			opts.Detector = &fd.EnabledDetectorMock{}
 			browser := &browser.Stub{}
 			opts.Browser = browser
 
@@ -1103,4 +1104,71 @@ func TestProjectsV1Deprecation(t *testing.T) {
 			reg.Verify(t)
 		})
 	})
+
+	t.Run("web mode", func(t *testing.T) {
+		t.Run("when projects v1 is supported, queries for it", func(t *testing.T) {
+			ios, _, _, _ := iostreams.Test()
+
+			reg := &httpmock.Registry{}
+			reg.Register(
+				// ( is required to avoid matching projectsV2
+				httpmock.GraphQL(`projects\(`),
+				// Simulate a GraphQL error to early exit the test.
+				httpmock.StatusStringResponse(500, ""),
+			)
+
+			_, cmdTeardown := run.Stub()
+			defer cmdTeardown(t)
+
+			// Ignore the error because we have no way to really stub it without
+			// fully stubbing a GQL error structure in the request body.
+			_ = createRun(&CreateOptions{
+				IO: ios,
+				HttpClient: func() (*http.Client, error) {
+					return &http.Client{Transport: reg}, nil
+				},
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+
+				Detector: &fd.EnabledDetectorMock{},
+				WebMode:  true,
+				// Required to force a lookup of projects
+				Projects: []string{"Project"},
+			})
+
+			// Verify that our request contained projects
+			reg.Verify(t)
+		})
+
+		t.Run("when projects v1 is not supported, does not query for it", func(t *testing.T) {
+			ios, _, _, _ := iostreams.Test()
+
+			reg := &httpmock.Registry{}
+			// ( is required to avoid matching projectsV2
+			reg.Exclude(t, httpmock.GraphQL(`projects\(`))
+
+			_, cmdTeardown := run.Stub()
+			defer cmdTeardown(t)
+
+			// Ignore the error because we're not really interested in it.
+			_ = createRun(&CreateOptions{
+				IO: ios,
+				HttpClient: func() (*http.Client, error) {
+					return &http.Client{Transport: reg}, nil
+				},
+				BaseRepo: func() (ghrepo.Interface, error) {
+					return ghrepo.New("OWNER", "REPO"), nil
+				},
+
+				Detector: &fd.DisabledDetectorMock{},
+				WebMode:  true,
+				// Required to force a lookup of projects
+				Projects: []string{"Project"},
+			})
+
+			// Verify that our request contained projectCards
+			reg.Verify(t)
+		})
+	})
 }
diff --git a/pkg/cmd/pr/shared/params_test.go b/pkg/cmd/pr/shared/params_test.go
index 5f5e674cc..15f00ca4f 100644
--- a/pkg/cmd/pr/shared/params_test.go
+++ b/pkg/cmd/pr/shared/params_test.go
@@ -2,13 +2,16 @@ package shared
 
 import (
 	"net/http"
+	"net/url"
 	"reflect"
 	"testing"
 
 	"github.com/cli/cli/v2/api"
+	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func Test_listURLWithQuery(t *testing.T) {
@@ -265,7 +268,7 @@ func Test_WithPrAndIssueQueryParams(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := WithPrAndIssueQueryParams(nil, nil, tt.args.baseURL, tt.args.state)
+			got, err := WithPrAndIssueQueryParams(nil, nil, tt.args.baseURL, tt.args.state, gh.ProjectsV1Supported)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("WithPrAndIssueQueryParams() error = %v, wantErr %v", err, tt.wantErr)
 				return
@@ -276,3 +279,144 @@ func Test_WithPrAndIssueQueryParams(t *testing.T) {
 		})
 	}
 }
+
+// TODO projectsV1Deprecation
+// Remove this test.
+func TestWithPrAndIssueQueryParamsProjectsV1Deprecation(t *testing.T) {
+	t.Run("when projectsV1 is supported, requests them", func(t *testing.T) {
+		reg := &httpmock.Registry{}
+		client := api.NewClientFromHTTP(&http.Client{
+			Transport: reg,
+		})
+
+		repo, _ := ghrepo.FromFullName("OWNER/REPO")
+
+		reg.Register(
+			httpmock.GraphQL(`query RepositoryProjectList\b`),
+			httpmock.StringResponse(`
+		{ "data": { "repository": { "projects": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		reg.Register(
+			httpmock.GraphQL(`query OrganizationProjectList\b`),
+			httpmock.StringResponse(`
+		{ "data": { "organization": { "projects": {
+			"nodes": [
+				{ "name": "Triage", "id": "TRIAGEID", "resourcePath": "/orgs/ORG/projects/1"  }
+			],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		reg.Register(
+			httpmock.GraphQL(`query RepositoryProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "repository": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		reg.Register(
+			httpmock.GraphQL(`query OrganizationProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "organization": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		reg.Register(
+			httpmock.GraphQL(`query UserProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "viewer": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+
+		u, err := WithPrAndIssueQueryParams(
+			client,
+			repo,
+			"http://example.com/hey",
+			IssueMetadataState{
+				ProjectTitles: []string{"Triage"},
+			},
+			gh.ProjectsV1Supported,
+		)
+		require.NoError(t, err)
+
+		url, err := url.Parse(u)
+		require.NoError(t, err)
+
+		require.Equal(
+			t,
+			url.Query().Get("projects"),
+			"ORG/1",
+		)
+	})
+
+	t.Run("when projectsV1 is not supported, does not request them", func(t *testing.T) {
+		reg := &httpmock.Registry{}
+		client := api.NewClientFromHTTP(&http.Client{
+			Transport: reg,
+		})
+
+		repo, _ := ghrepo.FromFullName("OWNER/REPO")
+
+		reg.Exclude(
+			t,
+			httpmock.GraphQL(`query RepositoryProjectList\b`),
+		)
+		reg.Exclude(
+			t,
+			httpmock.GraphQL(`query OrganizationProjectList\b`),
+		)
+
+		reg.Register(
+			httpmock.GraphQL(`query RepositoryProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "repository": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		reg.Register(
+			httpmock.GraphQL(`query OrganizationProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "organization": { "projectsV2": {
+			"nodes": [
+				{ "title": "TriageV2", "id": "TRIAGEV2ID", "resourcePath": "/orgs/ORG/projects/2"  }
+			],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+		reg.Register(
+			httpmock.GraphQL(`query UserProjectV2List\b`),
+			httpmock.StringResponse(`
+		{ "data": { "viewer": { "projectsV2": {
+			"nodes": [],
+			"pageInfo": { "hasNextPage": false }
+		} } } }
+		`))
+
+		u, err := WithPrAndIssueQueryParams(
+			client,
+			repo,
+			"http://example.com/hey",
+			IssueMetadataState{
+				ProjectTitles: []string{"TriageV2"},
+			},
+			gh.ProjectsV1Unsupported,
+		)
+		require.NoError(t, err)
+
+		url, err := url.Parse(u)
+		require.NoError(t, err)
+
+		require.Equal(
+			t,
+			url.Query().Get("projects"),
+			"ORG/2",
+		)
+	})
+}
diff --git a/pkg/cmd/pr/shared/survey_test.go b/pkg/cmd/pr/shared/survey_test.go
index c094b7556..6895b52ac 100644
--- a/pkg/cmd/pr/shared/survey_test.go
+++ b/pkg/cmd/pr/shared/survey_test.go
@@ -80,7 +80,7 @@ func TestMetadataSurvey_selectAll(t *testing.T) {
 	assert.Equal(t, []string{"hubot"}, state.Assignees)
 	assert.Equal(t, []string{"monalisa"}, state.Reviewers)
 	assert.Equal(t, []string{"good first issue"}, state.Labels)
-	assert.Equal(t, []string{"The road to 1.0"}, state.Projects)
+	assert.Equal(t, []string{"The road to 1.0"}, state.ProjectTitles)
 	assert.Equal(t, []string{}, state.Milestones)
 }
 
@@ -125,7 +125,7 @@ func TestMetadataSurvey_keepExisting(t *testing.T) {
 
 	assert.Equal(t, []string{"hubot"}, state.Assignees)
 	assert.Equal(t, []string{"good first issue"}, state.Labels)
-	assert.Equal(t, []string{"The road to 1.0"}, state.Projects)
+	assert.Equal(t, []string{"The road to 1.0"}, state.ProjectTitles)
 }
 
 // TODO projectsV1Deprecation