Repository: cli/cli
Pull request #10813 (2025-04-17 13:49:56)

Feature detect v1 projects on `issue view`
## Description

Relates to https://github.com/cli/cli/issues/10714
Builds on: https://github.com/cli/cli/pull/10811

This is the first of a series of PRs that prevents `gh` querying for deprecated classic project objects when targeting hosts that don't support it. For the moment, "support" is determined by whether the targeted host is GHES or not.

### 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 `projectCards` by running:

```
GH_DEBUG=api ./bin/gh issue view 1000 2>&1 | grep projectCards
```


Patch:
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/featuredetection/detector_mock.go b/internal/featuredetection/detector_mock.go
index 6f36dd3fc..6f760f209 100644
--- a/internal/featuredetection/detector_mock.go
+++ b/internal/featuredetection/detector_mock.go
@@ -1,5 +1,7 @@
 package featuredetection
 
+import "github.com/cli/cli/v2/internal/gh"
+
 type DisabledDetectorMock struct{}
 
 func (md *DisabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
@@ -14,6 +16,10 @@ func (md *DisabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error)
 	return RepositoryFeatures{}, nil
 }
 
+func (md *DisabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
+	return gh.ProjectsV1Unsupported
+}
+
 type EnabledDetectorMock struct{}
 
 func (md *EnabledDetectorMock) IssueFeatures() (IssueFeatures, error) {
@@ -27,3 +33,7 @@ func (md *EnabledDetectorMock) PullRequestFeatures() (PullRequestFeatures, error
 func (md *EnabledDetectorMock) RepositoryFeatures() (RepositoryFeatures, error) {
 	return allRepositoryFeatures, nil
 }
+
+func (md *EnabledDetectorMock) ProjectsV1() gh.ProjectsV1Support {
+	return gh.ProjectsV1Supported
+}
diff --git a/internal/featuredetection/feature_detection.go b/internal/featuredetection/feature_detection.go
index a9bbe25f8..fba317f58 100644
--- a/internal/featuredetection/feature_detection.go
+++ b/internal/featuredetection/feature_detection.go
@@ -4,6 +4,7 @@ import (
 	"net/http"
 
 	"github.com/cli/cli/v2/api"
+	"github.com/cli/cli/v2/internal/gh"
 	"golang.org/x/sync/errgroup"
 
 	ghauth "github.com/cli/go-gh/v2/pkg/auth"
@@ -13,6 +14,7 @@ type Detector interface {
 	IssueFeatures() (IssueFeatures, error)
 	PullRequestFeatures() (PullRequestFeatures, error)
 	RepositoryFeatures() (RepositoryFeatures, error)
+	ProjectsV1() gh.ProjectsV1Support
 }
 
 type IssueFeatures struct {
@@ -199,3 +201,13 @@ func (d *detector) RepositoryFeatures() (RepositoryFeatures, error) {
 
 	return features, nil
 }
+
+func (d *detector) ProjectsV1() gh.ProjectsV1Support {
+	// Currently, projects v1 support is entirely dependent on the host. As this is deprecated in GHES,
+	// we will do feature detection on whether the GHES version has support.
+	if ghauth.IsEnterprise(d.host) {
+		return gh.ProjectsV1Supported
+	}
+
+	return gh.ProjectsV1Unsupported
+}
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/gh/projects.go b/internal/gh/projects.go
new file mode 100644
index 000000000..34acf8d7c
--- /dev/null
+++ b/internal/gh/projects.go
@@ -0,0 +1,23 @@
+package gh
+
+// ProjectsV1Support provides type safety and readability around whether or not Projects v1 is supported
+// by the targeted host.
+//
+// It is a sealed type to ensure that consumers must use the exported ProjectsV1Supported and ProjectsV1Unsupported
+// variables to get an instance of the type.
+type ProjectsV1Support interface {
+	sealed()
+}
+
+type projectsV1Supported struct{}
+
+func (projectsV1Supported) sealed() {}
+
+type projectsV1Unsupported struct{}
+
+func (projectsV1Unsupported) sealed() {}
+
+var (
+	ProjectsV1Supported   ProjectsV1Support = projectsV1Supported{}
+	ProjectsV1Unsupported ProjectsV1Support = projectsV1Unsupported{}
+)
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/view/view.go b/pkg/cmd/issue/view/view.go
index f7838429b..a9e25513b 100644
--- a/pkg/cmd/issue/view/view.go
+++ b/pkg/cmd/issue/view/view.go
@@ -12,6 +12,8 @@ import (
 	"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"
 	"github.com/cli/cli/v2/pkg/cmd/issue/shared"
@@ -29,6 +31,7 @@ type ViewOptions struct {
 	IO         *iostreams.IOStreams
 	BaseRepo   func() (ghrepo.Interface, error)
 	Browser    browser.Browser
+	Detector   fd.Detector
 
 	IssueNumber int
 	WebMode     bool
@@ -89,7 +92,7 @@ func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Comman
 
 var defaultFields = []string{
 	"number", "url", "state", "createdAt", "title", "body", "author", "milestone",
-	"assignees", "labels", "projectCards", "reactionGroups", "lastComment", "stateReason",
+	"assignees", "labels", "reactionGroups", "lastComment", "stateReason",
 }
 
 func viewRun(opts *ViewOptions) error {
@@ -114,6 +117,18 @@ func viewRun(opts *ViewOptions) error {
 			lookupFields.Add("comments")
 			lookupFields.Remove("lastComment")
 		}
+
+		// TODO projectsV1Deprecation
+		// Remove this section as we should no longer add projectCards
+		if opts.Detector == nil {
+			cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
+			opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
+		}
+
+		projectsV1Support := opts.Detector.ProjectsV1()
+		if projectsV1Support == gh.ProjectsV1Supported {
+			lookupFields.Add("projectCards")
+		}
 	}
 
 	opts.IO.DetectTerminalTheme()
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
diff --git a/pkg/httpmock/registry.go b/pkg/httpmock/registry.go
index 51aa5a898..b7c5a117d 100644
--- a/pkg/httpmock/registry.go
+++ b/pkg/httpmock/registry.go
@@ -7,8 +7,6 @@ import (
 	"strings"
 	"sync"
 	"testing"
-
-	"github.com/stretchr/testify/assert"
 )
 
 // Replace http.Client transport layer with registry so all requests get
@@ -32,10 +30,21 @@ func (r *Registry) Register(m Matcher, resp Responder) {
 }
 
 func (r *Registry) Exclude(t *testing.T, m Matcher) {
+	registrationStack := string(debug.Stack())
+
 	excludedStub := &Stub{
 		Matcher: m,
 		Responder: func(req *http.Request) (*http.Response, error) {
-			assert.FailNowf(t, "Exclude error", "API called when excluded: %v", req.URL)
+			callStack := string(debug.Stack())
+
+			var errMsg strings.Builder
+			errMsg.WriteString("HTTP call was made when it should have been excluded:\n")
+			errMsg.WriteString(fmt.Sprintf("Request URL: %s\n", req.URL))
+			errMsg.WriteString(fmt.Sprintf("Was excluded by: %s\n", registrationStack))
+			errMsg.WriteString(fmt.Sprintf("Was called from: %s\n", callStack))
+
+			t.Error(errMsg.String())
+			t.FailNow()
 			return nil, nil
 		},
 		exclude: true,


Test patch:
diff --git a/internal/featuredetection/feature_detection_test.go b/internal/featuredetection/feature_detection_test.go
index 8af091c3f..f1152da2c 100644
--- a/internal/featuredetection/feature_detection_test.go
+++ b/internal/featuredetection/feature_detection_test.go
@@ -5,8 +5,10 @@ import (
 	"testing"
 
 	"github.com/MakeNowJust/heredoc"
+	"github.com/cli/cli/v2/internal/gh"
 	"github.com/cli/cli/v2/pkg/httpmock"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestIssueFeatures(t *testing.T) {
@@ -366,3 +368,19 @@ func TestRepositoryFeatures(t *testing.T) {
 		})
 	}
 }
+
+func TestProjectV1Support(t *testing.T) {
+	t.Parallel()
+
+	t.Run("when the host is enterprise, project v1 is supported", func(t *testing.T) {
+		detector := detector{host: "my.ghes.com"}
+		isProjectV1Supported := detector.ProjectsV1()
+		require.Equal(t, gh.ProjectsV1Supported, isProjectV1Supported)
+	})
+
+	t.Run("when the host is not enterprise, project v1 is not supported", func(t *testing.T) {
+		detector := detector{host: "github.com"}
+		isProjectV1Supported := detector.ProjectsV1()
+		require.Equal(t, gh.ProjectsV1Unsupported, isProjectV1Supported)
+	})
+}
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/view/view_test.go b/pkg/cmd/issue/view/view_test.go
index 2dd963687..391a288fb 100644
--- a/pkg/cmd/issue/view/view_test.go
+++ b/pkg/cmd/issue/view/view_test.go
@@ -10,6 +10,7 @@ import (
 
 	"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/run"
@@ -496,3 +497,66 @@ func TestIssueView_nontty_Comments(t *testing.T) {
 		})
 	}
 }
+
+// TODO projectsV1Deprecation
+// Remove this test.
+func TestProjectsV1Deprecation(t *testing.T) {
+	t.Run("when projects v1 is supported, is included in query", func(t *testing.T) {
+		ios, _, _, _ := iostreams.Test()
+
+		reg := &httpmock.Registry{}
+		reg.Register(
+			httpmock.GraphQL(`projectCards`),
+			// 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.
+		_ = viewRun(&ViewOptions{
+			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{},
+			IssueNumber: 123,
+		})
+
+		// Verify that our request contained projectCards
+		reg.Verify(t)
+	})
+
+	t.Run("when projects v1 is not supported, is not included in query", func(t *testing.T) {
+		ios, _, _, _ := iostreams.Test()
+
+		reg := &httpmock.Registry{}
+		reg.Exclude(t, httpmock.GraphQL(`projectCards`))
+
+		_, cmdTeardown := run.Stub()
+		defer cmdTeardown(t)
+
+		// Ignore the error because we're not really interested in it.
+		_ = viewRun(&ViewOptions{
+			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{},
+			IssueNumber: 123,
+		})
+
+		// Verify that our request contained projectCards
+		reg.Verify(t)
+	})
+}