-
Notifications
You must be signed in to change notification settings - Fork 66.9k
Expand file tree
/
Copy pathready-for-docs-review.ts
More file actions
289 lines (262 loc) · 9.07 KB
/
ready-for-docs-review.ts
File metadata and controls
289 lines (262 loc) · 9.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import { graphql } from '@octokit/graphql'
import {
addItemToProject,
isDocsTeamMember,
isGitHubOrgMember,
findFieldID,
findSingleSelectID,
generateUpdateProjectV2ItemFieldMutation,
getFeature,
getSize,
} from './projects'
/**
* Determines if a PR is authored by Copilot and extracts the human assignee
* @param data GraphQL response data containing PR information
* @returns Object with isCopilotAuthor boolean and copilotAssignee string
*/
function getCopilotAuthorInfo(data: Record<string, unknown>): {
isCopilotAuthor: boolean
copilotAssignee: string
} {
const item = data.item as Record<string, unknown>
const author = item.author as Record<string, unknown> | undefined
const assigneesObj = item.assignees as Record<string, unknown> | undefined
// Check if this is a Copilot-authored PR
const isCopilotAuthor = !!(
item.__typename === 'PullRequest' &&
author &&
author.login === 'copilot-swe-agent'
)
// For Copilot PRs, find the appropriate assignee (excluding Copilot itself)
let copilotAssignee = ''
if (isCopilotAuthor && assigneesObj && assigneesObj.nodes) {
const nodes = assigneesObj.nodes as Array<Record<string, unknown>>
const assigneeLogins = nodes
.map((assignee: Record<string, unknown>) => assignee.login as string)
.filter((login: string) => login !== 'copilot-swe-agent')
// Use the first non-Copilot assignee
copilotAssignee = assigneeLogins.length > 0 ? assigneeLogins[0] : ''
}
return { isCopilotAuthor, copilotAssignee: copilotAssignee || '' }
}
/**
* Determines the appropriate author field value based on contributor type
* @param isCopilotAuthor Whether the PR is authored by Copilot
* @param copilotAssignee The human assignee for Copilot PRs (empty string if none)
* @param firstTimeContributor Whether this is a first-time contributor
* @returns The formatted author field value
*/
function getAuthorFieldValue(
isCopilotAuthor: boolean,
copilotAssignee: string,
firstTimeContributor: boolean | undefined,
): string {
if (isCopilotAuthor) {
return copilotAssignee ? `Copilot + ${copilotAssignee}` : 'Copilot'
}
if (firstTimeContributor) {
return ':star: first time contributor'
}
return process.env.AUTHOR_LOGIN || ''
}
async function run() {
// Get info about the docs-content review board project
const data: Record<string, unknown> = await graphql(
`
query ($organization: String!, $projectNumber: Int!, $id: ID!) {
organization(login: $organization) {
projectV2(number: $projectNumber) {
id
fields(first: 100) {
nodes {
... on ProjectV2Field {
id
name
}
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
item: node(id: $id) {
__typename
... on PullRequest {
files(first: 100) {
nodes {
additions
deletions
path
}
}
author {
login
}
assignees(first: 10) {
nodes {
login
}
}
}
}
}
`,
{
id: process.env.ITEM_NODE_ID,
organization: process.env.ORGANIZATION,
projectNumber: parseInt(process.env.PROJECT_NUMBER || ''),
headers: {
authorization: `token ${process.env.TOKEN}`,
},
},
)
// Get the project ID
const organization = data.organization as Record<string, unknown>
const projectV2 = organization.projectV2 as Record<string, unknown>
const projectID = projectV2.id as string
// Get the ID of the fields that we want to populate
const datePostedID = findFieldID('Date posted', data)
const reviewDueDateID = findFieldID('Review due date', data)
const statusID = findFieldID('Status', data)
const featureID = findFieldID('Feature', data)
const contributorTypeID = findFieldID('Contributor type', data)
const sizeTypeID = findFieldID('Size', data)
const authorID = findFieldID('Contributor', data)
// Get the ID of the single select values that we want to set
const readyForReviewID = findSingleSelectID('Ready for review', 'Status', data)
const hubberTypeID = findSingleSelectID('Hubber or partner', 'Contributor type', data)
const docsMemberTypeID = findSingleSelectID('Docs team', 'Contributor type', data)
const osContributorTypeID = findSingleSelectID('OS contributor', 'Contributor type', data)
// Add the PR to the project
const newItemID = await addItemToProject(process.env.ITEM_NODE_ID || '', projectID)
// Determine the feature and size
const feature = getFeature(data)
const size = getSize(data)
const sizeType = findSingleSelectID(size, 'Size', data)
// If this is the OS repo, determine if this is a first time contributor
// If yes, set the author to 'first time contributor' instead of to the author login
let firstTimeContributor
if (process.env.REPO === 'github/docs') {
const contributorData: Record<string, unknown> = await graphql(
`
query ($author: String!) {
user(login: $author) {
contributionsCollection {
pullRequestContributionsByRepository {
contributions {
totalCount
}
repository {
nameWithOwner
}
}
issueContributionsByRepository {
contributions {
totalCount
}
repository {
nameWithOwner
}
}
}
}
}
`,
{
author: process.env.AUTHOR_LOGIN,
headers: {
authorization: `token ${process.env.TOKEN}`,
},
},
)
const user = contributorData.user as Record<string, unknown>
const contributionsCollection = user.contributionsCollection as Record<string, unknown>
const pullRequestContributions =
contributionsCollection.pullRequestContributionsByRepository as Array<Record<string, unknown>>
const docsPRData = pullRequestContributions.filter((item: Record<string, unknown>) => {
const repository = item.repository as Record<string, unknown>
return repository.nameWithOwner === 'github/docs'
})[0]
const prContributions = docsPRData
? (docsPRData.contributions as Record<string, unknown>)
: undefined
const prCount = prContributions ? (prContributions.totalCount as number) : 0
const issueContributions = contributionsCollection.issueContributionsByRepository as Array<
Record<string, unknown>
>
const docsIssueData = issueContributions.filter((item: Record<string, unknown>) => {
const repository = item.repository as Record<string, unknown>
return repository.nameWithOwner === 'github/docs'
})[0]
const issueContributionsObj = docsIssueData
? (docsIssueData.contributions as Record<string, unknown>)
: undefined
const issueCount = issueContributionsObj ? (issueContributionsObj.totalCount as number) : 0
if (prCount + issueCount <= 1) {
firstTimeContributor = true
}
}
const turnaround = process.env.REPO === 'github/docs' ? 3 : 2
// Check if this is a Copilot-authored PR and get the human assignee
const { isCopilotAuthor, copilotAssignee } = getCopilotAuthorInfo(data)
// Determine the author field value
const authorFieldValue = getAuthorFieldValue(
isCopilotAuthor,
copilotAssignee,
firstTimeContributor,
)
// Generate a mutation to populate fields for the new project item
const updateProjectV2ItemMutation = generateUpdateProjectV2ItemFieldMutation({
item: newItemID,
author: authorFieldValue,
turnaround,
feature,
})
// Determine which variable to use for the contributor type
let contributorType
if (isCopilotAuthor) {
// Treat Copilot PRs as Docs team
contributorType = docsMemberTypeID
} else if (await isDocsTeamMember(process.env.AUTHOR_LOGIN || '')) {
contributorType = docsMemberTypeID
} else if (await isGitHubOrgMember(process.env.AUTHOR_LOGIN || '')) {
contributorType = hubberTypeID
} else if (process.env.REPO === 'github/docs') {
contributorType = osContributorTypeID
} else {
// use hubber as the fallback so that the PR doesn't get lost on the board
contributorType = hubberTypeID
}
console.log(`Populating fields for item: ${newItemID}`)
await graphql(updateProjectV2ItemMutation, {
project: projectID,
statusID,
statusValueID: readyForReviewID,
datePostedID,
reviewDueDateID,
contributorTypeID,
contributorType,
sizeTypeID,
sizeType,
featureID,
authorID,
headers: {
authorization: `token ${process.env.TOKEN}`,
},
})
console.log('Done populating fields for item')
return newItemID
}
export { run }
try {
await run()
} catch (error) {
console.log(`#ERROR# ${error}`)
process.exit(1)
}