Remove unnecessary StateReasonDuplicate feature detection

The DUPLICATE enum variant for IssueClosedStateReason was added in
GHES 3.16, which is older than the earliest supported GHES version.
The feature detection check is therefore unnecessary.

Addresses: https://github.com/cli/cli/pull/12811#issuecomment-3997044372

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

View file

@ -23,15 +23,13 @@ type Detector interface {
}
type IssueFeatures struct {
StateReason bool
StateReasonDuplicate bool
ActorIsAssignable bool
StateReason bool
ActorIsAssignable bool
}
var allIssueFeatures = IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: true,
StateReason: true,
ActorIsAssignable: true,
}
type PullRequestFeatures struct {
@ -140,9 +138,8 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
}
features := IssueFeatures{
StateReason: false,
StateReasonDuplicate: false,
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
StateReason: false,
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
}
var featureDetection struct {
@ -151,11 +148,6 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
Name string
} `graphql:"fields(includeDeprecated: true)"`
} `graphql:"Issue: __type(name: \"Issue\")"`
IssueClosedStateReason struct {
EnumValues []struct {
Name string
} `graphql:"enumValues(includeDeprecated: true)"`
} `graphql:"IssueClosedStateReason: __type(name: \"IssueClosedStateReason\")"`
}
gql := api.NewClientFromHTTP(d.httpClient)
@ -170,15 +162,6 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
}
}
if features.StateReason {
for _, enumValue := range featureDetection.IssueClosedStateReason.EnumValues {
if enumValue.Name == "DUPLICATE" {
features.StateReasonDuplicate = true
break
}
}
}
return features, nil
}

View file

@ -23,9 +23,8 @@ func TestIssueFeatures(t *testing.T) {
name: "github.com",
hostname: "github.com",
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: true,
StateReason: true,
ActorIsAssignable: true,
},
wantErr: false,
},
@ -33,9 +32,8 @@ func TestIssueFeatures(t *testing.T) {
name: "ghec data residency (ghe.com)",
hostname: "stampname.ghe.com",
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: true,
StateReason: true,
ActorIsAssignable: true,
},
wantErr: false,
},
@ -46,50 +44,23 @@ func TestIssueFeatures(t *testing.T) {
`query Issue_fields\b`: `{"data": {}}`,
},
wantFeatures: IssueFeatures{
StateReason: false,
StateReasonDuplicate: false,
ActorIsAssignable: false,
StateReason: false,
ActorIsAssignable: false,
},
wantErr: false,
},
{
name: "GHE has state reason field without duplicate enum",
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"}
] }, "IssueClosedStateReason": { "enumValues": [
{"name": "COMPLETED"},
{"name": "NOT_PLANNED"}
] } } }
`),
},
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: false,
ActorIsAssignable: false,
},
wantErr: false,
},
{
name: "GHE has duplicate state reason enum value",
hostname: "git.my.org",
queryResponse: map[string]string{
`query Issue_fields\b`: heredoc.Doc(`
{ "data": { "Issue": { "fields": [
{"name": "stateReason"}
] }, "IssueClosedStateReason": { "enumValues": [
{"name": "COMPLETED"},
{"name": "NOT_PLANNED"},
{"name": "DUPLICATE"}
] } } }
`),
},
wantFeatures: IssueFeatures{
StateReason: true,
StateReasonDuplicate: true,
ActorIsAssignable: false,
StateReason: true,
},
wantErr: false,
},

View file

@ -196,12 +196,6 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
return fmt.Errorf("closing as duplicate is not supported on %s", repo.RepoHost())
}
reason = ""
} else if reason == "duplicate" && !features.StateReasonDuplicate {
if duplicateIssueID != "" {
return fmt.Errorf("closing as duplicate is not supported on %s", repo.RepoHost())
}
// If DUPLICATE is not supported silently close issue without setting StateReason.
reason = ""
}
}

View file

@ -16,15 +16,6 @@ import (
"github.com/stretchr/testify/require"
)
type issueFeaturesDetectorMock struct {
fd.EnabledDetectorMock
issueFeatures fd.IssueFeatures
}
func (md *issueFeaturesDetectorMock) IssueFeatures() (fd.IssueFeatures, error) {
return md.issueFeatures, nil
}
func TestNewCmdClose(t *testing.T) {
// Test shared parsing of issue number / URL.
argparsetest.TestArgParsing(t, NewCmdClose)
@ -282,71 +273,6 @@ func TestCloseRun(t *testing.T) {
},
wantStderr: "✓ Closed issue OWNER/REPO#13 (The title of the issue)\n",
},
{
name: "close issue with duplicate reason when duplicate is not supported",
opts: &CloseOptions{
IssueNumber: 13,
Reason: "duplicate",
Detector: &issueFeaturesDetectorMock{
issueFeatures: fd.IssueFeatures{
StateReason: true,
StateReasonDuplicate: false,
},
},
},
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: "close issue as duplicate when duplicate is not supported",
opts: &CloseOptions{
IssueNumber: 13,
DuplicateOf: "99",
Detector: &issueFeaturesDetectorMock{
issueFeatures: fd.IssueFeatures{
StateReason: true,
StateReasonDuplicate: false,
},
},
},
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(`query IssueByNumber\b`),
httpmock.StringResponse(`
{ "data": { "repository": {
"hasIssuesEnabled": true,
"issue": { "id": "DUPLICATE-ID", "number": 99}
} } }`),
)
},
wantErr: true,
errMsg: "closing as duplicate is not supported on github.com",
},
{
name: "duplicate of cannot point to same issue",
opts: &CloseOptions{