Skip to content

Instantly share code, notes, and snippets.

@ngeor ngeor/index.js
Created Dec 29, 2018

Embed
What would you like to do?
bitbucket-hipchat-pr-notifier
/* eslint-disable max-lines */
const request = require('request');
const Promise = require('promise');
const AWS = require('aws-sdk');
const encryptedPassword = process.env.BITBUCKET_PASSWORD;
let decryptedPassword;
const minSuccess = 200;
const maxSuccess = 300;
function baseAuthentication(username, password) {
const base64encodedData = new Buffer(username + ':' + password).toString('base64');
return base64encodedData;
}
/**
* Notifies HipChat.
* @param {string} msg - The message to send.
* @returns {Promise<string>} The message.
*/
function notifyHipChat(msg) {
const promise = new Promise(function(resolve, reject) {
if (!msg) {
resolve(msg);
return;
}
request({
url: process.env.HIPCHAT_NOTIFICATION_URL,
method: 'POST',
json: {
color: 'yellow',
message: msg,
notify: false,
message_format: 'text' // eslint-disable-line
}
}, function(error, response, body) { // eslint-disable-line
if (error) {
reject(error);
} else if (response.statusCode < minSuccess || response.statusCode >= maxSuccess) {
reject(new Error('HTTP code: ' + response.statusCode));
} else {
resolve(msg);
}
});
});
return promise;
}
/**
* Creates a request to bitbucket.
* @param {string} url - The URL of the request.
* @returns {Promise} The parsed JSON response.
*/
function bitbucketPromise(url) {
const promise = new Promise(function(resolve, reject) {
request({
url,
headers: {
Accept: 'application/json',
Authorization: 'Basic ' +
baseAuthentication(process.env.BITBUCKET_USER, decryptedPassword)
}
}, function(error, response, body) {
if (error) {
reject(error);
} else if (response.statusCode < minSuccess || response.statusCode >= maxSuccess) {
reject(new Error('HTTP code: ' + response.statusCode));
} else {
resolve(JSON.parse(body));
}
});
});
return promise;
}
/**
* Gets the approver.
* @param {object} participant - The participant.
* @returns {string} The approver name.
*/
function getApprover(participant) {
if (participant.role === 'REVIEWER') {
const reviewerName = participant.user.display_name;
const approved = participant.approved;
if (approved) {
return reviewerName;
}
}
return null;
}
/**
* Processes a pull request.
* @param {object} body - The PR object.
* @returns {object} Information about the PR.
*/
function processPR(body) {
const participants = body.participants;
const approvedBy = participants
.map(getApprover)
.filter(p => !!p);
const minApprovers = 2;
if (approvedBy.length >= minApprovers) {
return null;
}
return {
title: body.title,
approvedBy,
htmlLink: body.links.html.href
};
}
/**
* Gets the PR details.
* @param {string} prLink - The URL to the PR.
* @returns {object} Information about the PR.
*/
function getPRDetails(prLink) {
return bitbucketPromise(prLink).then(processPR);
}
/**
* Processes builds information.
* @param {object} builds - The builds object.
* @returns {Promise} A promise with all the PR links.
*/
function processBuilds(builds) {
const values = builds.values;
const prLinks = [];
for (let i = 0; i < values.length; i++) {
const pr = values[i];
if (pr.state === 'OPEN') {
prLinks.push(getPRDetails(pr.links.self.href));
}
}
return Promise.all(prLinks);
}
/**
* Formats the approvals message.
* @param {number} n - The number of approvals.
* @returns {string} The approvals message.
*/
function approvalMsg(n) {
return '' + (n || 'no') + ' ' + (n === 1 ? 'approval' : 'approvals');
}
/**
* Creates the message that notifies about one open PR.
* @param {object} pr - The PR.
* @returns {string} The message.
*/
function notifyOpenPR(pr) {
if (!pr) {
return null;
}
let msg = '"' + pr.title + '" ( ' + pr.htmlLink + ' ) has ' + approvalMsg(pr.approvedBy.length);
if (pr.approvedBy.length > 0) {
msg = msg + ' by ' + pr.approvedBy.join();
}
return msg;
}
/**
* Creates the message that notifies about open PRs.
* @param {array} prs - The pull requests.
* @returns {string} The notification message.
*/
function notifyOpenPRs(prs) {
const msg = prs
.map(notifyOpenPR)
.filter(m => !!m)
.join(', ');
if (msg) {
return 'PRs needing reviewers: ' + msg + '. Please review! (awthanks)';
}
return msg;
}
/**
* Processes one repository.
* @param {string} repoUrl - the repository URL.
* @returns {Promise<string>} The message returned by HipChat.
*/
function processOneRepo(repoUrl) {
return bitbucketPromise(repoUrl)
.then(processBuilds)
.then(notifyOpenPRs)
.then(notifyHipChat)
.then(function(msg) {
return msg;
});
}
/**
* Processes all repositories.
* @returns {Promise} All repositories.
*/
function processAllRepos() {
const repoSlugs = process.env.BITBUCKET_SLUGS.split(',');
const repoOwner = process.env.BITBUCKET_OWNER;
const repoUrls = repoSlugs.map(
slug => 'https://api.bitbucket.org/2.0/repositories/' + repoOwner +
'/' + slug + '/pullrequests');
const promises = repoUrls.map(processOneRepo);
return Promise.all(promises);
}
/**
* Decrypts a password using AWS KMS.
* @returns {Promise<string>} The password.
*/
function decryptPassword() {
const promise = new Promise(function(resolve, reject) {
if (decryptedPassword) {
resolve(decryptedPassword);
} else {
/*
* Decrypt code should run once and variables stored outside of the function
* handler so that these are decrypted once per container
*/
const kms = new AWS.KMS();
kms.decrypt({
CiphertextBlob: new Buffer(encryptedPassword, 'base64')
}, (err, data) => {
if (err) {
reject(err);
} else {
decryptedPassword = data.Plaintext.toString('ascii');
resolve(decryptedPassword);
}
});
}
});
return promise;
}
exports.handler = (event, context, callback) => {
decryptPassword()
.then(processAllRepos)
.then(msg => callback(null, msg))
.catch(e => callback(e));
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.