Repository: cli/cli
Pull request #10815 (2025-04-17 15:35:25)

Feature detect v1 projects on non web-mode `issue create`
## Description

Relates to: https://github.com/cli/cli/issues/10714
Builds on: https://github.com/cli/cli/pull/10813

This PR tackles projectv1 deprecation on issue creation that isn't `web mode` i.e. where:
 * The user has provided `--project`
 * The user has selected projects from the `Add Metadata > Projects` multiselect

### 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 "fake project" 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 53e6d879a..7c33825b0 100644
--- a/api/queries_repo.go
+++ b/api/queries_repo.go
@@ -863,7 +863,8 @@ type RepoMetadataInput struct {
 	Assignees  bool
 	Reviewers  bool
 	Labels     bool
-	Projects   bool
+	ProjectsV1 bool
+	ProjectsV2 bool
 	Milestones bool
 }
 
@@ -882,6 +883,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
 			return err
 		})
 	}
+
 	if input.Reviewers {
 		g.Go(func() error {
 			teams, err := OrganizationTeams(client, repo)
@@ -894,6 +896,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
 			return nil
 		})
 	}
+
 	if input.Reviewers {
 		g.Go(func() error {
 			login, err := CurrentLoginName(client, repo.RepoHost())
@@ -904,6 +907,7 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
 			return err
 		})
 	}
+
 	if input.Labels {
 		g.Go(func() error {
 			labels, err := RepoLabels(client, repo)
@@ -914,13 +918,23 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
 			return err
 		})
 	}
-	if input.Projects {
+
+	if input.ProjectsV1 {
+		g.Go(func() error {
+			var err error
+			result.Projects, err = v1Projects(client, repo)
+			return err
+		})
+	}
+
+	if input.ProjectsV2 {
 		g.Go(func() error {
 			var err error
-			result.Projects, result.ProjectsV2, err = relevantProjects(client, repo)
+			result.ProjectsV2, err = v2Projects(client, repo)
 			return err
 		})
 	}
+
 	if input.Milestones {
 		g.Go(func() error {
 			milestones, err := RepoMilestones(client, repo, "open")
@@ -943,7 +957,8 @@ type RepoResolveInput struct {
 	Assignees  []string
 	Reviewers  []string
 	Labels     []string
-	Projects   []string
+	ProjectsV1 bool
+	ProjectsV2 bool
 	Milestones []string
 }
 
@@ -970,7 +985,8 @@ func RepoResolveMetadataIDs(client *Client, repo ghrepo.Interface, input RepoRes
 
 	// there is no way to look up projects nor milestones by name, so preload them all
 	mi := RepoMetadataInput{
-		Projects:   len(input.Projects) > 0,
+		ProjectsV1: input.ProjectsV1,
+		ProjectsV2: input.ProjectsV2,
 		Milestones: len(input.Milestones) > 0,
 	}
 	result, err := RepoMetadata(client, repo, mi)
@@ -1245,18 +1261,12 @@ func ProjectNamesToPaths(client *Client, repo ghrepo.Interface, projectNames []s
 	return ProjectsToPaths(projects, projectsV2, projectNames)
 }
 
-// RelevantProjects retrieves set of Projects and ProjectsV2 relevant to given repository:
+// v1Projects retrieves set of RepoProjects 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) {
+func v1Projects(client *Client, repo ghrepo.Interface) ([]RepoProject, error) {
 	var repoProjects []RepoProject
 	var orgProjects []RepoProject
-	var userProjectsV2 []ProjectV2
-	var repoProjectsV2 []ProjectV2
-	var orgProjectsV2 []ProjectV2
 
 	g, _ := errgroup.WithContext(context.Background())
 
@@ -1268,6 +1278,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
 		}
 		return err
 	})
+
 	g.Go(func() error {
 		var err error
 		orgProjects, err = OrganizationProjects(client, repo)
@@ -1277,6 +1288,29 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
 		}
 		return nil
 	})
+
+	if err := g.Wait(); err != nil {
+		return nil, err
+	}
+
+	projects := make([]RepoProject, 0, len(repoProjects)+len(orgProjects))
+	projects = append(projects, repoProjects...)
+	projects = append(projects, orgProjects...)
+
+	return projects, nil
+}
+
+// v2Projects retrieves set of ProjectV2 relevant to given repository:
+// - ProjectsV2 owned by current user
+// - ProjectsV2 linked to repository
+// - ProjectsV2 owned by repository organization, if it belongs to one
+func v2Projects(client *Client, repo ghrepo.Interface) ([]ProjectV2, error) {
+	var userProjectsV2 []ProjectV2
+	var repoProjectsV2 []ProjectV2
+	var orgProjectsV2 []ProjectV2
+
+	g, _ := errgroup.WithContext(context.Background())
+
 	g.Go(func() error {
 		var err error
 		userProjectsV2, err = CurrentUserProjectsV2(client, repo.RepoHost())
@@ -1286,6 +1320,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
 		}
 		return nil
 	})
+
 	g.Go(func() error {
 		var err error
 		repoProjectsV2, err = RepoProjectsV2(client, repo)
@@ -1295,6 +1330,7 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
 		}
 		return nil
 	})
