Skip to content

Commit 22e29ec

Browse files
committed
Support cross-repository parent issues
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f71d9868-eef8-4fb0-84c6-df7c9a6a0ade
1 parent b2b9273 commit 22e29ec

10 files changed

Lines changed: 235 additions & 233 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,9 @@ The following sets of tools are available:
937937
(string, required)
938938
- `milestone`: Milestone number (number, optional)
939939
- `owner`: Repository owner (string, required)
940-
- `parent_issue_number`: Issue number of the parent issue. Only used when method is 'create'. The new issue is created and attached to this parent in the same operation. (number, optional)
940+
- `parent_issue_number`: Issue number of the parent issue. Only used when method is 'create' and cannot be combined with issue_fields. The new issue is created and attached to this parent in the same operation. (number, optional)
941+
- `parent_owner`: Repository owner of the parent issue. Defaults to the value of owner. Only used when method is 'create' and parent_issue_number is provided. (string, optional)
942+
- `parent_repo`: Repository name of the parent issue. Defaults to the value of repo. Only used when method is 'create' and parent_issue_number is provided. (string, optional)
941943
- `repo`: Repository name (string, required)
942944
- `state`: New state (string, optional)
943945
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)

docs/feature-flags.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ runtime behavior (such as output formatting) won't appear here.
6767
(string, required)
6868
- `milestone`: Milestone number (number, optional)
6969
- `owner`: Repository owner (string, required)
70-
- `parent_issue_number`: Issue number of the parent issue. Only used when method is 'create'. The new issue is created and attached to this parent in the same operation. (number, optional)
70+
- `parent_issue_number`: Issue number of the parent issue. Only used when method is 'create' and cannot be combined with issue_fields. The new issue is created and attached to this parent in the same operation. (number, optional)
71+
- `parent_owner`: Repository owner of the parent issue. Defaults to the value of owner. Only used when method is 'create' and parent_issue_number is provided. (string, optional)
72+
- `parent_repo`: Repository name of the parent issue. Defaults to the value of repo. Only used when method is 'create' and parent_issue_number is provided. (string, optional)
7173
- `repo`: Repository name (string, required)
7274
- `state`: New state (string, optional)
7375
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)
@@ -124,6 +126,8 @@ runtime behavior (such as output formatting) won't appear here.
124126
- `body`: Issue body content (optional) (string, optional)
125127
- `owner`: Repository owner (username or organization) (string, required)
126128
- `parent_issue_number`: Issue number of the parent issue. The new issue is created and attached to this parent in the same operation. (number, optional)
129+
- `parent_owner`: Repository owner of the parent issue. Defaults to the value of owner. Only used when parent_issue_number is provided. (string, optional)
130+
- `parent_repo`: Repository name of the parent issue. Defaults to the value of repo. Only used when parent_issue_number is provided. (string, optional)
127131
- `repo`: Repository name (string, required)
128132
- `title`: Issue title (string, required)
129133

docs/insiders-features.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ The list below is generated from the Go source. It covers tool **inventory and s
6161
(string, required)
6262
- `milestone`: Milestone number (number, optional)
6363
- `owner`: Repository owner (string, required)
64-
- `parent_issue_number`: Issue number of the parent issue. Only used when method is 'create'. The new issue is created and attached to this parent in the same operation. (number, optional)
64+
- `parent_issue_number`: Issue number of the parent issue. Only used when method is 'create' and cannot be combined with issue_fields. The new issue is created and attached to this parent in the same operation. (number, optional)
65+
- `parent_owner`: Repository owner of the parent issue. Defaults to the value of owner. Only used when method is 'create' and parent_issue_number is provided. (string, optional)
66+
- `parent_repo`: Repository name of the parent issue. Defaults to the value of repo. Only used when method is 'create' and parent_issue_number is provided. (string, optional)
6567
- `repo`: Repository name (string, required)
6668
- `state`: New state (string, optional)
6769
- `state_reason`: Reason for the state change. Ignored unless state is changed. (string, optional)

