Skip to content

Commit

Permalink
feat: ping reviewers based on which files were changed
Browse files Browse the repository at this point in the history
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
Ref: nodejs/node#34150
  • Loading branch information
mmarchini committed Jul 1, 2020
1 parent b5d80bd commit 6ccbba9
Show file tree
Hide file tree
Showing 5 changed files with 1,530 additions and 1,205 deletions.
32 changes: 32 additions & 0 deletions lib/node-owners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use static'

const yaml = require('js-yaml')
const micromatch = require('micromatch')

class Owners {
constructor (ownersDefinitions) {
console.log(ownersDefinitions)
this._ownersDefinitions = ownersDefinitions
}

static fromYaml (content) {
console.log(content)
return new Owners(yaml.safeLoad(content))
}

getOwnersForPaths (paths) {
let owners = []
for (const ownersGlob of Object.keys(this._ownersDefinitions)) {
console.log(paths, ownersGlob, micromatch(paths, ownersGlob))
if (micromatch(paths, ownersGlob).length > 0) {
owners = owners.concat(this._ownersDefinitions[ownersGlob])
}
}
// Remove duplicates before returning
return owners.filter((v, i) => owners.indexOf(v) === i)
}
}

module.exports = {
Owners
}
84 changes: 73 additions & 11 deletions lib/node-repo.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,88 @@
'use strict'

const request = require('request')
const LRU = require('lru-cache')
const retry = require('async').retry
const debug = require('debug')('node_repo')

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

const fiveSeconds = 5 * 1000

function deferredResolveLabelsThenUpdatePr (options) {
const timeoutMillis = (options.timeoutInSec || 0) * 1000
setTimeout(resolveLabelsThenUpdatePr, timeoutMillis, options)
}

function resolveLabelsThenUpdatePr (options) {
function getFilepathsChanged (options, handleFilepaths) {
options.logger.debug('Fetching PR files for labelling')

const getFiles = (cb) => {
const getPRFiles = (cb) => {
githubClient.pullRequests.getFiles({
owner: options.owner,
repo: options.repo,
number: options.prId
}, cb)
}

retry({ times: 5, interval: fiveSeconds }, getFiles, (err, res) => {
retry({ times: 5, interval: fiveSeconds }, getPRFiles, (err, res) => {
if (err) {
return options.logger.error(err, 'Error retrieving files from GitHub')
}

const filepathsChanged = res.data.map((fileMeta) => fileMeta.filename)
const resolvedLabels = resolveLabels(filepathsChanged, options.baseBranch)

fetchExistingThenUpdatePr(options, resolvedLabels)
handleFilepaths(options, filepathsChanged)
})
}

function deferredResolveOwnersThenPingPr (options) {
const timeoutMillis = (options.timeoutInSec || 0) * 1000
setTimeout(() => getFilepathsChanged(options, resolveOwnersThenPingPr), timeoutMillis, options)
}

function resolveOwnersThenPingPr (options, filepathsChanged) {
const { owner, repo } = options
githubClient.repos.get({
owner,
repo
}, (err, res) => {
if (err) {
return options.logger.error(err, 'Error retrieving repository data')
}
const data = res.data || {}
if (!data['default_branch']) {
return options.logger.error(err, 'Couldn\' determine default branch')
}

const { default_branch: defaultBranch } = data

const url = `https://raw.githubusercontent.com/${owner}/${repo}/${defaultBranch}/OWNERS.yml`
debug(`Fetching OWNERS on ${url}`)
request(url, (err, res, body) => {
if (err || !res || res.statusCode >= 400) {
return options.logger.error(err, 'Error retrieving OWNERS')
}
const owners = Owners.fromYaml(body)
const selectedOwners = owners.getOwnersForPaths(filepathsChanged)
console.log(selectedOwners)
if (selectedOwners.length > 0) {
pingOwners(options, selectedOwners)
}
console.log('done')
})
})
}

function deferredResolveLabelsThenUpdatePr (options) {
const timeoutMillis = (options.timeoutInSec || 0) * 1000
setTimeout(() => getFilepathsChanged(options, resolveLabelsThenUpdatePr), timeoutMillis, options)
}

function resolveLabelsThenUpdatePr (options, filepathsChanged) {
const resolvedLabels = resolveLabels(filepathsChanged, options.baseBranch)

fetchExistingThenUpdatePr(options, resolvedLabels)
}

function fetchExistingThenUpdatePr (options, labels) {
fetchExistingLabels(options, (err, existingLabels) => {
if (err) {
Expand All @@ -53,6 +99,21 @@ function fetchExistingThenUpdatePr (options, labels) {
})
}

function pingOwners (options, owners) {
createPrComment({
owner: options.owner,
repo: options.repo,
number: options.prId,
logger: options.logger
}, `Review requested:\n\n${owners.map(i => `- [ ] ${i}`).join('\n')}`, (err) => {
if (err) {
return options.logger.error(err, 'Error while pinging owners')
}

options.logger.info('Pinged owners: ' + owners)
})
}

function updatePrWithLabels (options, labels) {
// no need to request github if we didn't resolve any labels
if (!labels.length) {
Expand Down Expand Up @@ -192,6 +253,7 @@ exports.getBotPrLabels = getBotPrLabels
exports.removeLabelFromPR = removeLabelFromPR
exports.fetchExistingThenUpdatePr = fetchExistingThenUpdatePr
exports.resolveLabelsThenUpdatePr = deferredResolveLabelsThenUpdatePr
exports.resolveOwnersThenPingPr = deferredResolveOwnersThenPingPr

// exposed for testability
exports._fetchExistingLabels = fetchExistingLabels

0 comments on commit 6ccbba9

Please sign in to comment.