Skip to content

Commit 59d30c3

Browse files
committed
jenkins: POC for kicking off builds based on PR comments
This is the initial shot at triggering a Jenkins build when a repository collaborator mentions the bot in a comment with the following content: `@nodejs-github-bot run CI` In addition the bot has to be explicitly configured with $ENV variables per repo and related Jenkins build for this to be enabled. At first we'll start with https://github.com/nodejs/citgm which has been the biggest driver for getting this implemented. If that test run succeeds we can enable it on other repos as we see fit in collaboration with their corresponding working group. Refs nodejs#82
1 parent 31d0d2e commit 59d30c3

File tree

4 files changed

+160
-0
lines changed

4 files changed

+160
-0
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
1818
The webhook secret that GitHub signs the POSTed payloads with. This is created when the webhook is defined. The default is `hush-hush`.
1919
- **`TRAVIS_CI_TOKEN`**<br>
2020
For scripts that communicate with Travis CI. Your Travis token is visible on [yourprofile](https://travis-ci.org/profile) page, by clicking the "show token" link. Also See: https://blog.travis-ci.com/2013-01-28-token-token-token
21+
- **`JENKINS_API_CREDENTIALS`** (optional)<br>
22+
For scripts that communicate with Jenkins on http://ci.nodejs.org. The Jenkins API token is visible on
23+
your own profile page `https://ci.nodejs.org/user/<YOUR_GITHUB_USERNAME>/configure`, by clicking the
24+
"show API token" button. Also See: https://wiki.jenkins-ci.org/display/JENKINS/Authenticating+scripted+clients
25+
- **`JENKINS_JOB_URL_<REPO_NAME>`** (optional)<br>
26+
Only required for the trigger Jenkins build script, to know which job to trigger a build for when
27+
repository collaborator posts a comment to the bot. E.g. `JENKINS_JOB_URL_NODE=https://ci.nodejs.org/job/node-test-pull-request`
28+
- **`JENKINS_BUILD_TOKEN_<REPO_NAME>`** (optional)<br>
29+
Only required for the trigger Jenkins build script. The authentication token configured for a particular
30+
Jenkins job, for remote scripts to trigger builds remotely. Found on the job configuration page in
31+
`Build Triggers -> Trigger builds remotely (e.g., from scripts)`.
2132
- **`LOGIN_CREDENTIALS`**<br>
2233
Username and password used to protected the log files exposed in /logs. Expected format: `username:password`.
2334
- **`KEEP_LOGS`**<br>

lib/bot-username.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const memoize = require('async').memoize
2+
3+
const githubClient = require('./github-client')
4+
5+
function requestGitHubForUsername (cb) {
6+
githubClient.users.get({}, (err, currentUser) => {
7+
if (err) {
8+
return cb(err)
9+
}
10+
cb(null, currentUser.login)
11+
})
12+
}
13+
14+
exports.resolve = memoize(requestGitHubForUsername)

lib/github-events.js

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ module.exports = (app) => {
2727
app.emitGhEvent = function emitGhEvent (data, logger) {
2828
const repo = data.repository.name
2929
const org = data.repository.owner.login || data.organization.login
30+
31+
// Normalize how to fetch the PR / issue number for simpler retrieval in the
32+
// rest of the bot's code. For PRs the number is present in data.number,
33+
// but for webhook events raised for comments it's present in data.issue.number
34+
if (!data.number && data.issue) {
35+
data.number = data.issue.number
36+
}
37+
3038
const pr = data.number
3139

3240
// create unique logger which is easily traceable throughout the entire app

scripts/trigger-jenkins-build.js

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'use strict'
2+
3+
const request = require('request')
4+
5+
const githubClient = require('../lib/github-client')
6+
const botUsername = require('../lib/bot-username')
7+
8+
const jenkinsApiCredentials = process.env.JENKINS_API_CREDENTIALS || ''
9+
10+
function ifBotWasMentionedInCiComment (commentBody, cb) {
11+
botUsername.resolve((err, username) => {
12+
if (err) {
13+
return cb(err)
14+
}
15+
16+
const atBotName = new RegExp(`^@${username} run CI`, 'mi')
17+
const wasMentioned = commentBody.match(atBotName) !== null
18+
19+
cb(null, wasMentioned)
20+
})
21+
}
22+
23+
// URL to the Jenkins job should be triggered for a given repository
24+
function buildUrlForRepo (repo) {
25+
// e.g. JENKINS_JOB_URL_CITGM = https://ci.nodejs.org/job/citgm-continuous-integration-pipeline
26+
const jobUrl = process.env[`JENKINS_JOB_URL_${repo.toUpperCase()}`] || ''
27+
return jobUrl ? `${jobUrl}/build` : ''
28+
}
29+
30+
// Authentication token configured per Jenkins job needed when triggering a build,
31+
// this is set per job in Configure -> Build Triggers -> Trigger builds remotely
32+
function buildTokenForRepo (repo) {
33+
// e.g. JENKINS_BUILD_TOKEN_CITGM
34+
return process.env[`JENKINS_BUILD_TOKEN_${repo.toUpperCase()}`] || ''
35+
}
36+
37+
function triggerBuild (options, cb) {
38+
const { repo } = options
39+
const base64Credentials = new Buffer(jenkinsApiCredentials).toString('base64')
40+
const authorization = `Basic ${base64Credentials}`
41+
const buildParameters = [{
42+
name: 'GIT_REMOTE_REF',
43+
value: `refs/pull/${options.number}/head`
44+
}]
45+
const payload = JSON.stringify({ parameter: buildParameters })
46+
const uri = buildUrlForRepo(repo)
47+
const buildAuthToken = buildTokenForRepo(repo)
48+
49+
if (!uri) {
50+
return cb(new TypeError(`Will not trigger Jenkins build because $JENKINS_JOB_URL_${repo.toUpperCase()} is not set`))
51+
}
52+
53+
if (!buildAuthToken) {
54+
return cb(new TypeError(`Will not trigger Jenkins build because $JENKINS_BUILD_TOKEN_${repo.toUpperCase()} is not set`))
55+
}
56+
57+
options.logger.debug('Triggering Jenkins build')
58+
59+
request.post({
60+
uri,
61+
headers: { authorization },
62+
qs: { token: buildAuthToken },
63+
form: { json: payload }
64+
}, (err, response) => {
65+
if (err) {
66+
return cb(err)
67+
} else if (response.statusCode !== 201) {
68+
return cb(new Error(`Expected 201 from Jenkins, got ${response.statusCode}`))
69+
}
70+
71+
cb(null, response.headers.location)
72+
})
73+
}
74+
75+
function createPrComment ({ owner, repo, number, logger }, body) {
76+
githubClient.issues.createComment({
77+
owner,
78+
repo,
79+
number,
80+
body
81+
}, (err) => {
82+
if (err) {
83+
logger.error(err, 'Error while creating comment to reply on CI run comment')
84+
}
85+
})
86+
}
87+
88+
module.exports = (app) => {
89+
app.on('issue_comment.created', function handleCommentCreated (event, owner, repo) {
90+
const { number, logger, comment } = event
91+
const commentAuthor = comment.user.login
92+
const options = {
93+
owner,
94+
repo,
95+
number,
96+
logger
97+
}
98+
99+
function replyToCollabWithBuildStarted (err, buildUrl) {
100+
if (err) {
101+
logger.error(err, 'Error while triggering Jenkins build')
102+
return createPrComment(options, `@${commentAuthor} sadly an error occured when I tried to trigger a build :(`)
103+
}
104+
105+
createPrComment(options, `@${commentAuthor} build started: ${buildUrl}`)
106+
logger.info({ buildUrl }, 'Jenkins build started')
107+
}
108+
109+
function triggerBuildWhenCollaborator (err) {
110+
if (err) {
111+
return logger.debug(`Ignoring comment to me by @${commentAuthor} because they are not a repo collaborator`)
112+
}
113+
114+
triggerBuild(options, replyToCollabWithBuildStarted)
115+
}
116+
117+
ifBotWasMentionedInCiComment(comment.body, (err, wasMentioned) => {
118+
if (err) {
119+
return logger.error(err, 'Error while checking if the bot username was mentioned in a comment')
120+
}
121+
122+
if (!wasMentioned) return
123+
124+
githubClient.repos.checkCollaborator({ owner, repo, username: commentAuthor }, triggerBuildWhenCollaborator)
125+
})
126+
})
127+
}

0 commit comments

Comments
 (0)