From 59d30c3e675d57dd433919e8cc5196ac98db374a Mon Sep 17 00:00:00 2001 From: Phillip Johnsen Date: Thu, 9 Mar 2017 22:35:22 +0100 Subject: [PATCH] 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 https://github.com/nodejs/github-bot/issues/82 --- README.md | 11 +++ lib/bot-username.js | 14 ++++ lib/github-events.js | 8 ++ scripts/trigger-jenkins-build.js | 127 +++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 lib/bot-username.js create mode 100644 scripts/trigger-jenkins-build.js 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) + }) + }) +}