diff --git a/README.md b/README.md index c0a14621..a9a7631c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,17 @@ See [CONTRIBUTING.md](CONTRIBUTING.md). The webhook secret that GitHub signs the POSTed payloads with. This is created when the webhook is defined. The default is `hush-hush`. - **`TRAVIS_CI_TOKEN`**
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 +- **`JENKINS_API_CREDENTIALS`** (optional)
+ For scripts that communicate with Jenkins on http://ci.nodejs.org. The Jenkins API token is visible on + your own profile page `https://ci.nodejs.org/user//configure`, by clicking the + "show API token" button. Also See: https://wiki.jenkins-ci.org/display/JENKINS/Authenticating+scripted+clients +- **`JENKINS_JOB_URL_`** (optional)
+ Only required for the trigger Jenkins build script, to know which job to trigger a build for when + repository collaborator posts a comment to the bot. E.g. `JENKINS_JOB_URL_NODE=https://ci.nodejs.org/job/node-test-pull-request` +- **`JENKINS_BUILD_TOKEN_`** (optional)
+ Only required for the trigger Jenkins build script. The authentication token configured for a particular + Jenkins job, for remote scripts to trigger builds remotely. Found on the job configuration page in + `Build Triggers -> Trigger builds remotely (e.g., from scripts)`. - **`LOGIN_CREDENTIALS`**
Username and password used to protected the log files exposed in /logs. Expected format: `username:password`. - **`KEEP_LOGS`**
diff --git a/lib/bot-username.js b/lib/bot-username.js new file mode 100644 index 00000000..fa726e75 --- /dev/null +++ b/lib/bot-username.js @@ -0,0 +1,14 @@ +const memoize = require('async').memoize + +const githubClient = require('./github-client') + +function requestGitHubForUsername (cb) { + githubClient.users.get({}, (err, currentUser) => { + if (err) { + return cb(err) + } + cb(null, currentUser.login) + }) +} + +exports.resolve = memoize(requestGitHubForUsername) diff --git a/lib/github-events.js b/lib/github-events.js index 79920a4d..b4bd6b9b 100644 --- a/lib/github-events.js +++ b/lib/github-events.js @@ -27,6 +27,14 @@ module.exports = (app) => { app.emitGhEvent = function emitGhEvent (data, logger) { const repo = data.repository.name const org = data.repository.owner.login || data.organization.login + + // Normalize how to fetch the PR / issue number for simpler retrieval in the + // rest of the bot's code. For PRs the number is present in data.number, + // but for webhook events raised for comments it's present in data.issue.number + if (!data.number && data.issue) { + data.number = data.issue.number + } + const pr = data.number // create unique logger which is easily traceable throughout the entire app diff --git a/scripts/trigger-jenkins-build.js b/scripts/trigger-jenkins-build.js new file mode 100644 index 00000000..80a53eea --- /dev/null +++ b/scripts/trigger-jenkins-build.js @@ -0,0 +1,127 @@ +'use strict' + +const request = require('request') + +const githubClient = require('../lib/github-client') +const botUsername = require('../lib/bot-username') + +const jenkinsApiCredentials = process.env.JENKINS_API_CREDENTIALS || '' + +function ifBotWasMentionedInCiComment (commentBody, cb) { + botUsername.resolve((err, username) => { + if (err) { + return cb(err) + } + + const atBotName = new RegExp(`^@${username} run CI`, 'mi') + const wasMentioned = commentBody.match(atBotName) !== null + + cb(null, wasMentioned) + }) +} + +// URL to the Jenkins job should be triggered for a given repository +function buildUrlForRepo (repo) { + // e.g. JENKINS_JOB_URL_CITGM = https://ci.nodejs.org/job/citgm-continuous-integration-pipeline + const jobUrl = process.env[`JENKINS_JOB_URL_${repo.toUpperCase()}`] || '' + return jobUrl ? `${jobUrl}/build` : '' +} + +// Authentication token configured per Jenkins job needed when triggering a build, +// this is set per job in Configure -> Build Triggers -> Trigger builds remotely +function buildTokenForRepo (repo) { + // e.g. JENKINS_BUILD_TOKEN_CITGM + return process.env[`JENKINS_BUILD_TOKEN_${repo.toUpperCase()}`] || '' +} + +function triggerBuild (options, cb) { + const { repo } = options + const base64Credentials = new Buffer(jenkinsApiCredentials).toString('base64') + const authorization = `Basic ${base64Credentials}` + const buildParameters = [{ + name: 'GIT_REMOTE_REF', + value: `refs/pull/${options.number}/head` + }] + const payload = JSON.stringify({ parameter: buildParameters }) + const uri = buildUrlForRepo(repo) + const buildAuthToken = buildTokenForRepo(repo) + + if (!uri) { + return cb(new TypeError(`Will not trigger Jenkins build because $JENKINS_JOB_URL_${repo.toUpperCase()} is not set`)) + } + + if (!buildAuthToken) { + return cb(new TypeError(`Will not trigger Jenkins build because $JENKINS_BUILD_TOKEN_${repo.toUpperCase()} is not set`)) + } + + options.logger.debug('Triggering Jenkins build') + + request.post({ + uri, + headers: { authorization }, + qs: { token: buildAuthToken }, + form: { json: payload } + }, (err, response) => { + if (err) { + return cb(err) + } else if (response.statusCode !== 201) { + return cb(new Error(`Expected 201 from Jenkins, got ${response.statusCode}`)) + } + + cb(null, response.headers.location) + }) +} + +function createPrComment ({ owner, repo, number, logger }, body) { + githubClient.issues.createComment({ + owner, + repo, + number, + body + }, (err) => { + if (err) { + logger.error(err, 'Error while creating comment to reply on CI run comment') + } + }) +} + +module.exports = (app) => { + app.on('issue_comment.created', function handleCommentCreated (event, owner, repo) { + const { number, logger, comment } = event + const commentAuthor = comment.user.login + const options = { + owner, + repo, + number, + logger + } + + function replyToCollabWithBuildStarted (err, buildUrl) { + if (err) { + logger.error(err, 'Error while triggering Jenkins build') + return createPrComment(options, `@${commentAuthor} sadly an error occured when I tried to trigger a build :(`) + } + + createPrComment(options, `@${commentAuthor} build started: ${buildUrl}`) + logger.info({ buildUrl }, 'Jenkins build started') + } + + function triggerBuildWhenCollaborator (err) { + if (err) { + return logger.debug(`Ignoring comment to me by @${commentAuthor} because they are not a repo collaborator`) + } + + triggerBuild(options, replyToCollabWithBuildStarted) + } + + ifBotWasMentionedInCiComment(comment.body, (err, wasMentioned) => { + if (err) { + return logger.error(err, 'Error while checking if the bot username was mentioned in a comment') + } + + if (!wasMentioned) return + + githubClient.repos.checkCollaborator({ owner, repo, username: commentAuthor }, triggerBuildWhenCollaborator) + }) + }) +}