-
Notifications
You must be signed in to change notification settings - Fork 119
/
node-repo.js
191 lines (159 loc) · 5.27 KB
/
node-repo.js
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
/* eslint-disable camelcase */
import https from 'node:https'
import Aigle from 'aigle'
import githubClient from './github-client.js'
import { createPrComment } from './github-comment.js'
import { Owners } from './node-owners.js'
const fiveSeconds = 5 * 1000
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
export async function removeLabelFromPR (options, label) {
// no need to request github if we didn't resolve a label
if (!label) {
return
}
options.logger.debug('Trying to remove label: ' + label)
try {
await githubClient.issues.removeLabel({
owner: options.owner,
repo: options.repo,
issue_number: options.prId,
name: label
})
} catch (err) {
if (err.code === 404) {
options.logger.info('Label to remove did not exist, bailing ' + label)
throw err
}
options.logger.error(err, 'Error while removing a label')
throw err
}
options.logger.info('Removed a label ' + label)
return label
}
export function getBotPrLabels (options, cb) {
githubClient.issues.listEvents({
owner: options.owner,
repo: options.repo,
page: 1,
per_page: 100, // we probably won't hit this
issue_number: options.prId
}).then(res => {
const events = res.data || []
const ourLabels = []
for (const event of events) {
if (event.event === 'unlabeled') {
const index = ourLabels.indexOf(event.label.name)
if (index === -1) continue
ourLabels.splice(index, 1)
} else if (event.event === 'labeled') {
const index = ourLabels.indexOf(event.label.name)
if (index > -1) continue
if (event.actor.login === 'nodejs-github-bot') {
ourLabels.push(event.label.name)
}
}
}
cb(null, ourLabels)
}, cb)
}
async function deferredResolveOwnersThenPingPr (options) {
const timeoutMillis = (options.timeoutInSec || 0) * 1000
await sleep(timeoutMillis)
return resolveOwnersThenPingPr(options)
}
function getCodeOwnersUrl (owner, repo, defaultBranch) {
const base = 'raw.githubusercontent.com'
const filepath = '.github/CODEOWNERS'
return `https://${base}/${owner}/${repo}/${defaultBranch}/${filepath}`
}
async function listFiles ({ owner, repo, prId, logger }) {
try {
const response = await githubClient.pulls.listFiles({
owner,
repo,
pull_number: prId
})
return response.data.map(({ filename }) => filename)
} catch (err) {
logger.error(err, 'Error retrieving files from GitHub')
throw err
}
}
async function getDefaultBranch ({ owner, repo, logger }) {
try {
const data = (await githubClient.repos.get({ owner, repo })).data || { }
if (!data.default_branch) {
logger.error(null, 'Could not determine default branch')
throw new Error('unknown default branch')
}
return data.default_branch
} catch (err) {
logger.error(err, 'Error retrieving repository data')
throw err
}
}
function getCodeOwnersFile (url, { logger }) {
return new Promise((resolve, reject) => {
https.get(url, (res) => {
if (res.statusCode !== 200) {
logger.error(`Status code: ${res.statusCode}`, 'Error retrieving OWNERS')
return reject(res.statusCode)
}
let body = ''
res.on('data', (chunk) => {
body += chunk
})
res.on('end', () => {
return resolve(body)
})
}).on('err', (err) => {
logger.error(err, 'Error retrieving OWNERS')
return reject(err)
})
})
}
async function resolveOwnersThenPingPr (options) {
const { owner, repo } = options
const times = options.retries || 5
const interval = options.retryInterval || fiveSeconds
const retry = fn => Aigle.retry({ times, interval }, fn)
options.logger.debug('Getting file paths')
const filepathsChanged = await retry(() => listFiles(options))
options.logger = options.logger.child({ filepathsChanged })
options.logger.debug('Getting default branch')
const defaultBranch = await retry(() => getDefaultBranch(options))
const url = getCodeOwnersUrl(owner, repo, defaultBranch)
options.logger = options.logger.child({ codeownersUrl: url })
options.logger.debug('Fetching OWNERS')
const file = await retry(() => getCodeOwnersFile(url, options))
const owners = Owners.fromFile(file)
const selectedOwners = owners.getOwnersForPaths(filepathsChanged)
options.logger = options.logger.child({ owners: selectedOwners })
options.logger.debug('Codeowners file parsed')
if (selectedOwners.length > 0) {
await pingOwners(options, selectedOwners)
}
}
function getCommentForOwners (owners) {
return `Review requested:\n\n${owners.map(i => `- [ ] ${i}`).join('\n')}`
}
async function pingOwners (options, owners) {
options.logger.debug('Pinging codeowners')
try {
await createPrComment({
owner: options.owner,
repo: options.repo,
issue_number: options.prId,
logger: options.logger
}, getCommentForOwners(owners))
} catch (err) {
options.logger.error(err, 'Error while pinging owners')
throw err
}
options.logger.debug('Owners pinged successfully')
}
export { deferredResolveOwnersThenPingPr as resolveOwnersThenPingPr }
// exposed for testability
export const _testExports = {
pingOwners, getCodeOwnersFile, getCodeOwnersUrl, getDefaultBranch, listFiles, getCommentForOwners
}