Skip to content

Commit 2b099db

Browse files
committed
Auto labeling PRs with subsystem labels.
Started out with two labels in this iteration: c++ and test.
1 parent a5e4799 commit 2b099db

File tree

6 files changed

+201
-2
lines changed

6 files changed

+201
-2
lines changed

lib/node-labels.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict'
2+
3+
const subSystemLabels = {
4+
'c++': /^src\//
5+
}
6+
7+
const rxTests = /^test\//
8+
9+
function resolveLabels (filepathsChanged) {
10+
if (isOnly(rxTests, filepathsChanged)) {
11+
return ['test']
12+
}
13+
14+
return matchBySubSystemRegex(filepathsChanged)
15+
}
16+
17+
function isOnly (regexToMatch, filepathsChanged) {
18+
return filepathsChanged.every((filepath) => regexToMatch.test(filepath))
19+
}
20+
21+
function matchBySubSystemRegex (filepathsChanged) {
22+
// by putting matched labels into a map, we avoid duplicate labels
23+
const labelsMap = filepathsChanged.reduce((map, filepath) => {
24+
const mappedSubSystem = mappedSubSystemForFile(filepath)
25+
26+
if (mappedSubSystem) {
27+
map[mappedSubSystem] = true
28+
}
29+
30+
return map
31+
}, {})
32+
33+
return Object.keys(labelsMap)
34+
}
35+
36+
function mappedSubSystemForFile (filepath) {
37+
return Object.keys(subSystemLabels).find((labelName) => {
38+
const rxForLabel = subSystemLabels[labelName]
39+
40+
return rxForLabel.test(filepath) ? labelName : undefined
41+
})
42+
}
43+
44+
exports.resolveLabels = resolveLabels

lib/node-repo.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict'
2+
3+
const githubClient = require('./github-client')
4+
5+
const resolveLabels = require('./node-labels').resolveLabels
6+
7+
function resolveLabelsThenUpdatePr (options) {
8+
githubClient.pullRequests.getFiles({
9+
user: options.owner,
10+
repo: options.repo,
11+
number: options.prId
12+
}, (err, res) => {
13+
if (err) {
14+
return console.error(`! ${prInfoStr(options)} Error retrieving files`, err)
15+
}
16+
17+
const filepathsChanged = res.map((fileMeta) => fileMeta.filename)
18+
updatePrWithLabels(options, resolveLabels(filepathsChanged))
19+
})
20+
}
21+
22+
function updatePrWithLabels (options, labels) {
23+
// no need to request github if we didn't resolve any labels
24+
if (!labels.length) {
25+
return
26+
}
27+
28+
fetchExistingLabels(options, (err, existingLabels) => {
29+
if (err) {
30+
return
31+
}
32+
33+
const mergedLabels = labels.concat(existingLabels)
34+
35+
githubClient.issues.edit({
36+
user: options.owner,
37+
repo: options.repo,
38+
number: options.prId,
39+
labels: mergedLabels
40+
}, (err) => {
41+
if (err) {
42+
return console.error(`! ${prInfoStr(options)} Error while adding labels`, err)
43+
}
44+
45+
console.log(`! ${prInfoStr(options)} Added labels: ${labels}`)
46+
})
47+
})
48+
}
49+
50+
function fetchExistingLabels (options, cb) {
51+
githubClient.issues.getIssueLabels({
52+
user: options.owner,
53+
repo: options.repo,
54+
number: options.prId
55+
}, (err, res) => {
56+
if (err) {
57+
console.error(`! ${prInfoStr(options)} Error while fetching existing labels`, err)
58+
return cb(err)
59+
}
60+
61+
const existingLabels = res.map((labelMeta) => labelMeta.name)
62+
cb(null, existingLabels)
63+
})
64+
}
65+
66+
function prInfoStr (options) {
67+
return `${options.owner}/${options.repo}/#${options.prId}`
68+
}
69+
70+
exports.resolveLabelsThenUpdatePr = resolveLabelsThenUpdatePr

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"description": "Node.js GitHub Bot",
55
"scripts": {
66
"start": "node server.js",
7-
"test": "standard"
7+
"test": "tap test/*.test.js && standard",
8+
"test:watch": "nodemon -q -x 'npm test'"
89
},
910
"engines": {
1011
"node": ">= 4.2.0"
@@ -22,6 +23,8 @@
2223
},
2324
"devDependencies": {
2425
"eventsource": "^0.2.1",
25-
"standard": "^6.0.7"
26+
"nodemon": "^1.9.1",
27+
"standard": "^6.0.7",
28+
"tap": "^5.7.1"
2629
}
2730
}

scripts/node-subsystem-label.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
const debug = require('debug')('node_subsystem_label')
4+
5+
const nodeRepo = require('../lib/node-repo')
6+
7+
module.exports = function (app) {
8+
app.on('pull_request.opened', handlePrCreated)
9+
10+
function handlePrCreated (event, owner, repo) {
11+
const prId = event.number
12+
// subsystem labelling is for node core only
13+
if (repo !== 'node') return
14+
15+
debug(`/${owner}/${repo}/pull/${prId} opened`)
16+
// by not hard coding the owner repo to nodejs/node here,
17+
// we can test these this script in a different repo than
18+
// *actual* node core as long as the repo is named "node"
19+
nodeRepo.resolveLabelsThenUpdatePr({ owner, repo, prId })
20+
}
21+
}

server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ require('dotenv').load({ silent: true })
55
const glob = require('glob')
66
const express = require('express')
77
const bodyParser = require('body-parser')
8+
89
const captureRaw = (req, res, buffer) => { req.raw = buffer }
910

1011
const app = express()

test/node-labels.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict'
2+
3+
const tap = require('tap')
4+
5+
const nodeLabels = require('../lib/node-labels')
6+
7+
tap.test('label: "test" when only ./test/ files has been changed', (t) => {
8+
const labels = nodeLabels.resolveLabels([
9+
'test/debugger/test-debugger-pid.js',
10+
'test/debugger/test-debugger-repl-break-in-module.js',
11+
'test/debugger/test-debugger-repl-term.js'
12+
])
13+
14+
t.same(labels, ['test'])
15+
16+
t.end()
17+
})
18+
19+
tap.test('label: "c++" when ./src/* has been changed', (t) => {
20+
const labels = nodeLabels.resolveLabels([
21+
'src/async-wrap.h',
22+
'src/async-wrap.cc'
23+
])
24+
25+
t.same(labels, ['c++'])
26+
27+
t.end()
28+
})
29+
30+
//
31+
// Planned tests to be resolved later
32+
//
33+
34+
tap.test('label: "repl" when ./lib/repl.js has been changed', (t) => {
35+
const labels = nodeLabels.resolveLabels([
36+
'lib/repl.js',
37+
'test/debugger/test-debugger-pid.js',
38+
'test/debugger/test-debugger-repl-break-in-module.js',
39+
'test/debugger/test-debugger-repl-term.js'
40+
])
41+
42+
t.same(labels, ['repl'], { todo: true })
43+
44+
t.end()
45+
})
46+
47+
tap.test('label: "lib / src" when more than 5 sub-systems has been changed', (t) => {
48+
const labels = nodeLabels.resolveLabels([
49+
'lib/assert.js',
50+
'lib/dns.js',
51+
'lib/repl.js',
52+
'lib/process.js',
53+
'src/async-wrap.cc',
54+
'lib/module.js'
55+
])
56+
57+
t.same(labels, ['lib / src'], { todo: true })
58+
59+
t.end()
60+
})

0 commit comments

Comments
 (0)