Repository: cli/cli (Go)

Issue:
Duplicate Review Requests When Using `gh pr edit --add-reviewer`
### Describe the bug

When running the command `gh pr edit --add-reviewer`, the specified reviewer receives two review requests instead of one. This causes the reviewer to approve the same pull request twice.

### Affected version

```
gh version 2.74.1 (2025-06-10)
https://github.com/cli/cli/releases/tag/v2.74.1
```

### Steps to reproduce the behavior

- Run the following command: `gh pr edit <PR_NUMBER> --add-reviewer <REVIEWER_IDS>`

### Expected vs actual behavior

#### Expected
- The reviewer should receive only one review request
- The reviewer should need to approve only once

#### Actual
- The reviewer receives two separate review requests
- The reviewer must approve the PR twice for it to be fully approved

### Logs

```
[git remote -v]
[git config --get-regexp ^remote\..*\.gh-resolved$]
* Request at 2025-06-11 06:47:10.13476036[9](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:10) +0000 UTC m=+0.060470440
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 657
> Content-Type: application/json; charset=utf-8
> Graphql-Features: merge_queue
> Time-Zone: Etc/UTC
> User-Agent: GitHub CLI 2.74.0

GraphQL query:
query PullRequestByNumber($owner: String!, $repo: String!, $pr_number: Int!) {
    repository(owner: $owner, name: $repo) {
      pullRequest(number: $pr_number) {id,url,title,body,baseRefName,reviewRequests(first: [10](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:11)0) {nodes {requestedReviewer {__typename,...on User{login},...on Team{organization{login}name,slug}}}},labels(first:100){nodes{id,name,description,color},totalCount},milestone{number,title,description,dueOn},assignedActors(first: 10) {nodes {...on User {id,login,name,__typename}...on Bot {id,login,__typename}},totalCount},number}
    }
  }
GraphQL variables: {"owner":"Mildwhale","pr_number":62,"repo":"app-deploy-slack-bot"}

< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, [11](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:12) Jun 2025 06:47:10 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A009:223FC2:1F97927:3F1BC29:6849266E
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4997
< X-Ratelimit-Reset: 1749628029
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 3
< X-Xss-Protection: 0

{
  "data": {
    "repository": {
      "pullRequest": {
        "id": "PR_kwDOFb5pX86Z-PaA",
        "url": "https://github.com/Mildwhale/app-deploy-slack-bot/pull/62",
        "title": "zzz",
        "body": "",
        "baseRefName": "master",
        "reviewRequests": {
          "nodes": []
        },
        "labels": {
          "nodes": [],
          "totalCount": 0
        },
        "milestone": null,
        "assignedActors": {
          "nodes": [],
          "totalCount": 0
        },
        "number": 62
      }
    }
  }
}

* Request took 216.91861ms
* Request at 2025-06-11 06:47:10.352963763 +0000 UTC m=+0.278673824
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 470
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: Etc/UTC
> User-Agent: GitHub CLI 2.74.0

GraphQL query:
query PullRequestProjectItems($endCursor:String$name:String!$number:Int!$owner:String!){repository(owner: $owner, name: $name){pullRequest(number: $number){projectItems(first: 100, after: $endCursor){nodes{id,project{id,title},status:fieldValueByName(name: "Status"){... on ProjectV2ItemFieldSingleSelectValue{optionId,name}}},pageInfo{hasNextPage,endCursor}}}}}
GraphQL variables: {"endCursor":null,"name":"app-deploy-slack-bot","number":62,"owner":"Mildwhale"}

< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, 11 Jun 2025 06:47:10 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A009:223FC2:1F97A33:3F1BE32:6849266E
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4996
< X-Ratelimit-Reset: 1749628029
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 4
< X-Xss-Protection: 0

{
  "data": {
    "repository": {
      "pullRequest": {
        "projectItems": {
          "nodes": [],
          "pageInfo": {
            "hasNextPage": false,
            "endCursor": null
          }
        }
      }
    }
  }
}

* Request took 174.997079ms
* Request at 2025-06-11 06:47:10.528196507 +0000 UTC m=+0.453906588
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 45
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: Etc/UTC
> User-Agent: GitHub CLI 2.74.0

{
  "query": "query UserCurrent{viewer{login}}"
}

* Request at 2025-06-11 06:47:10.528506341 +0000 UTC m=+0.4542164[12](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:13)
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 401
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: Etc/UTC
> User-Agent: GitHub CLI 2.74.0

GraphQL query:
query RepositoryAssignableActors($endCursor:String$name:String!$owner:String!){repository(owner: $owner, name: $name){suggestedActors(first: 100, after: $endCursor, capabilities: CAN_BE_ASSIGNED){nodes{... on User{id,login,name,__typename},... on Bot{id,login,__typename}},pageInfo{hasNextPage,endCursor}}}}
GraphQL variables: {"endCursor":null,"name":"app-deploy-slack-bot","owner":"Mildwhale"}

* Request at 2025-06-11 06:47:10.528706338 +0000 UTC m=+0.454416419
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 278
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: Etc/UTC
> User-Agent: GitHub CLI 2.74.0

GraphQL query:
query OrganizationTeamList($endCursor:String$owner:String!){organization(login: $owner){teams(first: 100, orderBy: {field: NAME, direction: ASC}, after: $endCursor){nodes{id,slug},pageInfo{hasNextPage,endCursor}}}}
GraphQL variables: {"endCursor":null,"owner":"Mildwhale"}

< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, 11 Jun 2025 06:47:10 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A009:223FC2:1F97B1F:3F1C009:6849266E
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4995
< X-Ratelimit-Reset: 1749628029
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 5
< X-Xss-Protection: 0

{
  "data": {
    "organization": null
  },
  "errors": [
    {
      "type": "NOT_FOUND",
      "path": [
        "organization"
      ],
      "locations": [
        {
          "line": 1,
          "column": 61
        }
      ],
      "message": "Could not resolve to an Organization with the login of 'Mildwhale'."
    }
  ]
}

* Request took 57.76367ms
< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, 11 Jun 2025 06:47:10 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A009:223FC2:1F97B1E:3F1C002:6849266E
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4994
< X-Ratelimit-Reset: 1749628029
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 6
< X-Xss-Protection: 0

{
  "data": {
    "viewer": {
      "login": "github-actions[bot]"
    }
  }
}

* Request took 124.497[14](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:15)8ms
< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, 11 Jun 2025 06:47:10 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=3[15](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:16)36000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A009:223FC2:1F97B1F:3F1C005:6849266E
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4993
< X-Ratelimit-Reset: [17](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:18)49628029
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 7
< X-Xss-Protection: 0

{
  "data": {
    "repository": {
      "suggestedActors": {
        "nodes": [
          {
            "id": "MDQ6VXNlcjMxMTM4MTA=",
            "login": "Mildwhale",
            "name": "Kyujin Kim",
            "__typename": "User"
          }
        ],
        "pageInfo": {
          "hasNextPage": false,
          "endCursor": "Mg"
        }
      }
    }
  }
}

* Request took 334.424312ms
* Request at 2025-06-11 06:47:10.863264[18](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:19)2 +0000 UTC m=+0.788974253
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 277
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: Etc/UTC
> User-Agent: GitHub CLI 2.74.0

GraphQL query:
mutation PullRequestUpdateRequestReviews($input:RequestReviewsInput!){requestReviews(input: $input){pullRequest{id}}}
GraphQL variables: {"input":{"pullRequestId":"PR_kwDOFb5pX86Z-PaA","userIds":["MDQ6VXNlcjMxMTM4MTA=","MDQ6VXNlcjMxMTM4MTA="],"teamIds":[],"union":false}}

* Request at 2025-06-11 06:47:10.864124385 +0000 UTC m=+0.789834456
* Request to https://api.github.com/graphql
> POST /graphql HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Length: 179
> Content-Type: application/json
> Graphql-Features: merge_queue
> Time-Zone: Etc/UTC
> User-Agent: GitHub CLI 2.74.0

GraphQL query:
mutation PullRequestUpdate($input:UpdatePullRequestInput!){updatePullRequest(input: $input){__typename}}
GraphQL variables: {"input":{"pullRequestId":"PR_kwDOFb5pX86Z-PaA"}}

< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, 11 Jun 2025 06:47:11 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A009:223FC2:1F97CE8:3F1C35E:6849266E
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4991
< X-Ratelimit-Reset: 1749628029
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 9
< X-Xss-Protection: 0

{
  "data": {
    "requestReviews": {
      "pullRequest": {
        "id": "PR_kwDOFb5pX86Z-PaA"
      }
    }
  },
  "extensions": {
    "warnings": [
      {
        "type": "DEPRECATION",
        "message": "The id MDQ6VXNlcjMxMTM4MTA= is deprecated. Update your cache to use the next_global_id from the data payload.",
        "data": {
          "legacy_global_id": "MDQ6VXNlcjMxMTM4MTA=",
          "next_global_id": "U_kgDOAC-DUg"
        },
        "link": "https://docs.github.com"
      },
      {
        "type": "DEPRECATION",
        "message": "The id MDQ6VXNlcjMxMTM4MTA= is deprecated. Update your cache to use the next_global_id from the data payload.",
        "data": {
          "legacy_global_id": "MDQ6VXNlcjMxMTM4MTA=",
          "next_global_id": "U_kgDOAC-DUg"
        },
        "link": "https://docs.github.com"
      }
    ]
  }
}

* Request took 273.085601ms
< HTTP/2.0 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Expose-Headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset
< Content-Security-Policy: default-src 'none'
< Content-Type: application/json; charset=utf-8
< Date: Wed, 11 Jun 2025 06:47:11 GMT
< Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
< Server: github.com
< Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
< Vary: Accept-Encoding, Accept, X-Requested-With
< X-Content-Type-Options: nosniff
< X-Frame-Options: deny
< X-Github-Media-Type: github.v4; param=merge-info-preview.nebula-preview; format=json
< X-Github-Request-Id: A009:223FC2:1F97CE9:3F1C362:6849266E
< X-Ratelimit-Limit: 5000
< X-Ratelimit-Remaining: 4992
< X-Ratelimit-Reset: 1749628029
< X-Ratelimit-Resource: graphql
< X-Ratelimit-Used: 8
< X-Xss-Protection: 0

{
  "data": {
    "updatePullRequest": {
      "__typename": "UpdatePullRequestPayload"
    }
  }
}

* Request took 300.5679[19](https://github.com/Mildwhale/app-deploy-slack-bot/actions/runs/15577973994/job/43866492547#step:5:20)ms
```

