Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/queries_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ func IssueCreate(client *Client, repo *Repository, params map[string]interface{}
}
issue := &result.CreateIssue.Issue

// Assign users using login-based mutation when ActorAssignees is true (github.com).
// Assign users using login-based mutation when ApiActorsSupported is true (github.com).
if assigneeLogins, ok := params["assigneeLogins"].([]string); ok && len(assigneeLogins) > 0 {
err := ReplaceActorsForAssignableByLogin(client, repo, issue.ID, assigneeLogins)
if err != nil {
Expand Down
10 changes: 8 additions & 2 deletions api/queries_pr.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,15 +524,15 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
}
}

// Assign users using login-based mutation when ActorAssignees is true (github.com).
// Assign users using login-based mutation when ApiActorsSupported is true (github.com).
if assigneeLogins, ok := params["assigneeLogins"].([]string); ok && len(assigneeLogins) > 0 {
err := ReplaceActorsForAssignableByLogin(client, repo, pr.ID, assigneeLogins)
if err != nil {
return pr, err
}
}

// TODO requestReviewsByLoginCleanup
// TODO ApiActorsSupported
// Request reviewers using either login-based (github.com) or ID-based (GHES) mutation.
// The ID-based path can be removed once GHES supports requestReviewsByLogin.
userLogins, hasUserLogins := params["userReviewerLogins"].([]string)
Expand Down Expand Up @@ -599,6 +599,12 @@ func ReplaceActorsForAssignableByLogin(client *Client, repo ghrepo.Interface, as

actorLogins := make([]githubv4.String, len(logins))
for i, l := range logins {
// The replaceActorsForAssignable mutation requires the [bot] suffix
// for bot actor logins (e.g. "copilot-swe-agent[bot]"), unlike
// requestReviewsByLogin which has a separate botLogins field.
if l == CopilotAssigneeLogin {
l = l + "[bot]"
}
actorLogins[i] = githubv4.String(l)
}

Expand Down
20 changes: 11 additions & 9 deletions api/queries_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -963,14 +963,15 @@ func (m *RepoMetadataResult) Merge(m2 *RepoMetadataResult) {
}

type RepoMetadataInput struct {
Assignees bool
ActorAssignees bool
Reviewers bool
TeamReviewers bool
Labels bool
ProjectsV1 bool
ProjectsV2 bool
Milestones bool
Assignees bool
Reviewers bool
TeamReviewers bool
// TODO ApiActorsSupported
ApiActorsSupported bool
Labels bool
ProjectsV1 bool
ProjectsV2 bool
Milestones bool
}

// RepoMetadata pre-fetches the metadata for attaching to issues and pull requests
Expand All @@ -979,7 +980,8 @@ func RepoMetadata(client *Client, repo ghrepo.Interface, input RepoMetadataInput
var g errgroup.Group

if input.Assignees || input.Reviewers {
if input.ActorAssignees {
// TODO ApiActorsSupported
if input.ApiActorsSupported {
g.Go(func() error {
actors, err := RepoAssignableActors(client, repo)
if err != nil {
Expand Down
147 changes: 66 additions & 81 deletions docs/triage.md
Original file line number Diff line number Diff line change
@@ -1,117 +1,102 @@
# Triage role

As we get more issues and pull requests opened on the GitHub CLI, we've decided on a weekly rotation triage role as defined by our First Responder (FR) rotation. The primary responsibility of the FR during that week is to triage incoming issues from the Open Source community, as defined below. An issue is considered "triaged" when the `needs-triage` label is removed.
The primary responsibility of the First Responder (FR) during their weekly rotation is to triage incoming issues and pull requests from the open source community. An issue is considered "triaged" when the `needs-triage` label is removed.

## Expectations for triaging incoming issues
## Quick Guide

Review and label [open issues missing either the `enhancement`, `bug`, or `docs` label](https://github.com/cli/cli/issues?q=is%3Aopen+is%3Aissue+-label%3Abug%2Cenhancement%2Cdocs+) and the label(s) corresponding to the command space prefixed with `gh-`, such as `gh-pr` for the `gh pr` command set, or `gh-extension` for the `gh extension` command set.
Pick an issue from the triage queue.

The heuristics for triaging the different issue types are as follows:
**Your goal:** Do what is needed to remove the `needs-triage` label.

### Bugs
1. **Can we close it?**
- Duplicate → Comment and close as duplicate, linking the original
- Spam → Add `invalid` or `suspected-spam` (auto-closes)
- Abuse → Add `invalid`, remove content, report, block (see [Spam and abuse](#spam-and-abuse))
- Off-topic → Add `off-topic` (auto-closes with comment)

For bugs, the FR should engage with the issue and community with the goal to remove the `needs-triage` label from the issue.
2. **Is it a bug?**
- Reproducible → Add `bug` and a priority label (`priority-1`, `priority-2`, or `priority-3`)
- Not reproducible → Add `unable-to-reproduce` (auto-requests info, 14-day timer)

To be considered triaged, `bug` issues require the following:
3. **Is it an enhancement?**
- Clear value → Add `enhancement` (auto-posts backlog comment)
- Unclear → Comment for clarification and add `more-info-needed` (14-day timer)

- A severity label `priority-1`, `priority-2`, and `priority-3`
- Clearly defined Acceptance Criteria, added to the Issue as a standalone comment (see [example](https://github.com/cli/cli/issues/9469#issuecomment-2292315743))
4. **Is it a pull request?** (see [Community pull requests](#community-pull-requests))
- Spam or AI sludge → Add `invalid` (auto-closes)
- Tiny fix (e.g., typo) → Review, test, and merge directly
- Not linked to a help-wanted issue → Add `no-help-wanted-issue` (auto-closes with comment)
- Valid → Add `ready-for-review` and run CI (auto-removes `needs-triage`, auto-posts acknowledging comment)

#### Bug severities
The `needs-triage` label is automatically removed when end-state labels (`enhancement`, `bug`, `ready-for-review`) are applied or the issue is closed.

| Severity | Description |
| - | - |
| `priority-1` | Affects a large population and inhibits work |
| `priority-2` | Affects more than a few users but doesn't prevent core functions |
| `priority-3` | Affects a small number of users or is largely cosmetic |

### Enhancements and Docs

For `enhancement` issues, the FR's role is to prepare the issue for team review and triage.

When a new issue is opened, the FR **should**:
## Bug Triage

- Acknowledge the issue
- Assign themselves to the issue
- Ensure there is enough information to understand the enhancement's scope and value
- Ask the user for more information about value and use-case, if necessary
- Leave the `needs-triage` label on the issue
- Add the `more-info-needed` and `needs-investigation` labels as needed
1. Try to reproduce the issue
2. If reproducible (or strongly suspect an intermittent bug) → add `bug` and a priority label
3. If not reproducible → add `unable-to-reproduce` (auto-requests info, 14-day timer) or request clarification with `more-info-needed`

When the FR has enough information to be triaged, they should:
- Remove the `more-info-needed` and `needs-investigation` labels
- Remove their assignment from the issue
### Bug Priorities

The FR should **avoid**:

- Thoroughly investigating the enhancement's technical feasibility
- Prematurely accepting the enhancement request
- Removing the `needs-triage` label
- Labeling issues as `help wanted`
| Priority | Description |
|----------|-------------|
| `priority-1` | Affects a large population and inhibits work. **Escalate internally via the appropriate incident channel; may require a hotfix.** |
| `priority-2` | Affects more than a few users but does not prevent core functions |
| `priority-3` | Affects a small number of users or is largely cosmetic |

## Additional triaging labels
## Enhancement Triage

The FR can consider adding any of the following labels below.
**Do:**
- Ensure the value is clear (ask if needed) and apply `more-info-needed` while waiting for clarification
- Apply the `enhancement` label once value is clear (auto-posts backlog comment)

| Label | Description |
| - | - |
| `discuss` | Some issues require discussion with the internal team. Adding this label will automatically open up an internal discussion with the team to facilitate this discussion. |
| `core` | Defines what we would like to do internally. We tend to lean towards `help wanted` by default, and adding `core` should be reserved for trickier issues or implementations we have strong opinions/preferences about. |
| `more-info-needed` | After asking any contributors for more information, add this label so it is clear that the issue has been responded to and we are waiting on the user. |
| `needs-investigation` | Used when the issue requires further investigation before it can be reviewed and triaged. This is often used for issues that are not clearly bugs or enhancements, or when the FR needs to gather more information before proceeding. |
| `invalid` | Added to spam and abusive issues. |
**Don't:**
- Deep-dive technical feasibility
- Prematurely accept or suggest the feature will be added

### Labels for team enhancement triaging
## Community Pull Requests

The FR should **avoid** adding these labels outside of team enhancement triage.
Community pull requests receive `needs-triage` (as well as `external`) just like issues do, but **are not meant to be reviewed as part of triage.**

| Label | Description |
| - | - |
| `good first issue` | Used to denote when an issue may be a good candidate for a first-time contributor to the CLI. These are usually small and well defined issues. |
| `help wanted` | These issues are ready for community contribution. |
| `help wanted candidate` | Used to denote when an issue may be a good candidate for community contribution. Issues labelled this way are discussed internally and may be promoted to `help wanted`. |
The triager's responsibility is to do a quick pass:

## Expectations for community pull requests
1. **Spam or AI sludge** → Add `invalid` label (auto-closes). Block user if necessary.
2. **Tiny mergeable fix** (e.g., typo) → Review, test, and merge.
3. **Not related to a help-wanted issue** → Add `no-help-wanted-issue` (auto-closes with comment).
4. **Valid for review** → Add `ready-for-review` and run CI (auto-removes `needs-triage`, auto-posts acknowledging comment).

All incoming pull requests are assigned to one of the engineers for review on a load-balanced basis.
The person in a triage role for a week could take a glance at these pull requests, mostly to see whether
the changeset is feasible and to allow the associated CI run for new contributors.
The pull request will be auto-assigned to an engineer on the team; that engineer will wait to review until `needs-triage` is removed.

## Spam and abuse
## Spam and Abuse

The primary goal of triaging spam and abuse is to remove distracting and offensive content from our community.

We get a lot of spam. Whenever you determine an issue as spam, add the `invalid` label and close it as "won't do". For spammy comments, simply mark them as spam using GitHub's built-in spam feature.

Abusive contributions are defined by our [Code of Conduct](../.github/CODE-OF-CONDUCT.md). Any contribution you determine abusive should be removed. Repeat offenses or particularly offensive abuse should be reported using GitHub's reporting features and the user blocked. If an entire issue is abusive, label it as `invalid` and close as "won't do".
- **Spam issues:** Add the `invalid` label (auto-closes as "won't do").
- **Spam comments:** Mark as spam using GitHub's built-in feature.
- **Abusive content:** Defined by our [Code of Conduct](../.github/CODE-OF-CONDUCT.md). Remove the content. Repeat offenses or particularly offensive abuse should be reported and the user blocked.

## Weekly PR audit
## Automated Workflows

In the interest of not letting our open PR list get out of hand (20+ total PRs _or_ multiple PRs
over a few months old), try to audit open PRs each week with the goal of getting them merged and/or
closed. It's likely too much work to deal with every PR, but even getting a few closer to done is
helpful.

For each PR, ask:

- is this too stale (more than two months old or too many conflicts)? close with comment
- is this really close but author is absent? push commits to finish, request review
- is this waiting on triage? go through the PR triage flow

## Useful aliases

This gist has some useful aliases for first responders:

https://gist.github.com/vilmibm/ee6ed8a783e4fef5b69b2ed42d743b1a
| Label | Automation |
|-------|------------|
| `needs-triage` | Auto-added on open; removed when classified or closed |
| `more-info-needed` | Auto-closes after 14 days without response |
| `unable-to-reproduce` | Auto-adds `more-info-needed` + posts comment |
| `enhancement` | Auto-posts backlog comment |
| `invalid` | Auto-closes immediately |
| `suspected-spam` | Auto-closes immediately |
| `off-topic` | Auto-posts explanation comment + closes |
| `no-help-wanted-issue` | Auto-posts explanation comment + closes |
| `ready-for-review` | Auto-removes `needs-triage` + posts acknowledging comment |

## Examples

We want our project to be a safe and encouraging open-source environment. Below are some examples
of how to empathetically respond to or close an issue/PR:
We want our project to be a safe and encouraging open-source environment. Below are some examples of how to empathetically respond to or close an issue/PR:

- [Closing a quality PR its scope is too large](https://github.com/cli/cli/pull/1161)
- [Closing a quality PR when its scope is too large](https://github.com/cli/cli/pull/1161)
- [Closing a stale PR](https://github.com/cli/cli/pull/557#issuecomment-639077269)
- [Closing a PR that doesn't follow our CONTRIBUTING policy](https://github.com/cli/cli/pull/864)
- [Responding to a bug report](https://github.com/desktop/desktop/issues/9195#issuecomment-592243129)
- [Closing an issue that out of scope](https://github.com/cli/cli/issues/777#issuecomment-612926229)
- [Closing an issue that is out of scope](https://github.com/cli/cli/issues/777#issuecomment-612926229)
- [Closing an issue with a feature request](https://github.com/desktop/desktop/issues/9722#issuecomment-625461766)

2 changes: 1 addition & 1 deletion internal/codespaces/portforwarder/port_forwarder.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func (fwd *CodespacesPortForwarder) UpdatePortVisibility(ctx context.Context, re
done := make(chan error)
go func() {
// Connect to the tunnel
err = fwd.connection.Connect(ctx)
err := fwd.connection.Connect(ctx)
if err != nil {
done <- fmt.Errorf("connect failed: %v", err)
return
Expand Down
30 changes: 27 additions & 3 deletions internal/featuredetection/feature_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,35 @@ type Detector interface {
}

type IssueFeatures struct {
ActorIsAssignable bool
// TODO ApiActorsSupported
// ApiActorsSupported indicates the host supports actor-based APIs. True for
// github.com and ghe.com, false for GHES.
//
// The GitHub API has two generations of assignee/reviewer types:
//
// Legacy (GHES): Uses AssignableUser (users only) and node-ID-based mutations.
// - assignableUsers query returns []AssignableUser
// - Mutations take node IDs (assigneeIds, userReviewerIds, teamReviewerIds)
//
// Actor-based (github.com): Uses AssignableActor (User + Bot union) and
// login-based mutations, enabling assignment of non-user actors like Copilot.
// - suggestedActors query returns []AssignableActor (User | Bot)
// - suggestedReviewerActors returns []ReviewerCandidate (User | Bot | Team)
// - Mutations take logins (replaceActorsForAssignable, requestReviewsByLogin)
//
// When GHES adds support for the actor-based types and mutations, this flag
// can be removed and all // TODO ApiActorsSupported sites collapsed to the
// actor-only path. To verify GHES support, check whether the GHES GraphQL
// schema includes:
// - The suggestedActors field on Repository (assignee search)
// - The suggestedReviewerActors field on PullRequest (reviewer search)
// - The replaceActorsForAssignable mutation
// - The requestReviewsByLogin mutation
ApiActorsSupported bool
}

var allIssueFeatures = IssueFeatures{
ActorIsAssignable: true,
ApiActorsSupported: true,
}

type PullRequestFeatures struct {
Expand Down Expand Up @@ -136,7 +160,7 @@ func (d *detector) IssueFeatures() (IssueFeatures, error) {
}

return IssueFeatures{
ActorIsAssignable: false, // replaceActorsForAssignable GraphQL mutation unavailable on GHES
ApiActorsSupported: false, // TODO ApiActorsSupported — actor-based mutations unavailable on GHES
}, nil
}

Expand Down
6 changes: 3 additions & 3 deletions internal/featuredetection/feature_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ func TestIssueFeatures(t *testing.T) {
name: "github.com",
hostname: "github.com",
wantFeatures: IssueFeatures{
ActorIsAssignable: true,
ApiActorsSupported: true,
},
wantErr: false,
},
{
name: "ghec data residency (ghe.com)",
hostname: "stampname.ghe.com",
wantFeatures: IssueFeatures{
ActorIsAssignable: true,
ApiActorsSupported: true,
},
wantErr: false,
},
{
name: "GHE",
hostname: "git.my.org",
wantFeatures: IssueFeatures{
ActorIsAssignable: false,
ApiActorsSupported: false,
},
wantErr: false,
},
Expand Down
20 changes: 10 additions & 10 deletions pkg/cmd/issue/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func createRun(opts *CreateOptions) (err error) {

// Replace special values in assignees
// For web mode, @copilot should be replaced by name; otherwise, login.
assigneeReplacer := prShared.NewSpecialAssigneeReplacer(apiClient, baseRepo.RepoHost(), issueFeatures.ActorIsAssignable, !opts.WebMode)
assigneeReplacer := prShared.NewSpecialAssigneeReplacer(apiClient, baseRepo.RepoHost(), issueFeatures.ApiActorsSupported, !opts.WebMode)
assignees, err := assigneeReplacer.ReplaceSlice(opts.Assignees)
if err != nil {
return err
Expand All @@ -188,14 +188,14 @@ func createRun(opts *CreateOptions) (err error) {
assigneeSet.AddValues(assignees)

tb := prShared.IssueMetadataState{
Type: prShared.IssueMetadata,
ActorAssignees: issueFeatures.ActorIsAssignable,
Assignees: assigneeSet.ToSlice(),
Labels: opts.Labels,
ProjectTitles: opts.Projects,
Milestones: milestones,
Title: opts.Title,
Body: opts.Body,
Type: prShared.IssueMetadata,
ApiActorsSupported: issueFeatures.ApiActorsSupported, // TODO ApiActorsSupported
Assignees: assigneeSet.ToSlice(),
Labels: opts.Labels,
ProjectTitles: opts.Projects,
Milestones: milestones,
Title: opts.Title,
Body: opts.Body,
}

if opts.RecoverFile != "" {
Expand Down Expand Up @@ -309,7 +309,7 @@ func createRun(opts *CreateOptions) (err error) {
State: &tb,
}
var assigneeSearchFunc func(string) prompter.MultiSelectSearchResult
if issueFeatures.ActorIsAssignable {
if issueFeatures.ApiActorsSupported {
assigneeSearchFunc = prShared.RepoAssigneeSearchFunc(apiClient, baseRepo)
}
err = prShared.MetadataSurvey(opts.Prompter, opts.IO, baseRepo, fetcher, &tb, projectsV1Support, nil, assigneeSearchFunc)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/issue/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ func Test_createRun(t *testing.T) {
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "ISSUEID", inputs["assignableId"])
assert.Equal(t, []interface{}{"copilot-swe-agent", "MonaLisa"}, inputs["actorLogins"])
assert.Equal(t, []interface{}{"copilot-swe-agent[bot]", "MonaLisa"}, inputs["actorLogins"])
}))
},
wantsStdout: "https://github.com/OWNER/REPO/issues/12\n",
Expand Down Expand Up @@ -1161,7 +1161,7 @@ func TestIssueCreate_AtCopilotAssignee(t *testing.T) {
{ "data": { "replaceActorsForAssignable": { "__typename": "" } } }
`, func(inputs map[string]interface{}) {
assert.Equal(t, "NEWISSUEID", inputs["assignableId"])
assert.Equal(t, []interface{}{"copilot-swe-agent"}, inputs["actorLogins"])
assert.Equal(t, []interface{}{"copilot-swe-agent[bot]"}, inputs["actorLogins"])
}))

output, err := runCommand(http, true, `-a @copilot -t hello -b "cash rules everything around me"`, nil)
Expand Down
Loading
Loading