Skip to content

Commit af8b06a

Browse files
committed
feat: ping reviewers based on which files were changed
Using a custom OWNERS file, `github-bot` will ping the appropriate teams based on which files were changed in a Pull Request. This feature is inteded to work around GitHub's limitation which prevents teams without explicit write access from being added as reviewers (thus preventing the vast majority of teams in the org from being used on GitHub's CODEOWNERS feature). The OWNERS file is a yaml file (to simplify parsing) composed of key-value paris. The key is always a string and represents a glob of the changed files. The value is always an array of strings, and each string is a team to be pinged. Ref: nodejs/node#33984
1 parent b5d80bd commit af8b06a

File tree

5 files changed

+1530
-1205
lines changed

5 files changed

+1530
-1205
lines changed

lib/node-owners.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use static'
2+
3+
const yaml = require('js-yaml')
4+
const micromatch = require('micromatch')
5+
6+
class Owners {
7+
constructor (ownersDefinitions) {
8+
console.log(ownersDefinitions)
9+
this._ownersDefinitions = ownersDefinitions
10+
}
11+
12+
static fromYaml (content) {
13+
console.log(content)
14+
return new Owners(yaml.safeLoad(content))
15+
}
16+
17+
getOwnersForPaths (paths) {
18+
let owners = []
19+
for (const ownersGlob of Object.keys(this._ownersDefinitions)) {
20+
console.log(paths, ownersGlob, micromatch(paths, ownersGlob))
21+
if (micromatch(paths, ownersGlob).length > 0) {
22+
owners = owners.concat(this._ownersDefinitions[ownersGlob])
23+
}
24+
}
25+
// Remove duplicates before returning
26+
return owners.filter((v, i) => owners.indexOf(v) === i)
27+
}
28+
}
29+
30+
module.exports = {
31+
Owners
32+
}

lib/node-repo.js

+73-11
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,88 @@
11
'use strict'
22

3+
const request = require('request')
34
const LRU = require('lru-cache')
45
const retry = require('async').retry
6+
const debug = require('debug')('node_repo')
57

68
const githubClient = require('./github-client')
79
const resolveLabels = require('./node-labels').resolveLabels
10+
const { Owners } = require('./node-owners')
11+
const { createPrComment } = require('./github-comment')
812
const existingLabelsCache = new LRU({ max: 1, maxAge: 1000 * 60 * 60 })
913

1014
const fiveSeconds = 5 * 1000
1115

12-
function deferredResolveLabelsThenUpdatePr (options) {
13-
const timeoutMillis = (options.timeoutInSec || 0) * 1000
14-
setTimeout(resolveLabelsThenUpdatePr, timeoutMillis, options)
15-
}
16-
17-
function resolveLabelsThenUpdatePr (options) {
16+
function getFilepathsChanged (options, handleFilepaths) {
1817
options.logger.debug('Fetching PR files for labelling')
1918

20-
const getFiles = (cb) => {
19+
const getPRFiles = (cb) => {
2120
githubClient.pullRequests.getFiles({
2221
owner: options.owner,
2322
repo: options.repo,
2423
number: options.prId
2524
}, cb)
2625
}
2726

28-
retry({ times: 5, interval: fiveSeconds }, getFiles, (err, res) => {
27+
retry({ times: 5, interval: fiveSeconds }, getPRFiles, (err, res) => {
2928
if (err) {
3029
return options.logger.error(err, 'Error retrieving files from GitHub')
3130
}
32-
3331
const filepathsChanged = res.data.map((fileMeta) => fileMeta.filename)
34-
const resolvedLabels = resolveLabels(filepathsChanged, options.baseBranch)
3532

36-
fetchExistingThenUpdatePr(options, resolvedLabels)
33+
handleFilepaths(options, filepathsChanged)
3734
})
3835
}
3936

37+
function deferredResolveOwnersThenPingPr (options) {
38+
const timeoutMillis = (options.timeoutInSec || 0) * 1000
39+
setTimeout(() => getFilepathsChanged(options, resolveOwnersThenPingPr), timeoutMillis, options)
40+
}
41+
42+
function resolveOwnersThenPingPr (options, filepathsChanged) {
43+
const { owner, repo } = options
44+
githubClient.repos.get({
45+
owner,
46+
repo
47+
}, (err, res) => {
48+
if (err) {
49+
return options.logger.error(err, 'Error retrieving repository data')
50+
}
51+
const data = res.data || {}
52+
if (!data['default_branch']) {
53+
return options.logger.error(err, 'Couldn\' determine default branch')
54+
}
55+
56+
const { default_branch: defaultBranch } = data
57+
58+
const url = `https://raw.githubusercontent.com/${owner}/${repo}/${defaultBranch}/OWNERS.yml`
59+
debug(`Fetching OWNERS on ${url}`)
60+
request(url, (err, res, body) => {
61+
if (err || !res || res.statusCode >= 400) {
62+
return options.logger.error(err, 'Error retrieving OWNERS')
63+
}
64+
const owners = Owners.fromYaml(body)
65+
const selectedOwners = owners.getOwnersForPaths(filepathsChanged)
66+
console.log(selectedOwners)
67+
if (selectedOwners.length > 0) {
68+
pingOwners(options, selectedOwners)
69+
}
70+
console.log('done')
71+
})
72+
})
73+
}
74+
75+
function deferredResolveLabelsThenUpdatePr (options) {
76+
const timeoutMillis = (options.timeoutInSec || 0) * 1000
77+
setTimeout(() => getFilepathsChanged(options, resolveLabelsThenUpdatePr), timeoutMillis, options)
78+
}
79+
80+
function resolveLabelsThenUpdatePr (options, filepathsChanged) {
81+
const resolvedLabels = resolveLabels(filepathsChanged, options.baseBranch)
82+
83+
fetchExistingThenUpdatePr(options, resolvedLabels)
84+
}
85+
4086
function fetchExistingThenUpdatePr (options, labels) {
4187
fetchExistingLabels(options, (err, existingLabels) => {
4288
if (err) {
@@ -53,6 +99,21 @@ function fetchExistingThenUpdatePr (options, labels) {
5399
})
54100
}
55101

102+
function pingOwners (options, owners) {
103+
createPrComment({
104+
owner: options.owner,
105+
repo: options.repo,
106+
number: options.prId,
107+
logger: options.logger
108+
}, `Review requested:\n\n${owners.map(i => `- [ ] ${i}`).join('\n')}`, (err) => {
109+
if (err) {
110+
return options.logger.error(err, 'Error while pinging owners')
111+
}
112+
113+
options.logger.info('Pinged owners: ' + owners)
114+
})
115+
}
116+
56117
function updatePrWithLabels (options, labels) {
57118
// no need to request github if we didn't resolve any labels
58119
if (!labels.length) {
@@ -192,6 +253,7 @@ exports.getBotPrLabels = getBotPrLabels
192253
exports.removeLabelFromPR = removeLabelFromPR
193254
exports.fetchExistingThenUpdatePr = fetchExistingThenUpdatePr
194255
exports.resolveLabelsThenUpdatePr = deferredResolveLabelsThenUpdatePr
256+
exports.resolveOwnersThenPingPr = deferredResolveOwnersThenPingPr
195257

196258
// exposed for testability
197259
exports._fetchExistingLabels = fetchExistingLabels

0 commit comments

Comments
 (0)