<!-- Note: Set `GH_DEBUG=true` for verbose logs or `GH_DEBUG=api` for verbose logs with HTTP traffic details. -->

### Log Analysis

While analyzing the internal GraphQL request made by the CLI, I found that the userIds field contains the same user ID twice, which likely causes the duplication:

```graphql
GraphQL query:
mutation PullRequestUpdateRequestReviews($input:RequestReviewsInput!){requestReviews(input: $input){pullRequest{id}}}
GraphQL variables: {"input":{"pullRequestId":"PR_kwDOJ1e7tM6Z-KSE","userIds":["MDQ6VXNlcjMxMTM4MTA=","MDQ6VXNlcjMxMTM4MTA="],"teamIds":["T_kwDOAsZIZs4Afapn"],"union":false}}
```
As shown above, the same userId is passed twice in the mutation request.


Discussion:
Thanks for the detailed report. Was this an issue in 2.74.0?
> Thanks for the detailed report. Was this an issue in 2.74.0?

Yes, I confirmed that the issue also exists in version 2.74.0 when run in a GitHub Actions environment.
For reference, I reproduced it with v2.74.1 via CLI as well.

Ok then we probably accidentally introduced it in v2.73.0 when we did work around supporting copilot as an issue assignee, https://github.com/cli/cli/releases/tag/v2.73.0 thanks!
The bug is in this function https://github.com/cli/cli/blob/49eee93a34068da337c312114d5fb424c1bc9f88/api/queries_repo.go#L699-L730

If we find a match in the first loop for `assignableUsers` we should not proceed to `assignableActors`.
@Mildwhale if you're open to trying out the branch https://github.com/cli/cli/pull/11099 from [source](https://github.com/cli/cli/blob/49eee93a34068da337c312114d5fb424c1bc9f88/docs/source.md) that would be great, but I think this fixes the bug.
@williammartin Sounds good! I'll try out #11099 soon and let you know!
@williammartin Tried out #11099 — looks like the issue's fixed. Thanks a lot for the quick fix!



Patch:
diff --git a/api/queries_repo.go b/api/queries_repo.go
index efbcfcb197d..3190745ea99 100644
--- a/api/queries_repo.go
+++ b/api/queries_repo.go
@@ -709,19 +709,22 @@ func (m *RepoMetadataResult) MembersToIDs(names []string) ([]string, error) {
 		}
 
 		// Look for ID in assignable actors if not found in assignable users
-		for _, a := range m.AssignableActors {
-			if strings.EqualFold(assigneeLogin, a.Login()) {
-				ids = append(ids, a.ID())
-				found = true
-				break
-			}
-			if strings.EqualFold(assigneeLogin, a.DisplayName()) {
-				ids = append(ids, a.ID())
-				found = true
-				break
+		if !found {
+			for _, a := range m.AssignableActors {
+				if strings.EqualFold(assigneeLogin, a.Login()) {
+					ids = append(ids, a.ID())
+					found = true
+					break
+				}
+				if strings.EqualFold(assigneeLogin, a.DisplayName()) {
+					ids = append(ids, a.ID())
+					found = true
+					break
+				}
 			}
 		}
 
+		// And if we still didn't find an ID, return an error
 		if !found {
 			return nil, fmt.Errorf("'%s' not found", assigneeLogin)
 		}


Test patch:
diff --git a/api/queries_repo_test.go b/api/queries_repo_test.go
index 9040a001802..c291fc4686a 100644
--- a/api/queries_repo_test.go
+++ b/api/queries_repo_test.go
@@ -461,6 +461,78 @@ t001: team(slug:"robots"){id,slug}
 	}
 }
 
