Add Issues 2.0 support: issue types, sub-issues, and relationships#13057
Add Issues 2.0 support: issue types, sub-issues, and relationships#13057
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for GitHub Issues 2.0 capabilities (issue types, sub-issues, and blocked-by/blocking relationships) across gh issue create/edit/view/list, including GHES feature detection for relationships.
Changes:
- Adds new GraphQL fields/types and mutations for issue types, sub-issues, and relationships; exports these via
--json. - Extends issue create/edit/list/view commands with new flags, interactive prompts, and TTY rendering for Issues 2.0 metadata.
- Introduces GHES relationship feature detection via schema introspection and adds/updates tests for flags and behaviors.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/cmd/pr/shared/survey_test.go | Regression test ensuring issue-only editable fields don’t appear unless explicitly allowed. |
| pkg/cmd/pr/shared/params.go | Adds IssueType filtering into search query building. |
| pkg/cmd/pr/shared/editable.go | Adds IssueType/Parent editable fields, survey prompts, and option fetching. |
| pkg/cmd/issue/view/view_test.go | Adds TTY and JSON tests for Issues 2.0 fields in gh issue view. |
| pkg/cmd/issue/view/view.go | Adds default fields and TTY rendering for issue type/parent/sub-issues/relationships; GHES-gates relationship query fields. |
| pkg/cmd/issue/shared/resolve.go | Adds shared helpers to resolve issue refs and type names. |
| pkg/cmd/issue/list/list_test.go | Adds tests for --type flag parsing and query behavior. |
| pkg/cmd/issue/list/list.go | Adds --type filter support to issue list search path. |
| pkg/cmd/issue/edit/edit_test.go | Adds flag parsing and behavior tests for type/parent/sub-issues/relationships including GHES unsupported path. |
| pkg/cmd/issue/edit/edit.go | Implements new edit flags and mutations for type/parent/sub-issues/relationships, plus interactive picker integration. |
| pkg/cmd/issue/create/create_test.go | Adds flag parsing and behavior tests for type/parent/relationships including GHES unsupported path. |
| pkg/cmd/issue/create/create.go | Implements create flags, interactive type selection, and post-create mutations for Issues 2.0 fields. |
| internal/featuredetection/feature_detection_test.go | Adds tests for relationship support detection via schema introspection. |
| internal/featuredetection/feature_detection.go | Adds IssueRelationshipsSupported detection and plumbing. |
| api/query_builder.go | Adds GraphQL fragment cases and issue-only fields for Issues 2.0 fields. |
| api/queries_issue.go | Adds Issue struct fields/types plus mutations and helper queries for issue types/refs/relationships. |
| api/export_pr.go | Extends JSON export mapping for new issue fields (type, parent, sub-issues, relationships). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if err == nil { | ||
| typeNames := make([]string, len(issueTypes)) | ||
| for i, t := range issueTypes { | ||
| typeNames[i] = t.Name | ||
| } | ||
| editable.IssueType.Options = typeNames | ||
| } |
There was a problem hiding this comment.
FetchOptions silently ignores errors from api.RepoIssueTypes when editable.IssueType.Edited is true. In interactive edit flows this can result in the user selecting “Type” but never being prompted (and no update happening) with no indication why. Recommend returning the error (or at least surfacing it) when the user has opted into editing Type, so failures don’t degrade silently.
| if err == nil { | |
| typeNames := make([]string, len(issueTypes)) | |
| for i, t := range issueTypes { | |
| typeNames[i] = t.Name | |
| } | |
| editable.IssueType.Options = typeNames | |
| } | |
| if err != nil { | |
| return fmt.Errorf("fetching issue types: %w", err) | |
| } | |
| typeNames := make([]string, len(issueTypes)) | |
| for i, t := range issueTypes { | |
| typeNames[i] = t.Name | |
| } | |
| editable.IssueType.Options = typeNames |
There was a problem hiding this comment.
I mean I think this could happen if it's a personal repo, so maybe this is actually a deeper thing in that we should be detecting if it's a repo that even supports issue types.
| if err != nil { | ||
| return err | ||
| } | ||
| editable.IssueType.Value = editable.IssueType.Options[selected] |
There was a problem hiding this comment.
If editable.IssueType.Edited is true but IssueType.Options is empty, the survey currently skips prompting and leaves IssueType.Value unchanged, even though the user explicitly chose to edit the field. This can lead to a “successful” submission that does nothing. Consider returning an error in this case (e.g., “no issue types available”) or falling back to a free-text input prompt.
| editable.IssueType.Value = editable.IssueType.Options[selected] | |
| editable.IssueType.Value = editable.IssueType.Options[selected] | |
| } else { | |
| editable.IssueType.Value, err = p.Input("Type", editable.IssueType.Default) | |
| if err != nil { | |
| return err | |
| } |
There was a problem hiding this comment.
Same as above comment - this is a deeper problem. We gotta check if there issue types are even possible.
Though I suppose this could still happen if there are no issue types available.
| searchTerms := options.Search | ||
| if options.IssueType != "" { | ||
| if searchTerms != "" { | ||
| searchTerms += " " | ||
| } | ||
| if strings.Contains(options.IssueType, " ") { | ||
| searchTerms += fmt.Sprintf(`type:"%s"`, options.IssueType) | ||
| } else { | ||
| searchTerms += "type:" + options.IssueType | ||
| } |
There was a problem hiding this comment.
IssueType is appended into ImmutableKeywords without any escaping beyond a space check. Since ImmutableKeywords bypasses search.Query’s quoting/escaping, values containing quotes (or other special characters) can produce malformed queries; unquoted values also allow injecting additional qualifiers via --type. Consider quoting/escaping the type name similarly to search.quote (e.g., use strconv.Quote/escape quotes) and always treat the flag value as a literal.
| if r, ok := repo.Value(); ok { | ||
| targetRepo = r | ||
| } | ||
|
|
There was a problem hiding this comment.
ResolveIssueRef allows issue URLs from a different host (since ParseIssueFromArg uses u.Hostname()), but callers later run mutations against baseRepo.RepoHost(). A cross-host ref will resolve a node ID from another instance and then fail during mutation with a confusing “could not resolve node ID” style error. Recommend explicitly validating targetRepo.RepoHost() == baseRepo.RepoHost() and returning a clear error when the ref points to a different host.
| if targetRepo.RepoHost() != baseRepo.RepoHost() { | |
| return "", fmt.Errorf("issue reference %q belongs to a different host (%s) than the current repository (%s)", ref, targetRepo.RepoHost(), baseRepo.RepoHost()) | |
| } |
There was a problem hiding this comment.
Oof that's a good call. Need to look at this
| // TODO IssueRelationshipsCleanup | ||
| issueFeatures, issueErr := opts.Detector.IssueFeatures() | ||
| if issueErr == nil && issueFeatures.IssueRelationshipsSupported { | ||
| lookupFields.AddValues([]string{"blockedBy", "blocking"}) | ||
| } |
There was a problem hiding this comment.
The GHES gating for blockedBy/blocking is only applied in the non---json path (inside the else branch). If a user requests --json blockedBy/blocking, those fields will still be included in lookupFields and the query will fail on GHES 3.17–3.18 with a schema error rather than the intended clear message. Consider applying the same IssueRelationshipsSupported check when opts.Exporter != nil and returning a friendly error when unsupported (or stripping those fields from the requested set).
There was a problem hiding this comment.
This is intentional - this is how we handle other JSON fields. We don't bother since users can just not request the fields that aren't supported.
| var defaultFields = []string{ | ||
| "number", "url", "state", "createdAt", "title", "body", "author", "milestone", | ||
| "assignees", "labels", "reactionGroups", "lastComment", "stateReason", | ||
| "issueType", "parent", "subIssues", "subIssuesSummary", | ||
| } |
There was a problem hiding this comment.
defaultFields is used for both TTY and non-TTY output, but the non-TTY/raw renderer doesn’t display issueType, parent, subIssues, or subIssuesSummary. Fetching these unconditionally increases the GraphQL payload for scripted/non-TTY use. Consider only adding these fields when opts.IO.IsStdoutTTY() (or when explicitly requested via --json).
There was a problem hiding this comment.
hmmm I think the non-tty actually should have this info and this is an oversight.
| if err := cmdutil.MutuallyExclusive( | ||
| "specify only one of --set-parent or --remove-parent", | ||
| flags.Changed("set-parent"), | ||
| opts.RemoveParent, | ||
| ); err != nil { | ||
| return err | ||
| } | ||
|
|
There was a problem hiding this comment.
set blocking and remove blocking should probably also be mutually exclusive.
Add API infrastructure for issue types, sub-issues, and issue relationships (blocked-by/blocking): - New types: IssueType, LinkedIssue, SubIssues, SubIssuesSummary, LinkedIssueConnection - New Issue struct fields for all Issues 2.0 data - GraphQL query builder cases for new fields - ExportData cases for JSON output - Mutation functions: UpdateIssueIssueType, AddSubIssue, RemoveSubIssue, AddBlockedBy, RemoveBlockedBy - Helper functions: RepoIssueTypes, IssueNodeID - Feature detection: IssueRelationshipsSupported for GHES 3.19+ (issue types and sub-issues are GA on GHES 3.17+, no detection needed) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Display new issue metadata in TTY view: - Issue type on state line (gray, before Open/Closed) - Type, Parent, Blocked by, Blocking metadata rows - Sub-issues section with completion progress (X/Y, Z%) - Cross-repo references show full owner/repo#N format All new fields included in defaultFields and JSON export. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Post-creation mutations for Issues 2.0 fields: - --type: resolve type name to ID via RepoIssueTypes, then updateIssueIssueType mutation - --parent: resolve issue ref (number or URL), then addSubIssue mutation (supports cross-repo URLs) - --blocked-by: resolve refs, then addBlockedBy mutations - --blocking: resolve refs, then addBlockedBy with swapped args Interactive mode: type picker when repo has issue types configured. GHES: relationships gated behind IssueRelationshipsSupported feature detection (3.19+). Types and sub-issues need no detection (GA 3.17+). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
New flags for issue edit: - --type: set issue type by name - --set-parent / --remove-parent: set or remove parent issue (mutually exclusive via cmdutil.MutuallyExclusive) - --add-sub-issue / --remove-sub-issue: manage sub-issues - --add-blocked-by / --remove-blocked-by: manage blocked-by relationships - --add-blocking: add blocking relationships (swaps API args) Interactive mode: Type and Parent added to the field picker survey. FetchOptions loads issue types when Type is selected. Editable struct: added IssueType and Parent fields with Dirty(), Clone(), FieldsToEditSurvey, and EditFieldsSurvey support. GHES: relationships gated behind IssueRelationshipsSupported. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Filter issues by type using the search API path. The --type flag appends a type: qualifier to the search query, forcing the search path (same as --label and --milestone). Updated FilterOptions with IssueType field, IsDefault(), and SearchQueryBuild() to include the type qualifier. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create tests (11 new): - Flag parsing: --type, --parent (number/URL), --blocked-by, --blocking - Behavior: type resolution + mutation, type not found error, parent resolution + addSubIssue, blocked-by/blocking with swapped args verification, GHES unsupported error Edit tests (18 new): - Flag parsing: --type, --set-parent, --remove-parent, mutual exclusivity, --add-sub-issue, --remove-sub-issue, --add-blocked-by, --remove-blocked-by, --add-blocking - Behavior: type edit, set/remove parent, add/remove sub-issues, add/remove blocked-by, add-blocking with swapped args, batch edit type across multiple issues - Bug fix: copy SetParent value into Editable.Parent.Value View tests (5 new): - TTY: full view with all Issues 2.0 fields, regression test with no new fields - JSON: issueType export, parent/subIssues/subIssuesSummary export, blockedBy/blocking export Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…okup Address code review findings: - Extract resolveIssueRef into shared.ResolveIssueRef (was duplicated between create.go and edit.go) - Extract issue type name→ID resolution into shared.ResolveIssueTypeName (was duplicated between create applyIssueTypes and edit applyEditIssueType) - Fix double import of issue/shared in view.go Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Critical fixes: - GHES data-flow regression: blockedBy/blocking fields now conditionally added to view lookupFields only when IssueRelationshipsSupported is true (GHES 3.19+). Previously would break gh issue view on GHES 3.17-3.18. - State line separator: restore original bullet (•) to avoid breaking downstream parsers. Issue type prefix uses middle dot (·). Optimizations: - Batch edit --type: resolve issueTypeID once before the loop instead of per-goroutine (eliminates N-1 redundant API calls) - Parent removal: include id in parent GraphQL fragment, use issue.Parent.ID directly instead of extra IssueNodeID lookup Nit fixes: - Fix formatLinkedIssueRef godoc to match actual behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add flag parsing and behavior tests for gh issue list --type: - TestNewCmdList/type_flag: verifies opts.IssueType is set - Test_issueList/with_issue_type: verifies search path is forced and query includes type:Bug qualifier Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Completes the symmetry of relationship flags: - --add-blocked-by / --remove-blocked-by - --add-blocking / --remove-blocking The --remove-blocking flag swaps API args (same as --add-blocking): calls RemoveBlockedBy(issueId=OTHER, blockingIssueId=THIS). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix PR edit regression: gate Type and Parent behind Allowed bool in FieldsToEditSurvey, matching the Reviewers.Allowed pattern. Only issue edit sets Allowed=true; PR edit won't show these fields. - Add missing RemoveBlocking assertion in flag parsing tests - Quote issue type names containing spaces in search queries (type:"Bug Report" instead of type:Bug Report) - Remove duplicate TODO comment in view.go - Avoid double RepoIssueTypes API call in interactive create: cache the resolved ID from the picker, skip re-resolution - Rename applyIssueTypes → applyIssueType (singular) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Verifies Type and Parent only appear in the interactive picker when Allowed is explicitly set. Prevents regression where issue-only fields leak into gh pr edit's interactive mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Restore // TODO projectsV1Deprecation comment above TestProjectsV1Deprecation (was displaced when new tests were inserted at that location) - Add 'relationships unsupported on GHES' test case to edit command using DisabledDetectorMock (parity with create's GHES tests) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ions The AddBlockedByPayload and RemoveBlockedByPayload types expose the result as 'issue', not 'blockedIssue'. Found during live spec testing against github.com — the mutations returned empty responses. Updated mutation queries and corresponding test fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Issues 2.0 support: issue types, sub-issues, and relationships
Closes #10298
Closes #9696
Closes #11757
Closes #12477
Closes #12152
Adds support for GitHub's Issues 2.0 features across
gh issue create,edit,view, andlist.New flags
gh issue create--type Bug--parent 100--blocked-by 200,201--blocking 300Interactive mode prompts for issue type when the repo has types configured.
gh issue edit--type Bug--set-parent 100--remove-parent--add-sub-issue 123,124--remove-sub-issue 123--add-blocked-by 200--remove-blocked-by 201--add-blocking 300,301--remove-blocking 300Interactive mode adds Type and Parent to the field picker. Batch editing
supported for
--typeacross multiple issues.gh issue viewTTY output now displays:
Bug · Open)Sub-issues · 3/5 (60%))JSON output supports new fields:
issueType,parent,subIssues,subIssuesSummary,blockedBy,blocking.gh issue list--type BugGHES compatibility
__typeschema introspection for theblockedByfield on the Issue type. Unsupported GHES versions receive a clear error message. Gated with// TODO IssueRelationshipsCleanupfor cleanup when GHES 3.18 support ends.Implementation details
--blockingand--add-blockingswap API arguments: callsaddBlockedBy(issueId=OTHER, blockingIssueId=THIS)--parent,--blocked-by,--blocking, and sub-issue flagspkg/cmd/issue/shared/resolve.go(ResolveIssueRef,ResolveIssueTypeName)Allowedin the Editable pattern to prevent leaking intogh pr edittype:"Bug Report")Testing
Changes
17 files changed, 2105 insertions(+), 18 deletions(-)
api/queries_issue.goapi/query_builder.goissueOnlyFieldsapi/export_pr.gointernal/featuredetection/IssueRelationshipsSupporteddetectionpkg/cmd/issue/create/create.gopkg/cmd/issue/edit/edit.gopkg/cmd/issue/view/view.gopkg/cmd/issue/list/list.go--typefilter via search pathpkg/cmd/issue/shared/resolve.goResolveIssueRef,ResolveIssueTypeNamepkg/cmd/pr/shared/editable.goIssueType/Parentfields,Allowedgatingpkg/cmd/pr/shared/params.goIssueTypeinFilterOptionsand search queryKey notes for reviewers
--blockingarg swap: The GitHub API only hasaddBlockedBy(issueId, blockingIssueId). To support--blocking, we swap the arguments — the OTHER issue becomesissueIdand THIS issue becomesblockingIssueId. Same for--remove-blocking.Allowedgating:IssueTypeandParentare added to the sharedEditablestruct but gated behindAllowed = true, which only the issue edit command sets. This prevents these issue-only fields from leaking intogh pr edit's interactive picker. Regression test insurvey_test.go.blockedByandblockingare only added to the GraphQL query whenIssueRelationshipsSupportedis true. Without this,gh issue viewwould break on GHES 3.17–3.18 where these fields don't exist.--type, the type name is resolved to an ID once before the loop rather than per-issue, avoiding N-1 redundantRepoIssueTypesAPI calls.Manual test results
All scenarios tested against on github.com.
24/24 scenarios passing (click to expand)
Issue types
create-with-typegh issue create --title "..." --type Bugcreate-error-invalid-typegh issue create --title "Oops" --type Bugztype "Bugz" not found; available types: Task, Bug, Featureedit-change-typegh issue edit 1 --type Featurecreate-interactive-typegh issue create(interactive)Sub-issues
create-with-parentgh issue create --title "..." --parent 3edit-set-parentgh issue edit 5 --set-parent 3--json parentedit-remove-parentgh issue edit 5 --remove-parentedit-add-sub-issuesgh issue edit 3 --add-sub-issue 6,7--json subIssuesedit-remove-sub-issuegh issue edit 3 --remove-sub-issue 6edit-error-mutual-parentgh issue edit 5 --set-parent 1 --remove-parentspecify only one of --set-parent or --remove-parentRelationships
create-with-relationshipsgh issue create --blocked-by 8 --blocking 9edit-add-remove-blocked-bygh issue edit 14 --add-blocked-by 12 --remove-blocked-by 13edit-add-blockinggh issue edit 14 --add-blocking 15,16View
view-with-typeBug · Openon state line,Type: Bugrowview-parent-sub-issuesview-child-parentview-relationshipsview-json-issueType--json issueTypereturns id, name, description, colorview-json-parent-subIssues--json parent,subIssues,subIssuesSummaryreturns structured dataview-json-relationships--json blockedBy,blockingreturns arrays with number, title, url, stateList
list-filter-by-typegh issue list --type BugCombined
create-all-flagsgh issue create --type Task --parent 3 --blocked-by 8 --blocking 9edit-batch-typegh issue edit 18 19 20 --type Bugedit-interactivegh issue edit 21(interactive)TODO: GHES-specific scenarios (
create-error-ghes-relationships), but GHES feature detection does have unit test coverage with mock introspection responses.