Add --duplicate-of flag and duplicate reason to gh issue close
Support closing issues as duplicates via --reason duplicate and --duplicate-of <issue> flags. The --duplicate-of flag accepts an issue number or URL, validates it references a different issue (not a PR), and passes the duplicate issue ID to the closeIssue mutation. Feature detection checks whether the GHES instance supports the DUPLICATE enum value in IssueClosedStateReason before using it.
This commit is contained in:
parent
097bad6f73
commit
01c83acfe8
4 changed files with 368 additions and 22 deletions
|
|
@ -23,13 +23,15 @@ type Detector interface {
|
|||
}
|
||||
|
||||
type IssueFeatures struct {
|
||||
StateReason bool
|
||||
ActorIsAssignable bool
|
||||
StateReason bool
|
||||
StateReasonDuplicate bool
|
||||
ActorIsAssignable bool
|
||||
}
|
||||
|
||||
var allIssueFeatures = IssueFeatures{
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
StateReason: true,
|
||||
StateReasonDuplicate: true,
|
||||
ActorIsAssignable: true,
|
||||
}
|
||||
|
||||
type PullRequestFeatures struct {
|
||||
|
|
@ -138,8 +140,9 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
|
|||
}
|
||||
|
||||
features := IssueFeatures{
|
||||
StateReason: false,
|
||||
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
|
||||
StateReason: false,
|
||||
StateReasonDuplicate: false,
|
||||
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
|
||||
}
|
||||
|
||||
var featureDetection struct {
|
||||
|
|
@ -162,6 +165,30 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if !features.StateReason {
|
||||
return features, nil
|
||||
}
|
||||
|
||||
var issueClosedStateReasonFeatureDetection struct {
|
||||
IssueClosedStateReason struct {
|
||||
EnumValues []struct {
|
||||
Name string
|
||||
} `graphql:"enumValues(includeDeprecated: true)"`
|
||||
} `graphql:"IssueClosedStateReason: __type(name: \"IssueClosedStateReason\")"`
|
||||
}
|
||||
|
||||
err = gql.Query(d.host, "IssueClosedStateReason_enumValues", &issueClosedStateReasonFeatureDetection, nil)
|
||||
if err != nil {
|
||||
return features, err
|
||||
}
|
||||
|
||||
for _, enumValue := range issueClosedStateReasonFeatureDetection.IssueClosedStateReason.EnumValues {
|
||||
if enumValue.Name == "DUPLICATE" {
|
||||
features.StateReasonDuplicate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return features, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ func TestIssueFeatures(t *testing.T) {
|
|||
name: "github.com",
|
||||
hostname: "github.com",
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
StateReason: true,
|
||||
StateReasonDuplicate: true,
|
||||
ActorIsAssignable: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -32,8 +33,9 @@ func TestIssueFeatures(t *testing.T) {
|
|||
name: "ghec data residency (ghe.com)",
|
||||
hostname: "stampname.ghe.com",
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
ActorIsAssignable: true,
|
||||
StateReason: true,
|
||||
StateReasonDuplicate: true,
|
||||
ActorIsAssignable: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
@ -44,13 +46,14 @@ func TestIssueFeatures(t *testing.T) {
|
|||
`query Issue_fields\b`: `{"data": {}}`,
|
||||
},
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: false,
|
||||
ActorIsAssignable: false,
|
||||
StateReason: false,
|
||||
StateReasonDuplicate: false,
|
||||
ActorIsAssignable: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "GHE has state reason field",
|
||||
name: "GHE has state reason field without duplicate enum",
|
||||
hostname: "git.my.org",
|
||||
queryResponse: map[string]string{
|
||||
`query Issue_fields\b`: heredoc.Doc(`
|
||||
|
|
@ -58,9 +61,41 @@ func TestIssueFeatures(t *testing.T) {
|
|||
{"name": "stateReason"}
|
||||
] } } }
|
||||
`),
|
||||
`query IssueClosedStateReason_enumValues\b`: heredoc.Doc(`
|
||||
{ "data": { "IssueClosedStateReason": { "enumValues": [
|
||||
{"name": "COMPLETED"},
|
||||
{"name": "NOT_PLANNED"}
|
||||
] } } }
|
||||
`),
|
||||
},
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
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"}
|
||||
] } } }
|
||||
`),
|
||||
`query IssueClosedStateReason_enumValues\b`: heredoc.Doc(`
|
||||
{ "data": { "IssueClosedStateReason": { "enumValues": [
|
||||
{"name": "COMPLETED"},
|
||||
{"name": "NOT_PLANNED"},
|
||||
{"name": "DUPLICATE"}
|
||||
] } } }
|
||||
`),
|
||||
},
|
||||
wantFeatures: IssueFeatures{
|
||||
StateReason: true,
|
||||
StateReasonDuplicate: true,
|
||||
ActorIsAssignable: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type CloseOptions struct {
|
|||
IssueNumber int
|
||||
Comment string
|
||||
Reason string
|
||||
DuplicateOf string
|
||||
|
||||
Detector fd.Detector
|
||||
}
|
||||
|
|
@ -55,6 +56,13 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
|
|||
}
|
||||
|
||||
opts.IssueNumber = issueNumber
|
||||
if opts.DuplicateOf != "" {
|
||||
if opts.Reason == "" {
|
||||
opts.Reason = "duplicate"
|
||||
} else if opts.Reason != "duplicate" {
|
||||
return cmdutil.FlagErrorf("`--duplicate-of` can only be used with `--reason duplicate`")
|
||||
}
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
|
|
@ -64,13 +72,22 @@ func NewCmdClose(f *cmdutil.Factory, runF func(*CloseOptions) error) *cobra.Comm
|
|||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.Comment, "comment", "c", "", "Leave a closing comment")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Reason, "reason", "r", "", []string{"completed", "not planned"}, "Reason for closing")
|
||||
cmdutil.StringEnumFlag(cmd, &opts.Reason, "reason", "r", "", []string{"completed", "not planned", "duplicate"}, "Reason for closing")
|
||||
cmd.Flags().StringVar(&opts.DuplicateOf, "duplicate-of", "", "Mark as duplicate of another issue by number or URL")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func closeRun(opts *CloseOptions) error {
|
||||
cs := opts.IO.ColorScheme()
|
||||
closeReason := opts.Reason
|
||||
if opts.DuplicateOf != "" {
|
||||
if closeReason == "" {
|
||||
closeReason = "duplicate"
|
||||
} else if closeReason != "duplicate" {
|
||||
return cmdutil.FlagErrorf("`--duplicate-of` can only be used with `--reason duplicate`")
|
||||
}
|
||||
}
|
||||
|
||||
httpClient, err := opts.HttpClient()
|
||||
if err != nil {
|
||||
|
|
@ -92,6 +109,32 @@ func closeRun(opts *CloseOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
var duplicateIssueID string
|
||||
if opts.DuplicateOf != "" {
|
||||
if issue.IsPullRequest() {
|
||||
return cmdutil.FlagErrorf("`--duplicate-of` is only supported for issues")
|
||||
}
|
||||
duplicateIssueNumber, duplicateRepo, err := shared.ParseIssueFromArg(opts.DuplicateOf)
|
||||
if err != nil {
|
||||
return cmdutil.FlagErrorf("invalid value for `--duplicate-of`: %v", err)
|
||||
}
|
||||
duplicateIssueRepo := baseRepo
|
||||
if parsedRepo, present := duplicateRepo.Value(); present {
|
||||
duplicateIssueRepo = parsedRepo
|
||||
}
|
||||
if ghrepo.IsSame(baseRepo, duplicateIssueRepo) && issue.Number == duplicateIssueNumber {
|
||||
return cmdutil.FlagErrorf("`--duplicate-of` cannot reference the current issue")
|
||||
}
|
||||
duplicateIssue, err := shared.FindIssueOrPR(httpClient, duplicateIssueRepo, duplicateIssueNumber, []string{"id"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if duplicateIssue.IsPullRequest() {
|
||||
return cmdutil.FlagErrorf("`--duplicate-of` must reference an issue")
|
||||
}
|
||||
duplicateIssueID = duplicateIssue.ID
|
||||
}
|
||||
|
||||
if opts.Comment != "" {
|
||||
commentOpts := &prShared.CommentableOptions{
|
||||
Body: opts.Comment,
|
||||
|
|
@ -108,7 +151,7 @@ func closeRun(opts *CloseOptions) error {
|
|||
}
|
||||
}
|
||||
|
||||
err = apiClose(httpClient, baseRepo, issue, opts.Detector, opts.Reason)
|
||||
err = apiClose(httpClient, baseRepo, issue, opts.Detector, closeReason, duplicateIssueID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -118,12 +161,12 @@ func closeRun(opts *CloseOptions) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue, detector fd.Detector, reason string) error {
|
||||
func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue, detector fd.Detector, reason string, duplicateIssueID string) error {
|
||||
if issue.IsPullRequest() {
|
||||
return api.PullRequestClose(httpClient, repo, issue.ID)
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
if reason != "" || duplicateIssueID != "" {
|
||||
if detector == nil {
|
||||
cachedClient := api.NewCachedHTTPClient(httpClient, time.Hour*24)
|
||||
detector = fd.NewDetector(cachedClient, repo.RepoHost())
|
||||
|
|
@ -135,6 +178,15 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
|
|||
// 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 = ""
|
||||
} 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 = ""
|
||||
}
|
||||
}
|
||||
|
|
@ -144,6 +196,8 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
|
|||
// If no reason is specified do not set it.
|
||||
case "not planned":
|
||||
reason = "NOT_PLANNED"
|
||||
case "duplicate":
|
||||
reason = "DUPLICATE"
|
||||
default:
|
||||
reason = "COMPLETED"
|
||||
}
|
||||
|
|
@ -158,8 +212,9 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
|
|||
|
||||
variables := map[string]interface{}{
|
||||
"input": CloseIssueInput{
|
||||
IssueID: issue.ID,
|
||||
StateReason: reason,
|
||||
IssueID: issue.ID,
|
||||
StateReason: reason,
|
||||
DuplicateIssueID: duplicateIssueID,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -168,6 +223,7 @@ func apiClose(httpClient *http.Client, repo ghrepo.Interface, issue *api.Issue,
|
|||
}
|
||||
|
||||
type CloseIssueInput struct {
|
||||
IssueID string `json:"issueId"`
|
||||
StateReason string `json:"stateReason,omitempty"`
|
||||
IssueID string `json:"issueId"`
|
||||
StateReason string `json:"stateReason,omitempty"`
|
||||
DuplicateIssueID string `json:"duplicateIssueId,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,15 @@ 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)
|
||||
|
|
@ -44,6 +53,29 @@ func TestNewCmdClose(t *testing.T) {
|
|||
Reason: "not planned",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reason duplicate",
|
||||
input: "123 --reason duplicate",
|
||||
output: CloseOptions{
|
||||
IssueNumber: 123,
|
||||
Reason: "duplicate",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate of sets duplicate reason",
|
||||
input: "123 --duplicate-of 456",
|
||||
output: CloseOptions{
|
||||
IssueNumber: 123,
|
||||
Reason: "duplicate",
|
||||
DuplicateOf: "456",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate of with invalid reason",
|
||||
input: "123 --reason completed --duplicate-of 456",
|
||||
wantErr: true,
|
||||
errMsg: "`--duplicate-of` can only be used with `--reason duplicate`",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -74,6 +106,7 @@ func TestNewCmdClose(t *testing.T) {
|
|||
assert.Equal(t, tt.output.IssueNumber, gotOpts.IssueNumber)
|
||||
assert.Equal(t, tt.output.Comment, gotOpts.Comment)
|
||||
assert.Equal(t, tt.output.Reason, gotOpts.Reason)
|
||||
assert.Equal(t, tt.output.DuplicateOf, gotOpts.DuplicateOf)
|
||||
if tt.expectedBaseRepo != nil {
|
||||
baseRepo, err := gotOpts.BaseRepo()
|
||||
require.NoError(t, err)
|
||||
|
|
@ -184,6 +217,201 @@ func TestCloseRun(t *testing.T) {
|
|||
},
|
||||
wantStderr: "✓ Closed issue OWNER/REPO#13 (The title of the issue)\n",
|
||||
},
|
||||
{
|
||||
name: "close issue with duplicate reason",
|
||||
opts: &CloseOptions{
|
||||
IssueNumber: 13,
|
||||
Reason: "duplicate",
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
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, 2, len(inputs))
|
||||
assert.Equal(t, "THE-ID", inputs["issueId"])
|
||||
assert.Equal(t, "DUPLICATE", inputs["stateReason"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
wantStderr: "✓ Closed issue OWNER/REPO#13 (The title of the issue)\n",
|
||||
},
|
||||
{
|
||||
name: "close issue as duplicate of another issue",
|
||||
opts: &CloseOptions{
|
||||
IssueNumber: 13,
|
||||
DuplicateOf: "99",
|
||||
Detector: &fd.EnabledDetectorMock{},
|
||||
},
|
||||
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}
|
||||
} } }`),
|
||||
)
|
||||
reg.Register(
|
||||
httpmock.GraphQL(`mutation IssueClose\b`),
|
||||
httpmock.GraphQLMutation(`{"id": "THE-ID"}`,
|
||||
func(inputs map[string]interface{}) {
|
||||
assert.Equal(t, 3, len(inputs))
|
||||
assert.Equal(t, "THE-ID", inputs["issueId"])
|
||||
assert.Equal(t, "DUPLICATE", inputs["stateReason"])
|
||||
assert.Equal(t, "DUPLICATE-ID", inputs["duplicateIssueId"])
|
||||
}),
|
||||
)
|
||||
},
|
||||
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{
|
||||
IssueNumber: 13,
|
||||
DuplicateOf: "13",
|
||||
},
|
||||
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"}
|
||||
} } }`),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "`--duplicate-of` cannot reference the current issue",
|
||||
},
|
||||
{
|
||||
name: "duplicate of must reference an issue",
|
||||
opts: &CloseOptions{
|
||||
IssueNumber: 13,
|
||||
DuplicateOf: "99",
|
||||
},
|
||||
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": { "__typename": "PullRequest", "id": "PULL-ID", "number": 99}
|
||||
} } }`),
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
errMsg: "`--duplicate-of` must reference an issue",
|
||||
},
|
||||
{
|
||||
name: "duplicate of with invalid format",
|
||||
opts: &CloseOptions{
|
||||
IssueNumber: 13,
|
||||
DuplicateOf: "not-an-issue",
|
||||
},
|
||||
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"}
|
||||
} } }`),
|
||||
)
|
||||
},
|
||||
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{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue