nixpkgs mirror (for testing)
github.com/NixOS/nixpkgs
nix
1async function handleReviewers({
2 github,
3 context,
4 core,
5 log,
6 dry,
7 pull_request,
8 reviews,
9 user_maintainers,
10 team_maintainers,
11 owners,
12 getUser,
13 getTeam,
14}) {
15 const pull_number = pull_request.number
16
17 // Users that the PR has already reached, e.g. they've left a review or have been requested for one
18 const users_reached = new Set([
19 ...pull_request.requested_reviewers.map(({ login }) => login.toLowerCase()),
20 ...reviews.map(({ user }) => user.login.toLowerCase()),
21 ])
22 log('reviewers - users_reached', Array.from(users_reached).join(', '))
23
24 // Same for teams
25 const teams_reached = new Set([
26 ...pull_request.requested_teams.map(({ slug }) => slug.toLowerCase()),
27 ...reviews.flatMap(({ onBehalfOf }) =>
28 onBehalfOf.nodes.map(({ slug }) => slug.toLowerCase()),
29 ),
30 ])
31 log('reviewers - teams_reached', Array.from(teams_reached).join(', '))
32
33 // Early sanity check, before we start making any API requests. The list of maintainers
34 // does not have duplicates so the only user to filter out from this list would be the
35 // PR author. Therefore, we check for a limit of 15+1, where 15 is the limit we check
36 // further down again.
37 // This is to protect against huge treewides consuming all our API requests for no
38 // reason.
39 if (user_maintainers.length + team_maintainers.length > 16) {
40 core.warning('Too many potential reviewers, skipping review requests.')
41 // Return a boolean on whether the "needs: reviewers" label should be set.
42 return users_reached.size === 0 && teams_reached.size === 0
43 }
44
45 // Users that should be reached
46 var users_to_reach = new Set([
47 ...(
48 await Promise.all(
49 user_maintainers.map(async (id) => {
50 const user = await getUser(id)
51 // User may have deleted their account
52 return user?.login?.toLowerCase()
53 }),
54 )
55 ).filter(Boolean),
56 ...owners
57 .filter((handle) => handle && !handle.includes('/'))
58 .map((handle) => handle.toLowerCase()),
59 ])
60 // We can't request a review from the author.
61 .difference(new Set([pull_request.user?.login.toLowerCase()]))
62
63 // Filter users to repository collaborators. If they're not, they can't be requested
64 // for review. In that case, they probably missed their invite to the maintainers team.
65 users_to_reach = new Set(
66 (
67 await Promise.all(
68 Array.from(users_to_reach, async (username) => {
69 // TODO: Restructure this file to only do the collaborator check for those users
70 // who were not already part of a team. Being a member of a team makes them
71 // collaborators by definition.
72 try {
73 await github.rest.repos.checkCollaborator({
74 ...context.repo,
75 username,
76 })
77 return username
78 } catch (e) {
79 if (e.status !== 404) throw e
80 core.warning(
81 `PR #${pull_number}: User ${username} cannot be requested for review because they don't exist or are not a repository collaborator, ignoring. They probably missed the automated invite to the maintainers team (see <https://github.com/NixOS/nixpkgs/issues/234293>).`,
82 )
83 }
84 }),
85 )
86 ).filter(Boolean),
87 )
88 log('reviewers - users_to_reach', Array.from(users_to_reach).join(', '))
89
90 // Similar for teams
91 var teams_to_reach = new Set([
92 ...(
93 await Promise.all(
94 team_maintainers.map(async (id) => {
95 const team = await getTeam(id)
96 // Team may have been deleted
97 return team?.slug?.toLowerCase()
98 }),
99 )
100 ).filter(Boolean),
101 ...owners
102 .map((handle) => handle.split('/'))
103 .filter(
104 ([org, slug]) =>
105 org.toLowerCase() === context.repo.owner.toLowerCase() && slug,
106 )
107 .map(([, slug]) => slug.toLowerCase()),
108 ])
109 teams_to_reach = new Set(
110 (
111 await Promise.all(
112 Array.from(teams_to_reach, async (slug) => {
113 try {
114 await github.rest.teams.checkPermissionsForRepoInOrg({
115 org: context.repo.owner,
116 team_slug: slug,
117 owner: context.repo.owner,
118 repo: context.repo.repo,
119 })
120 return slug
121 } catch (e) {
122 if (e.status !== 404) throw e
123 core.warning(
124 `PR #${pull_number}: Team ${slug} cannot be requested for review because it doesn't exist or has no repository permissions, ignoring. Probably wasn't added to the nixpkgs-maintainers team (see https://github.com/NixOS/nixpkgs/tree/master/maintainers#maintainer-teams)`,
125 )
126 }
127 }),
128 )
129 ).filter(Boolean),
130 )
131 log('reviewers - teams_to_reach', Array.from(teams_to_reach).join(', '))
132
133 if (users_to_reach.size + teams_to_reach.size > 15) {
134 core.warning(
135 `Too many reviewers (users: ${Array.from(users_to_reach).join(', ')}, teams: ${Array.from(teams_to_reach).join(', ')}), skipping review requests.`,
136 )
137 // Return a boolean on whether the "needs: reviewers" label should be set.
138 return users_reached.size === 0 && teams_reached.size === 0
139 }
140
141 // We don't want to rerequest reviews from people who already reviewed or were requested
142 const users_not_yet_reached = Array.from(
143 users_to_reach.difference(users_reached),
144 )
145 log('reviewers - users_not_yet_reached', users_not_yet_reached.join(', '))
146 // We don't want to rerequest reviews from teams who already reviewed or were requested
147 const teams_not_yet_reached = Array.from(
148 teams_to_reach.difference(teams_reached),
149 )
150 log('reviewers - teams_not_yet_reached', teams_not_yet_reached.join(', '))
151
152 if (
153 users_not_yet_reached.length === 0 &&
154 teams_not_yet_reached.length === 0
155 ) {
156 log('Has reviewer changes', 'false (skipped)')
157 } else if (dry) {
158 core.info(
159 `Requesting user reviewers for #${pull_number}: ${users_not_yet_reached.join(', ')} (dry)`,
160 )
161 core.info(
162 `Requesting team reviewers for #${pull_number}: ${teams_not_yet_reached.join(', ')} (dry)`,
163 )
164 } else {
165 // We had tried the "request all reviewers at once" thing in the past, but it didn't work out:
166 // https://github.com/NixOS/nixpkgs/commit/034613f860fcd339bd2c20c8f6bc259a2f9dc034
167 // If we're hitting API errors here again, we'll need to investigate - and possibly reverse
168 // course.
169 await github.rest.pulls.requestReviewers({
170 ...context.repo,
171 pull_number,
172 reviewers: users_not_yet_reached,
173 team_reviewers: teams_not_yet_reached,
174 })
175 }
176
177 // Return a boolean on whether the "needs: reviewers" label should be set.
178 return (
179 users_not_yet_reached.length === 0 &&
180 teams_not_yet_reached.length === 0 &&
181 users_reached.size === 0 &&
182 teams_reached.size === 0
183 )
184}
185
186module.exports = {
187 handleReviewers,
188}