fix(pr create): use login-based assignee mutation on github.com

When ActorAssignees is true (github.com), pass assignee logins directly
to the ReplaceActorsForAssignable mutation instead of resolving logins
to node IDs. This eliminates the need to bulk fetch all assignable
users/actors and fixes a bug where providing assignees via CLI flag
and then interactively adding metadata would fail with 'not found'
because the cached MetadataResult had no assignee data.

Changes:
- Set state.ActorAssignees = true in pr create (was missing)
- AddMetadataToIssueParams: pass assigneeLogins when ActorAssignees
  is true, skip fetch and ID resolution entirely
- CreatePullRequest/IssueCreate: call ReplaceActorsForAssignableByLogin
  after creation to assign via logins
- Consolidate replaceActorsForAssignable mutation into api/ package
  (ReplaceActorsForAssignableByLogin + ReplaceActorsForAssignableByID)
- Remove duplicate replaceActorAssigneesForEditable from editable_http.go
- Add TODO replaceActorsByLoginCleanup markers on edit paths

Fixes cli/cli#13000

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kynan Ware 2026-03-23 15:21:20 -06:00
parent 8723e3bb52
commit e6d9019bc9
8 changed files with 165 additions and 87 deletions

View file

@ -289,7 +289,8 @@ func IssueCreate(client *Client, repo *Repository, params map[string]interface{}
switch key {
case "assigneeIds", "body", "issueTemplate", "labelIds", "milestoneId", "projectIds", "repositoryId", "title":
inputParams[key] = val
case "projectV2Ids":
case "projectV2Ids", "assigneeLogins":
// handled after issue creation
default:
return nil, fmt.Errorf("invalid IssueCreate mutation parameter %s", key)
}
@ -310,6 +311,14 @@ 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).
if assigneeLogins, ok := params["assigneeLogins"].([]string); ok && len(assigneeLogins) > 0 {
err := ReplaceActorsForAssignableByLogin(client, repo, issue.ID, assigneeLogins)
if err != nil {
return issue, err
}
}
// projectV2 parameters aren't supported in the `createIssue` mutation,
// so add them after the issue has been created.
projectV2Ids, ok := params["projectV2Ids"].([]string)

View file

@ -524,6 +524,14 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
}
}
// Assign users using login-based mutation when ActorAssignees 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
// Request reviewers using either login-based (github.com) or ID-based (GHES) mutation.
// The ID-based path can be removed once GHES supports requestReviewsByLogin.
@ -581,6 +589,62 @@ func CreatePullRequest(client *Client, repo *Repository, params map[string]inter
return pr, nil
}
// ReplaceActorsForAssignableByLogin calls the replaceActorsForAssignable mutation
// using actor logins. This avoids the need to resolve logins to node IDs.
func ReplaceActorsForAssignableByLogin(client *Client, repo ghrepo.Interface, assignableID string, logins []string) error {
type ReplaceActorsForAssignableInput struct {
AssignableID githubv4.ID `json:"assignableId"`
ActorLogins []githubv4.String `json:"actorLogins"`
}
actorLogins := make([]githubv4.String, len(logins))
for i, l := range logins {
actorLogins[i] = githubv4.String(l)
}
variables := map[string]interface{}{
"input": ReplaceActorsForAssignableInput{
AssignableID: githubv4.ID(assignableID),
ActorLogins: actorLogins,
},
}
return replaceActorsForAssignable(client, repo, variables)
}
// ReplaceActorsForAssignableByID calls the replaceActorsForAssignable mutation
// using actor node IDs. Used for GHES and edit flows that resolve IDs from search results.
func ReplaceActorsForAssignableByID(client *Client, repo ghrepo.Interface, assignableID string, actorIDs []string) error {
type ReplaceActorsForAssignableInput struct {
AssignableID githubv4.ID `json:"assignableId"`
ActorIDs []githubv4.ID `json:"actorIds"`
}
ids := make([]githubv4.ID, len(actorIDs))
for i, id := range actorIDs {
ids[i] = githubv4.ID(id)
}
variables := map[string]interface{}{
"input": ReplaceActorsForAssignableInput{
AssignableID: githubv4.ID(assignableID),
ActorIDs: ids,
},
}
return replaceActorsForAssignable(client, repo, variables)
}
func replaceActorsForAssignable(client *Client, repo ghrepo.Interface, variables map[string]interface{}) error {
var mutation struct {
ReplaceActorsForAssignable struct {
TypeName string `graphql:"__typename"`
} `graphql:"replaceActorsForAssignable(input: $input)"`
}
return client.Mutate(repo.RepoHost(), "ReplaceActorsForAssignable", &mutation, variables)
}
// SuggestedAssignableActors fetches up to 10 suggested actors for a specific assignable
// (Issue or PullRequest) node ID. `assignableID` is the GraphQL node ID for the Issue/PR.
// Returns the actors, the total count of available assignees in the repo, and an error.