+
 	g.Go(func() error {
 		var err error
 		orgProjectsV2, err = OrganizationProjectsV2(client, repo)
@@ -1308,13 +1344,9 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
 	})
 
 	if err := g.Wait(); err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
-	projects := make([]RepoProject, 0, len(repoProjects)+len(orgProjects))
-	projects = append(projects, repoProjects...)
-	projects = append(projects, orgProjects...)
-
 	// ProjectV2 might appear across multiple queries so use a map to keep them deduplicated.
 	m := make(map[string]ProjectV2, len(userProjectsV2)+len(repoProjectsV2)+len(orgProjectsV2))
 	for _, p := range userProjectsV2 {
@@ -1331,7 +1363,27 @@ func relevantProjects(client *Client, repo ghrepo.Interface) ([]RepoProject, []P
 		projectsV2 = append(projectsV2, p)
 	}
 
-	return projects, projectsV2, nil
+	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) {
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 2e3e0de51..0976fd94f 100644
--- a/pkg/cmd/issue/create/create.go
+++ b/pkg/cmd/issue/create/create.go
@@ -4,10 +4,12 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"time"
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/browser"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/text"
@@ -24,6 +26,7 @@ type CreateOptions struct {
 	BaseRepo         func() (ghrepo.Interface, error)
 	Browser          browser.Browser
 	Prompter         prShared.Prompt
+	Detector         fd.Detector
 	TitledEditSurvey func(string, string) (string, string, error)
 
 	RootDirOverride string
@@ -46,11 +49,12 @@ type CreateOptions struct {
 
 func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command {
 	opts := &CreateOptions{
-		IO:               f.IOStreams,
-		HttpClient:       f.HttpClient,
-		Config:           f.Config,
-		Browser:          f.Browser,
-		Prompter:         f.Prompter,
+		IO:         f.IOStreams,
+		HttpClient: f.HttpClient,
+		Config:     f.Config,
+		Browser:    f.Browser,
+		Prompter:   f.Prompter,
+
 		TitledEditSurvey: prShared.TitledEditSurvey(&prShared.UserEditor{Config: f.Config, IO: f.IOStreams}),
 	}
 
@@ -146,6 +150,15 @@ func createRun(opts *CreateOptions) (err error) {
 		return
 	}
 
+	// TODO projectsV1Deprecation
+	// Remove this section as we should no longer need to detect
+	if opts.Detector == nil {
+		cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
+		opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
+	}
+
+	projectsV1Support := opts.Detector.ProjectsV1()
+
 	isTerminal := opts.IO.IsStdoutTTY()
 
 	var milestones []string
@@ -279,7 +292,7 @@ func createRun(opts *CreateOptions) (err error) {
 				Repo:      baseRepo,
 				State:     &tb,
 			}
-			err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb)
+			err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb, projectsV1Support)
 			if err != nil {
 				return
 			}
@@ -335,7 +348,7 @@ func createRun(opts *CreateOptions) (err error) {
 			params["issueTemplate"] = templateNameForSubmit
 		}
 
-		err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb)
+		err = prShared.AddMetadataToIssueParams(apiClient, baseRepo, params, &tb, projectsV1Support)
 		if err != nil {
 			return
 		}
diff --git a/pkg/cmd/pr/create/create.go b/pkg/cmd/pr/create/create.go
index eda7a3ce7..27c929dc4 100644
--- a/pkg/cmd/pr/create/create.go
+++ b/pkg/cmd/pr/create/create.go
@@ -440,7 +440,8 @@ func createRun(opts *CreateOptions) error {
 		if err != nil {
 			return err
 		}
-		return submitPR(*opts, *ctx, *state)
+		// TODO wm: revisit project support
+		return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
 	}
 
 	if opts.RecoverFile != "" {
@@ -536,7 +537,8 @@ func createRun(opts *CreateOptions) error {
 				Repo:      ctx.PRRefs.BaseRepo(),
 				State:     state,
 			}
-			err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.PRRefs.BaseRepo(), fetcher, state)
+			// TODO wm: revisit project support
+			err = shared.MetadataSurvey(opts.Prompter, opts.IO, ctx.PRRefs.BaseRepo(), fetcher, state, gh.ProjectsV1Supported)
 			if err != nil {
 				return err
 			}
@@ -565,11 +567,13 @@ func createRun(opts *CreateOptions) error {
 
 	if action == shared.SubmitDraftAction {
 		state.Draft = true
-		return submitPR(*opts, *ctx, *state)
+		// TODO wm: revisit project support
+		return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
 	}
 
 	if action == shared.SubmitAction {
-		return submitPR(*opts, *ctx, *state)
+		// TODO wm: revisit project support
+		return submitPR(*opts, *ctx, *state, gh.ProjectsV1Supported)
 	}
 
 	err = errors.New("expected to cancel, preview, or submit")
@@ -966,7 +970,7 @@ func getRemotes(opts *CreateOptions) (ghContext.Remotes, error) {
 	return remotes, nil
 }
 
-func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataState) error {
+func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataState, projectV1Support gh.ProjectsV1Support) error {
 	client := ctx.Client
 
 	params := map[string]interface{}{
@@ -982,7 +986,7 @@ func submitPR(opts CreateOptions, ctx CreateContext, state shared.IssueMetadataS
 		return errors.New("pull request title must not be blank")
 	}
 
-	err := shared.AddMetadataToIssueParams(client, ctx.PRRefs.BaseRepo(), params, &state)
+	err := shared.AddMetadataToIssueParams(client, ctx.PRRefs.BaseRepo(), params, &state, projectV1Support)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/cmd/pr/shared/editable.go b/pkg/cmd/pr/shared/editable.go
index cec3bfe8c..0bebb999a 100644
--- a/pkg/cmd/pr/shared/editable.go
+++ b/pkg/cmd/pr/shared/editable.go
@@ -381,7 +381,8 @@ func FetchOptions(client *api.Client, repo ghrepo.Interface, editable *Editable)
 		Reviewers:  editable.Reviewers.Edited,
 		Assignees:  editable.Assignees.Edited,
 		Labels:     editable.Labels.Edited,
-		Projects:   editable.Projects.Edited,
+		ProjectsV1: editable.Projects.Edited,
+		ProjectsV2: editable.Projects.Edited,
 		Milestones: editable.Milestone.Edited,
 	}
 	metadata, err := api.RepoMetadata(client, repo, input)
diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go
index 128c51068..9688a2aca 100644
--- a/pkg/cmd/pr/shared/params.go
+++ b/pkg/cmd/pr/shared/params.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 
 	"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/search"
 	"github.com/google/shlex"
@@ -56,7 +57,7 @@ func ValidURL(urlStr string) bool {
 
 // Ensure that tb.MetadataResult object exists and contains enough pre-fetched API data to be able
 // to resolve all object listed in tb to GraphQL IDs.
-func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState) error {
+func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetadataState, projectV1Support gh.ProjectsV1Support) error {
 	resolveInput := api.RepoResolveInput{}
 
 	if len(tb.Assignees) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.AssignableUsers) == 0) {
@@ -72,7 +73,11 @@ func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetada
 	}
 
 	if len(tb.Projects) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Projects) == 0) {
-		resolveInput.Projects = tb.Projects
+		if projectV1Support == gh.ProjectsV1Supported {
+			resolveInput.ProjectsV1 = true
+		}
+
+		resolveInput.ProjectsV2 = true
 	}
 
 	if len(tb.Milestones) > 0 && (tb.MetadataResult == nil || len(tb.MetadataResult.Milestones) == 0) {
@@ -93,12 +98,12 @@ func fillMetadata(client *api.Client, baseRepo ghrepo.Interface, tb *IssueMetada
 	return nil
 }
 
-func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState) error {
+func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, params map[string]interface{}, tb *IssueMetadataState, projectV1Support gh.ProjectsV1Support) error {
 	if !tb.HasMetadata() {
 		return nil
 	}
 
-	if err := fillMetadata(client, baseRepo, tb); err != nil {
+	if err := fillMetadata(client, baseRepo, tb, projectV1Support); err != nil {
 		return err
 	}
 
diff --git a/pkg/cmd/pr/shared/survey.go b/pkg/cmd/pr/shared/survey.go
index ce38535d9..23079a391 100644
--- a/pkg/cmd/pr/shared/survey.go
+++ b/pkg/cmd/pr/shared/survey.go
@@ -151,7 +151,7 @@ type RepoMetadataFetcher interface {
 	RepoMetadataFetch(api.RepoMetadataInput) (*api.RepoMetadataResult, error)
 }
 
-func MetadataSurvey(p Prompt, 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, projectsV1Support gh.ProjectsV1Support) error {
 	isChosen := func(m string) bool {
 		for _, c := range state.Metadata {
 			if m == c {
@@ -181,7 +181,8 @@ func MetadataSurvey(p Prompt, io *iostreams.IOStreams, baseRepo ghrepo.Interface
 		Reviewers:  isChosen("Reviewers"),
 		Assignees:  isChosen("Assignees"),
 		Labels:     isChosen("Labels"),
-		Projects:   isChosen("Projects"),
+		ProjectsV1: isChosen("Projects") && projectsV1Support == gh.ProjectsV1Supported,
+		ProjectsV2: isChosen("Projects"),
 		Milestones: isChosen("Milestone"),
 	}
 	metadataResult, err := fetcher.RepoMetadataFetch(metadataInput)
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 13aee459a..5f5f55b67 100644
--- a/api/queries_repo_test.go
+++ b/api/queries_repo_test.go
@@ -44,7 +44,8 @@ func Test_RepoMetadata(t *testing.T) {
 		Assignees:  true,
 		Reviewers:  true,
 		Labels:     true,
-		Projects:   true,
+		ProjectsV1: true,
+		ProjectsV2: true,
 		Milestones: true,
 	}
 
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 8e49700a0..d8c1c5d92 100644
--- a/pkg/cmd/issue/create/create_test.go
+++ b/pkg/cmd/issue/create/create_test.go
@@ -14,6 +14,7 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/internal/browser"
 	"github.com/cli/cli/v2/internal/config"
+	fd "github.com/cli/cli/v2/internal/featuredetection"
 	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	"github.com/cli/cli/v2/internal/prompter"
@@ -521,6 +522,7 @@ func runCommandWithRootDirOverridden(rt http.RoundTripper, isTTY bool, cli strin
 
 	cmd := NewCmdCreate(factory, func(opts *CreateOptions) error {
 		opts.RootDirOverride = rootDir
+		opts.Detector = &fd.EnabledDetectorMock{}
 		return createRun(opts)
 	})
 
@@ -1026,3 +1028,79 @@ func TestIssueCreate_projectsV2(t *testing.T) {
 
 	assert.Equal(t, "https://github.com/OWNER/REPO/issues/12\n", output.String())
 }
+
+// TODO projectsV1Deprecation
+// Remove this test.
+func TestProjectsV1Deprecation(t *testing.T) {
+
+	t.Run("non-interactive submission", 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.StubRepoInfoResponse("OWNER", "REPO", "main")
+			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{},
+				Title:    "Test Title",
+				Body:     "Test Body",
+				// 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{}
+			reg.StubRepoInfoResponse("OWNER", "REPO", "main")
+			// ( 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{},
+				Title:    "Test Title",
+				Body:     "Test Body",
+				// 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/survey_test.go b/pkg/cmd/pr/shared/survey_test.go
index d74696460..c094b7556 100644
--- a/pkg/cmd/pr/shared/survey_test.go
+++ b/pkg/cmd/pr/shared/survey_test.go
@@ -1,13 +1,16 @@
 package shared
 
 import (
+	"errors"
 	"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/internal/prompter"
 	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 type metadataFetcher struct {
@@ -68,7 +71,7 @@ func TestMetadataSurvey_selectAll(t *testing.T) {
 		Assignees: []string{"hubot"},
 		Type:      PRMetadata,
 	}
-	err := MetadataSurvey(pm, ios, repo, fetcher, state)
+	err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported)
 	assert.NoError(t, err)
 
 	assert.Equal(t, "", stdout.String())
@@ -113,7 +116,8 @@ func TestMetadataSurvey_keepExisting(t *testing.T) {
 	state := &IssueMetadataState{
 		Assignees: []string{"hubot"},
 	}
-	err := MetadataSurvey(pm, ios, repo, fetcher, state)
+
+	err := MetadataSurvey(pm, ios, repo, fetcher, state, gh.ProjectsV1Supported)
 	assert.NoError(t, err)
 
 	assert.Equal(t, "", stdout.String())
@@ -124,6 +128,63 @@ func TestMetadataSurvey_keepExisting(t *testing.T) {
 	assert.Equal(t, []string{"The road to 1.0"}, state.Projects)
 }
 
+// TODO projectsV1Deprecation
+// Remove this test and projectsV1MetadataFetcherSpy
+func TestMetadataSurveyProjectV1Deprecation(t *testing.T) {
+	t.Run("when projectsV1 is supported, requests projectsV1", func(t *testing.T) {
+		ios, _, _, _ := iostreams.Test()
+		repo := ghrepo.New("OWNER", "REPO")
+
+		fetcher := &projectsV1MetadataFetcherSpy{}
+		pm := prompter.NewMockPrompter(t)
+		pm.RegisterMultiSelect("What would you like to add?", []string{}, []string{"Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, options []string) ([]int, error) {
+			i, err := prompter.IndexFor(options, "Projects")
+			require.NoError(t, err)
+			return []int{i}, nil
+		})
+		pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring"}, func(_ string, _, _ []string) ([]int, error) {
+			return []int{0}, nil
+		})
+
+		err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Supported)
+		require.ErrorContains(t, err, "expected test error")
+
+		require.True(t, fetcher.projectsV1Requested, "expected projectsV1 to be requested")
+	})
+
+	t.Run("when projectsV1 is supported, does not request projectsV1", func(t *testing.T) {
+		ios, _, _, _ := iostreams.Test()
+		repo := ghrepo.New("OWNER", "REPO")
+
+		fetcher := &projectsV1MetadataFetcherSpy{}
+		pm := prompter.NewMockPrompter(t)
+		pm.RegisterMultiSelect("What would you like to add?", []string{}, []string{"Assignees", "Labels", "Projects", "Milestone"}, func(_ string, _, options []string) ([]int, error) {
+			i, err := prompter.IndexFor(options, "Projects")
+			require.NoError(t, err)
+			return []int{i}, nil
+		})
+		pm.RegisterMultiSelect("Projects", []string{}, []string{"Huge Refactoring"}, func(_ string, _, _ []string) ([]int, error) {
+			return []int{0}, nil
+		})
+
+		err := MetadataSurvey(pm, ios, repo, fetcher, &IssueMetadataState{}, gh.ProjectsV1Unsupported)
+		require.ErrorContains(t, err, "expected test error")
+
+		require.False(t, fetcher.projectsV1Requested, "expected projectsV1 not to be requested")
+	})
+}
+
+type projectsV1MetadataFetcherSpy struct {
+	projectsV1Requested bool
+}
+
+func (mf *projectsV1MetadataFetcherSpy) RepoMetadataFetch(input api.RepoMetadataInput) (*api.RepoMetadataResult, error) {
+	if input.ProjectsV1 {
+		mf.projectsV1Requested = true
+	}
+	return nil, errors.New("expected test error")
+}
+
 func TestTitledEditSurvey_cleanupHint(t *testing.T) {
 	var editorInitialText string
 	editor := &testEditor{