+func TestMembersToIDs(t *testing.T) {
+	t.Parallel()
+
+	t.Run("finds ids in assignable users", func(t *testing.T) {
+		t.Parallel()
+
+		repoMetadataResult := RepoMetadataResult{
+			AssignableUsers: []AssignableUser{
+				NewAssignableUser("MONAID", "monalisa", ""),
+				NewAssignableUser("MONAID2", "monalisa2", ""),
+			},
+			AssignableActors: []AssignableActor{
+				NewAssignableBot("HUBOTID", "hubot"),
+			},
+		}
+		ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
+		require.NoError(t, err)
+		require.Equal(t, []string{"MONAID"}, ids)
+	})
+
+	t.Run("finds ids by assignable actor logins", func(t *testing.T) {
+		t.Parallel()
+
+		repoMetadataResult := RepoMetadataResult{
+			AssignableActors: []AssignableActor{
+				NewAssignableBot("HUBOTID", "hubot"),
+				NewAssignableUser("MONAID", "monalisa", ""),
+			},
+		}
+		ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
+		require.NoError(t, err)
+		require.Equal(t, []string{"MONAID"}, ids)
+	})
+
+	t.Run("finds ids by assignable actor display names", func(t *testing.T) {
+		t.Parallel()
+
+		repoMetadataResult := RepoMetadataResult{
+			AssignableActors: []AssignableActor{
+				NewAssignableUser("MONAID", "monalisa", "mona"),
+			},
+		}
+		ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa (mona)"})
+		require.NoError(t, err)
+		require.Equal(t, []string{"MONAID"}, ids)
+	})
+
+	t.Run("when a name appears in both assignable users and actors, the id is only returned once", func(t *testing.T) {
+		t.Parallel()
+
+		repoMetadataResult := RepoMetadataResult{
+			AssignableUsers: []AssignableUser{
+				NewAssignableUser("MONAID", "monalisa", ""),
+			},
+			AssignableActors: []AssignableActor{
+				NewAssignableUser("MONAID", "monalisa", ""),
+			},
+		}
+		ids, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
+		require.NoError(t, err)
+		require.Equal(t, []string{"MONAID"}, ids)
+	})
+
+	t.Run("when id is not found, returns an error", func(t *testing.T) {
+		t.Parallel()
+
+		repoMetadataResult := RepoMetadataResult{}
+		_, err := repoMetadataResult.MembersToIDs([]string{"monalisa"})
+		require.Error(t, err)
+	})
+}
+
 func sliceEqual(a, b []string) bool {
 	if len(a) != len(b) {
 		return false