pkg/github/__toolsnaps__/create_issue.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
"minimum": 1,
2323
"type": "number"
2424
},
25+
"parent_owner": {
26+
"description": "Repository owner of the parent issue. Defaults to the value of owner. Only used when parent_issue_number is provided.",
27+
"type": "string"
28+
},
29+
"parent_repo": {
30+
"description": "Repository name of the parent issue. Defaults to the value of repo. Only used when parent_issue_number is provided.",
31+
"type": "string"
32+
},
2533
"repo": {
2634
"description": "Repository name",
2735
"type": "string"

pkg/github/__toolsnaps__/issue_write.snap

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,18 @@
9292
"type": "string"
9393
},
9494
"parent_issue_number": {
95-
"description": "Issue number of the parent issue. Only used when method is 'create'. The new issue is created and attached to this parent in the same operation.",
95+
"description": "Issue number of the parent issue. Only used when method is 'create' and cannot be combined with issue_fields. The new issue is created and attached to this parent in the same operation.",
9696
"minimum": 1,
9797
"type": "number"
9898
},
99+
"parent_owner": {
100+
"description": "Repository owner of the parent issue. Defaults to the value of owner. Only used when method is 'create' and parent_issue_number is provided.",
101+
"type": "string"
102+
},
103+
"parent_repo": {
104+
"description": "Repository name of the parent issue. Defaults to the value of repo. Only used when method is 'create' and parent_issue_number is provided.",
105+
"type": "string"
106+
},
99107
"repo": {
100108
"description": "Repository name",
101109
"type": "string"

pkg/github/issues.go

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -116,36 +116,31 @@ func getCloseStateReason(stateReason string) IssueClosedStateReason {
116116
}
117117

118118
// issueFieldWriteMetadataNode queries only the fields needed to resolve a write: the field's
119-
// node ID, fullDatabaseId (BigInt scalar, returned as string), name, and data type for validation.
119+
// fullDatabaseId (BigInt scalar, returned as string) plus its name and data type for validation.
120120
// shurcooL/githubv4 cannot use interface-level fragments at union top-level, so we repeat
121121
// fullDatabaseId on each concrete type; all four implement IssueFieldCommon.
122122
type issueFieldWriteMetadataNode struct {
123123
TypeName githubv4.String `graphql:"__typename"`
124124
IssueFieldText struct {
125-
ID githubv4.ID
126125
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
127126
Name githubv4.String
128127
DataType githubv4.String
129128
} `graphql:"... on IssueFieldText"`
130129
IssueFieldNumber struct {
131-
ID githubv4.ID
132130
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
133131
Name githubv4.String
134132
DataType githubv4.String
135133
} `graphql:"... on IssueFieldNumber"`
136134
IssueFieldDate struct {
137-
ID githubv4.ID
138135
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
139136
Name githubv4.String
140137
DataType githubv4.String
141138
} `graphql:"... on IssueFieldDate"`
142139
IssueFieldSingleSelect struct {
143-
ID githubv4.ID
144140
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
145141
Name githubv4.String
146142
DataType githubv4.String
147143
Options []struct {
148-
ID githubv4.ID
149144
FullDatabaseID githubv4.String `graphql:"fullDatabaseId"`
150145
Name githubv4.String
151146
}
@@ -313,9 +308,33 @@ func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Cli
313308
return nil, nil, nil
314309
}
315310

316-
fieldByName, err := fetchIssueFieldWriteMetadata(ctx, gqlClient, owner, repo)
317-
if err != nil {
318-
return nil, nil, err
311+
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
312+
var query issueFieldWriteMetadataQuery
313+
vars := map[string]any{
314+
"owner": githubv4.String(owner),
315+
"repo": githubv4.String(repo),
316+
}
317+
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
318+
return nil, nil, fmt.Errorf("failed to query issue fields metadata: %w", err)
319+
}
320+
321+
// Build name → node map, dispatching on concrete type to extract name.
322+
fieldByName := make(map[string]issueFieldWriteMetadataNode, len(query.Repository.IssueFields.Nodes))
323+
for _, node := range query.Repository.IssueFields.Nodes {
324+
var name string
325+
switch string(node.TypeName) {
326+
case "IssueFieldText":
327+
name = string(node.IssueFieldText.Name)
328+
case "IssueFieldNumber":
329+
name = string(node.IssueFieldNumber.Name)
330+
case "IssueFieldDate":
331+
name = string(node.IssueFieldDate.Name)
332+
case "IssueFieldSingleSelect":
333+
name = string(node.IssueFieldSingleSelect.Name)
334+
default:
335+
continue
336+
}
337+
fieldByName[strings.ToLower(strings.TrimSpace(name))] = node
319338
}
320339

321340
resolved := make([]*github.IssueRequestFieldValue, 0, len(issueFields))
@@ -382,38 +401,6 @@ func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Cli
382401
return resolved, fieldIDsToDelete, nil
383402
}
384403

385-
func fetchIssueFieldWriteMetadata(ctx context.Context, gqlClient *githubv4.Client, owner, repo string) (map[string]issueFieldWriteMetadataNode, error) {
386-
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
387-
var query issueFieldWriteMetadataQuery
388-
vars := map[string]any{
389-
"owner": githubv4.String(owner),
390-
"repo": githubv4.String(repo),
391-
}
392-
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
393-
return nil, fmt.Errorf("failed to query issue fields metadata: %w", err)
394-
}
395-
396-
fieldByName := make(map[string]issueFieldWriteMetadataNode, len(query.Repository.IssueFields.Nodes))
397-
for _, node := range query.Repository.IssueFields.Nodes {
398-
var name string
399-
switch string(node.TypeName) {
400-
case "IssueFieldText":
401-
name = string(node.IssueFieldText.Name)
402-
case "IssueFieldNumber":
403-
name = string(node.IssueFieldNumber.Name)
404-
case "IssueFieldDate":
405-
name = string(node.IssueFieldDate.Name)
406-
case "IssueFieldSingleSelect":
407-
name = string(node.IssueFieldSingleSelect.Name)
408-
default:
409-
continue
410-
}
411-
fieldByName[strings.ToLower(strings.TrimSpace(name))] = node
412-
}
413-
414-
return fieldByName, nil
415-
}
416-
417404
// fetchExistingIssueFieldValues retrieves the current field values for an issue
418405
// as IssueRequestFieldValue entries, ready to be merged before an update.
419406
func fetchExistingIssueFieldValues(ctx context.Context, gqlClient *githubv4.Client, owner, repo string, issueNumber int) ([]*github.IssueRequestFieldValue, error) {
@@ -2362,8 +2349,8 @@ var issueWriteFormParams = map[string]struct{}{
23622349
"_ui_submitted": {},
23632350
}
23642351

2365-
// parent_issue_number is intentionally omitted because the current form cannot
2366-
// represent it. Calls that supply a parent bypass the form instead of dropping it.
2352+
// Parent issue parameters are intentionally omitted because the current form cannot
2353+
// represent them. Calls that supply a parent bypass the form instead of dropping them.
23672354

23682355
// issueWriteAwaitingFormResult builds the "awaiting form submission" stub
23692356
// returned when issue_write hands off to the MCP App form. The body is shared
@@ -2443,9 +2430,17 @@ Options are:
24432430
},
24442431
"parent_issue_number": {
24452432
Type: "number",
2446-
Description: "Issue number of the parent issue. Only used when method is 'create'. The new issue is created and attached to this parent in the same operation.",
2433+
Description: "Issue number of the parent issue. Only used when method is 'create' and cannot be combined with issue_fields. The new issue is created and attached to this parent in the same operation.",
24472434
Minimum: jsonschema.Ptr(1.0),
24482435
},
2436+
"parent_owner": {
2437+
Type: "string",
2438+
Description: "Repository owner of the parent issue. Defaults to the value of owner. Only used when method is 'create' and parent_issue_number is provided.",
2439+
},
2440+
"parent_repo": {
2441+
Type: "string",
2442+
Description: "Repository name of the parent issue. Defaults to the value of repo. Only used when method is 'create' and parent_issue_number is provided.",
2443+
},
24492444
"title": {
24502445
Type: "string",
24512446
Description: "Issue title",
@@ -2644,12 +2639,26 @@ Options are:
26442639
if parentProvided && method != "create" {
26452640
return utils.NewToolResultError("parent_issue_number can only be used with the create method"), nil, nil
26462641
}
2642+
parentOwner, err := OptionalParam[string](args, "parent_owner")
2643+
if err != nil {
2644+
return utils.NewToolResultError(err.Error()), nil, nil
2645+
}
2646+
parentRepo, err := OptionalParam[string](args, "parent_repo")
2647+
if err != nil {
2648+
return utils.NewToolResultError(err.Error()), nil, nil
2649+
}
2650+
if !parentProvided && (parentOwner != "" || parentRepo != "") {
2651+
return utils.NewToolResultError("parent_owner and parent_repo can only be used when parent_issue_number is provided"), nil, nil
2652+
}
26472653

26482654
var issueFields []issueWriteFieldInput
26492655
issueFields, err = optionalIssueWriteFields(args)
26502656
if err != nil {
26512657
return utils.NewToolResultError(err.Error()), nil, nil
26522658
}
2659+
if parentProvided && len(issueFields) > 0 {
2660+
return utils.NewToolResultError("issue_fields cannot be used with parent_issue_number"), nil, nil
2661+
}
26532662

26542663
client, err := deps.GetClient(ctx)
26552664
if err != nil {
@@ -2661,35 +2670,29 @@ Options are:
26612670
return utils.NewToolResultErrorFromErr("failed to get GraphQL client", err), nil, nil
26622671
}
26632672

2673+
var issueFieldValues []*github.IssueRequestFieldValue
2674+
var fieldIDsToDelete []int64
2675+
if len(issueFields) > 0 {
2676+
issueFieldValues, fieldIDsToDelete, err = resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields)
2677+
if err != nil {
2678+
return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil
2679+
}
2680+
}
2681+
26642682
switch method {
26652683
case "create":
26662684
if parentProvided {
2667-
result, err := createIssueWithParent(ctx, client, gqlClient, owner, repo, title, body, assignees, labels, milestoneNum, issueType, issueFields, parentIssueNumber)
2685+
result, err := createIssueWithParent(ctx, client, gqlClient, owner, repo, title, body, assignees, labels, milestoneNum, issueType, parentIssueNumber, parentOwner, parentRepo)
26682686
return result, nil, err
26692687
}
26702688

2671-
var issueFieldValues []*github.IssueRequestFieldValue
2672-
if len(issueFields) > 0 {
2673-
issueFieldValues, _, err = resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields)
2674-
if err != nil {
2675-
return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil
2676-
}
2677-
}
26782689
result, err := CreateIssue(ctx, client, owner, repo, title, body, assignees, labels, milestoneNum, issueType, issueFieldValues)
26792690
return result, nil, err
26802691
case "update":
26812692
issueNumber, err := RequiredInt(args, "issue_number")
26822693
if err != nil {
26832694
return utils.NewToolResultError(err.Error()), nil, nil
26842695
}
2685-
var issueFieldValues []*github.IssueRequestFieldValue
2686-
var fieldIDsToDelete []int64
2687-
if len(issueFields) > 0 {
2688-
issueFieldValues, fieldIDsToDelete, err = resolveIssueRequestFieldValues(ctx, gqlClient, owner, repo, issueFields)
2689-
if err != nil {
2690-
return utils.NewToolResultError(fmt.Sprintf("failed to resolve issue_fields: %v", err)), nil, nil
2691-
}
2692-
}
26932696
result, err := UpdateIssue(ctx, client, gqlClient, owner, repo, issueNumber, title, body, assignees, labels, milestoneNum, issueType, issueFieldValues, fieldIDsToDelete, state, stateReason, duplicateOf, UpdateIssueOptions{
26942697
AssigneesProvided: assigneesProvided,
26952698
LabelsProvided: labelsProvided,

0 commit comments

Comments
 (0)