Remove StateReason feature detection for issue close

The stateReason field was added in GHES ~3.4, which is far older than
the earliest supported GHES version (3.14). The feature detection and
conditional inclusion of stateReason is therefore unnecessary.

This removes:
- StateReason field from IssueFeatures struct
- GHES introspection query in IssueFeatures() (only ActorIsAssignable
  remains, which is always false on GHES)
- Conditional stateReason field inclusion in issue list
- Feature detection guard in issue close
- Feature detection guard in FindIssueOrPR

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kynan Ware 2026-03-04 13:31:30 -07:00
parent 6b56a23970
commit 4f2304d4e5
6 changed files with 6 additions and 128 deletions

View file

@ -23,12 +23,10 @@ type Detector interface {
}
type IssueFeatures struct {
StateReason bool
ActorIsAssignable bool
}
var allIssueFeatures = IssueFeatures{
StateReason: true,
ActorIsAssignable: true,
}
@ -137,32 +135,9 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
return allIssueFeatures, nil
}
features := IssueFeatures{
StateReason: false,
return IssueFeatures{
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
}
var featureDetection struct {
Issue struct {
Fields []struct {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Issue: __type(name: \"Issue\")"`
}
gql := api.NewClientFromHTTP(d.httpClient)
err := gql.Query(d.host, "Issue_fields", &featureDetection, nil)
if err != nil {
return features, err
}
for _, field := range featureDetection.Issue.Fields {
if field.Name == "stateReason" {
features.StateReason = true
}
}
return features, nil
}, nil
}
func (d *detector) PullRequestFeatures() (PullRequestFeatures, error) {

View file

@ -23,7 +23,6 @@ func TestIssueFeatures(t *testing.T) {
name: "github.com",
hostname: "github.com",
wantFeatures: IssueFeatures{
StateReason: true,
ActorIsAssignable: true,
},
wantErr: false,
@ -32,38 +31,18 @@ func TestIssueFeatures(t *testing.T) {
name: "ghec data residency (ghe.com)",
hostname: "stampname.ghe.com",
wantFeatures: IssueFeatures{
StateReason: true,
ActorIsAssignable: true,
},
wantErr: false,
},
{
name: "GHE empty response",
name: "GHE",
hostname: "git.my.org",
queryResponse: map[string]string{
`query Issue_fields\b`: `{"data": {}}`,
},
wantFeatures: IssueFeatures{
StateReason: false,
ActorIsAssignable: false,
},
wantErr: false,
},
{
name: "GHE has state reason field",
hostname: "git.my.org",
queryResponse: map[string]string{
`query Issue_fields\b`: heredoc.Doc(`
{ "data": { "Issue": { "fields": [
{"name": "stateReason"}
] } } }
`),
},
wantFeatures: IssueFeatures{
StateReason: true,
},
wantErr: false,
},
}
for _, tt := range tests {

View file

@ -3,11 +3,9 @@ package close
import (
"fmt"
"net/http"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/issue/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
@ -26,8 +24,6 @@ type CloseOptions struct {
Comment string
Reason string
DuplicateOf string
Detector fd.Detector
}
func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Command {
@ -165,7 +161,7 @@ func closeRun(opts *CloseOptions) error {
}
}
err = apiClose(httpClient, baseRepo, issue, opts.Detector, closeReason, duplicateIssueID)
err = apiClose(httpClient, baseRepo, issue, closeReason, duplicateIssueID)
if err != nil {
return err
}
@ -175,30 +171,11 @@ func closeRun(opts *CloseOptions) error {
return nil
}
func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue, detector fd.Detector, reason string, duplicateIssueID string) error {
func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue, reason string, duplicateIssueID string) error {
if issue.IsPullRequest() {
return api.PullRequestClose(httpClient, repo, issue.ID)
}
if reason != "" || duplicateIssueID != "" {
if detector == nil {
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
detector = fd.NewDetector(cachedClient, repo.RepoHost())
}
features, err := detector.IssueFeatures()
if err != nil {
return err
}
// TODO stateReasonCleanup
if !features.StateReason {
// If StateReason is not supported silently close issue without setting StateReason.
if duplicateIssueID != "" {
return fmt.Errorf("closing as duplicate is not supported on %s", repo.RepoHost())
}
reason = ""
}
}
switch reason {
case "":
// If no reason is specified do not set it.

View file

@ -5,7 +5,6 @@ import (
"net/http"
"testing"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/pkg/cmd/issue/argparsetest"
"github.com/cli/cli/v2/pkg/cmdutil"
@ -185,7 +184,6 @@ func TestCloseRun(t *testing.T) {
opts: &CloseOptions{
IssueNumber: 13,
Reason: "not planned",
Detector: &fd.EnabledDetectorMock{},
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
@ -213,7 +211,6 @@ func TestCloseRun(t *testing.T) {
opts: &CloseOptions{
IssueNumber: 13,
Reason: "duplicate",
Detector: &fd.EnabledDetectorMock{},
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
@ -241,7 +238,6 @@ func TestCloseRun(t *testing.T) {
opts: &CloseOptions{
IssueNumber: 13,
DuplicateOf: "99",
Detector: &fd.EnabledDetectorMock{},
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
@ -338,33 +334,6 @@ func TestCloseRun(t *testing.T) {
wantErr: true,
errMsg: "invalid value for `--duplicate-of`: invalid issue format: \"not-an-issue\"",
},
{
name: "close issue with reason when reason is not supported",
opts: &CloseOptions{
IssueNumber: 13,
Reason: "not planned",
Detector: &fd.DisabledDetectorMock{},
},
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL(`query IssueByNumber\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"hasIssuesEnabled": true,
"issue": { "id": "THE-ID", "number": 13, "title": "The title of the issue"}
} } }`),
)
reg.Register(
httpmock.GraphQL(`mutation IssueClose\b`),
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
func(inputs map[string]interface{}) {
assert.Equal(t, 1, len(inputs))
assert.Equal(t, "THE-ID", inputs["issueId"])
}),
)
},
wantStderr: "✓ Closed issue OWNER/REPO#13 (The title of the issue)\n",
},
{
name: "issue already closed",
opts: &CloseOptions{

View file

@ -147,15 +147,7 @@ func listRun(opts *ListOptions) error {
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
opts.Detector = fd.NewDetector(cachedClient, baseRepo.RepoHost())
}
features, err := opts.Detector.IssueFeatures()
if err != nil {
return err
}
fields := defaultFields
// TODO stateReasonCleanup
if features.StateReason {
fields = append(defaultFields, "stateReason")
}
fields := append(defaultFields, "stateReason")
filterOptions := prShared.FilterOptions{
Entity: "issue",

View file

@ -8,10 +8,8 @@ import (
"regexp"
"strconv"
"strings"
"time"
"github.com/cli/cli/v2/api"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/ghrepo"
o "github.com/cli/cli/v2/pkg/option"
"github.com/cli/cli/v2/pkg/set"
@ -138,18 +136,6 @@ func FindIssuesOrPRs(httpClient *http.Client, repo ghrepo.Interface, issueNumber
func FindIssueOrPR(httpClient *http.Client, repo ghrepo.Interface, number int, fields []string) (*api.Issue, error) {
fieldSet := set.NewStringSet()
fieldSet.AddValues(fields)
if fieldSet.Contains("stateReason") {
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
detector := fd.NewDetector(cachedClient, repo.RepoHost())
features, err := detector.IssueFeatures()
if err != nil {
return nil, err
}
// TODO stateReasonCleanup
if !features.StateReason {
fieldSet.Remove("stateReason")
}
}
var getProjectItems bool
if fieldSet.Contains("projectItems") {