Skip to content

Commit

Permalink
Feature: Scripts action
Browse files Browse the repository at this point in the history
Includes performance improvements to the backend request queue and fixes to configuration consumption.
  • Loading branch information
KurtWagner committed Jan 18, 2018
1 parent 219d354 commit 5d1411a
Show file tree
Hide file tree
Showing 17 changed files with 514 additions and 207 deletions.
32 changes: 23 additions & 9 deletions CHANGES.md
@@ -1,20 +1,34 @@
### 0.0.1

First release as migration of [Checkee](https://github.com/kurtwagner/checkee)
tool for commenting on pull requests from linter results.

### 0.0.2
### 0.0.5

**Fixes**
**Improvement**
- Support parallel requests in backend package, this should result in faster turnarounds when the requests are known up front. Requests that page through are yet to be optimised to look ahead.

- Fixes #4 - Can't read attribute "username" on `null` when comparing existing `deleted` comment.
**Fix**
- Non-relative configuration and credentials paths were not being resolved correctly.
- `.bitbucket-toolbox.js` configuration was not being picked up.

**Feature**
- New `scripts` action where you can perform and respond to certain events. For now this only supports the "openPullRequests" type. This will load all open pull requests (including changed chunks).

### 0.0.4

**Fix**
- Fix - `chalk` should be a production dependency.

### 0.0.3

**Enhancement**
- Resolves #1 - Introduces `--fail-on-severity` option to the `comments` action.

### 0.0.4
### 0.0.2

**Fix**
- Fix - `chalk` should be a production dependency.
**Fixes**

- Fixes #4 - Can't read attribute "username" on `null` when comparing existing `deleted` comment.

### 0.0.1

First release as migration of [Checkee](https://github.com/kurtwagner/checkee)
tool for commenting on pull requests from linter results.
18 changes: 9 additions & 9 deletions bin/bitbucket-toolbox.js
Expand Up @@ -2,20 +2,20 @@
'use strict';

const logger = require('../lib/logger');
const {getConfig} = require('../lib/config');
const {getArgs} = require('../lib/args');
const {getCredentialsFromArgs} = require('../lib/credentials');
const {dispatchAction} = require('../lib/actions');
const { getConfig } = require('../lib/config');
const { getArgs } = require('../lib/args');
const { getCredentialsFromArgs } = require('../lib/credentials');
const { dispatchAction } = require('../lib/actions');

const {name, version} = require('../package.json');
const { name, version } = require('../package.json');
logger.title(`${name} ${version}`);

try {
const config = getConfig();
const {action, args} = getArgs(process.argv);
const { action, args } = getArgs(process.argv);
const credentials = getCredentialsFromArgs(args);
dispatchAction(action, {config, args, credentials})

dispatchAction(action, { config, args, credentials, argv: process.argv })
.then(() => {
logger.success(`Running "${action}"`);
logger.log('Done.');
Expand All @@ -25,7 +25,7 @@ try {
handleError(e);
}

function handleError({message}) {
function handleError({ message }) {
logger.error(message);
process.exit(1);
}
3 changes: 2 additions & 1 deletion lib/actions/constants.js
Expand Up @@ -2,4 +2,5 @@

module.exports = {
ACTION_COMMENTS: 'comments',
};
ACTION_SCRIPT: 'run',
};
18 changes: 13 additions & 5 deletions lib/actions/dispatcher.js
@@ -1,10 +1,15 @@
'use strict';

const {ACTION_COMMENTS} = require('./constants');
const {executeCommentsAction} = require('./comments');
const { ACTION_COMMENTS, ACTION_SCRIPT } = require('./constants');

const { executeCommentsAction } = require('./comments');
const { executeScriptAction } = require('./script');

const { highlightText } = require('../colors');

const actions = {
[ACTION_COMMENTS]: executeCommentsAction,
[ACTION_SCRIPT]: executeScriptAction,
};

module.exports = {
Expand All @@ -17,7 +22,10 @@ function dispatchAction(action, options) {
}

function getActionDispatcher(action) {
return actions[action] || function defaultAction() {
throw new Error(`Unknown action ${action}`);
};
return (
actions[action] ||
function defaultAction() {
throw new Error(`Unknown action ${highlightText(action)}`);
}
);
}
84 changes: 84 additions & 0 deletions lib/actions/script.js
@@ -0,0 +1,84 @@
'use strict';

const Q = require('q');

const { highlightText } = require('../colors');
const logger = require('../logger');
const { BitbucketClient } = require('../bitbucket');
const { ACTION_SCRIPT } = require('./constants');

const SCRIPT_TYPES = {
openPullRequests: openPullRequests,
};

module.exports = {
executeScriptAction,
};

function executeScriptAction(actionDetails) {
const { argv, config } = actionDetails;
const { scriptRunner, scriptConfig } = getAndVerifyScript({ argv, config });

return scriptRunner(actionDetails).then(result => {
return Q(scriptConfig.resolve(result));
});
}

function getAndVerifyScript({ config, argv }) {
if (!config.scripts) {
throw new Error(`Missing "scripts" in configuration file.`);
}

const scriptName = argv[argv.findIndex(arg => arg === ACTION_SCRIPT) + 1];
if (!scriptName) {
throw new Error(`Missing script name. e.g, "bitbucket-toolbox ${ACTION_SCRIPT} <scriptName>"`);
}

const scriptConfig = config.scripts[scriptName];
if (!scriptConfig) {
throw new Error(`No script configuration for ${highlightText(scriptName)}`);
}
if (!scriptConfig.type) {
throw new Error(`Script ${highlightText(scriptName)} is missing a "type". ${getAvailableScriptTypesText()}`);
}
if (!scriptConfig.resolve) {
throw new Error(`Script ${highlightText(scriptName)} is missing a "resolve" function.`);
}

const scriptRunner = SCRIPT_TYPES[scriptConfig.type];
if (!scriptRunner) {
throw new Error(
`Script ${highlightText(scriptName)} has an unknown type ${highlightText(
scriptConfig.type
)}. ${getAvailableScriptTypesText()}`
);
}

return { scriptConfig, scriptRunner };
}

function openPullRequests({ args, config, credentials }) {
const { repoUser = config.bitbucket.repoUser, repoSlug = config.bitbucket.repoSlug } = args;

if (!repoSlug || !repoUser) {
throw new Error('Missing required repo slug and repo user');
}

const client = new BitbucketClient({
username: credentials.bitbucket.username,
password: credentials.bitbucket.password,
});

logger.step(1, 2, `Loading open pull requests`);
return client
.repository(repoUser, repoSlug)
.pullRequests()
.then(pullRequests => {
logger.step(2, 2, `Resolving script`);
return { pullRequests };
});
}

function getAvailableScriptTypesText() {
return `Valid types: ${Object.keys(SCRIPT_TYPES).join(', ')}`;
}
78 changes: 44 additions & 34 deletions lib/bitbucket/backend.js
Expand Up @@ -7,25 +7,29 @@ const HTTP_TOO_MANY_REQUESTS = 429;
const HTTP_FORBIDDEN = 403;
const HTTP_UNAUTHORIZED = 401;

const DEFAULT_TRY_AGAIN_MS = 1000;

class BitbucketBackend {
constructor({ username, password }) {
// set maxParallelRequests to 1 to one do one at a time
constructor({ username, password, maxParallelRequests = 10 }) {
this._username = username;
this._password = password;
this._maxParallelRequests = maxParallelRequests;

/**
* Due to rate limits we're going to queue requests and control how
* they're fired out. This is slower but reduces our chances of hitting
* the pesky limits
*/
this._requestQueue = [];
this._queueRunning = false;
this._tryAgainInMilliseconds = 2000;
this._inProcessing = 0;
this._tryAgainInMilliseconds = DEFAULT_TRY_AGAIN_MS;
}

request(method, url, formData) {
const backend = this;
const deferred = Q.defer();

const executeRequest = () => {
const requestConfig = backend._requestConfig({ url, formData });
request[method](requestConfig, handleResponse);
Expand All @@ -34,41 +38,47 @@ class BitbucketBackend {
backend._requestQueue.push(executeRequest);

// if the queue isn't running, kick it off
if (!backend._queueRunning) backend._nextIntQueue();
if (backend._inProcessing < backend._maxParallelRequests) backend._nextIntQueue();

return deferred.promise;

////////////////////////////////////////////////////

function handleResponse(error, response, body) {
switch (response.statusCode) {
case HTTP_TOO_MANY_REQUESTS: {
retryRequest();
break;
}
case HTTP_FORBIDDEN: {
console.error(body);
throw new Error('Forbidden request made.');
}
case HTTP_UNAUTHORIZED: {
const message = [
'1. You have configured your username and password',
'2. Your username is in fact your username and not email',
'3. Your username and password are correct',
];
throw new Error(`We could not authorize you. Please ensure:\n\n${message.join('\n')}`);
}
default: {
if (error) {
deferred.reject(error);
} else {
deferred.resolve({ statusCode: response.statusCode, body });
try {
switch (response.statusCode) {
case HTTP_TOO_MANY_REQUESTS: {
retryRequest();
break;
}
case HTTP_FORBIDDEN: {
console.error(body);
throw new Error('Forbidden request made.');
}
case HTTP_UNAUTHORIZED: {
const message = [
'1. You have configured your username and password',
'2. Your username is in fact your username and not email',
'3. Your username and password are correct',
];
throw new Error(`We could not authorize you. Please ensure:\n\n${message.join('\n')}`);
}
default: {
if (error) {
deferred.reject(error);
} else {
deferred.resolve({ statusCode: response.statusCode, body });
}
backend._nextIntQueue();
}
backend._nextIntQueue();
}
} catch (e) {
throw e;
} finally {
--backend._inProcessing;
}
}

function retryRequest() {
setTimeout(() => {
backend._requestQueue.unshift(executeRequest);
Expand All @@ -80,11 +90,11 @@ class BitbucketBackend {

_nextIntQueue() {
if (this._requestQueue.length > 0) {
this._queueRunning = true;
++this._inProcessing;
const queueMethod = this._requestQueue.shift();
queueMethod();
} else {
this._queueRunning = false;
this._tryAgainInMilliseconds = DEFAULT_TRY_AGAIN_MS; // reset when none left
}
}

Expand Down

0 comments on commit 5d1411a

Please sign in to comment.