Skip to content
This repository has been archived by the owner on Mar 4, 2022. It is now read-only.

Commit

Permalink
Implement 'builder envs' action.
Browse files Browse the repository at this point in the history
Fixes #29

* Implement 'builder envs'
* Add flag '--envs-path'
* Update documentation.
* Add tests for args for envs action.
  • Loading branch information
ryan-roemer committed Dec 8, 2015
1 parent ac9c126 commit 1aef9d2
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 10 deletions.
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ Run multiple tasks from `script` concurrently. Roughly analogous to
`npm run <task1> | npm run <task2> | npm run <task3>`, but kills all processes on
first non-zero exit (which makes it suitable for test tasks).


```sh
$ builder concurrent <task1> <task2> <task3>
```
Expand All @@ -170,6 +169,38 @@ group, not the group itself. So, if `builder concurrent --tries=3 foo bar baz`
is run and bar fails twice, then only `bar` would be retried. `foo` and `baz`
would only execute _once_ if successful.

##### builder envs

Run a single task from `script` concurrently for item in an array of different
environment variables. Roughly analogous to:

```sh
$ FOO=VAL1 npm run <task> | FOO=VAL2 npm run <task> | FOO=VAL3 npm run <task>
```

... but kills all processes on first non-zero exit (which makes it suitable for
test tasks). Usage:

```sh
$ builder envs <task> <json-array>
$ builder envs <task> --envs-path=<path-to-json-file>
```

Examples:

```sh
$ builder envs <task> '[{ "FOO": "VAL1" }, { "FOO": "VAL2" }, { "ENV1": "VAL3" }]'
$ builder envs <task> '[{ "FOO": "VAL1", "BAR": "VAL2" }, { "FOO": "VAL3" }]'
```

Flags:

* `--builderrc`: Path to builder config file (default: `.builderrc`)
* `--tries`: Number of times to attempt a task (default: `1`)
* `--queue`: Number of concurrent processes to run (default: unlimited - `0|null`)
* `--[no-]buffer`: Buffer output until process end (default: `false`)
* `--envs-path`: Path to JSON env variable array file (default: `null`)

## Tasks

The underlying concept here is that `builder` `script` commands simply _are_
Expand Down Expand Up @@ -385,7 +416,7 @@ the archetype into your project and remove all Builder dependencies:
* Review all of the combined `scripts` tasks and:
* resolve duplicate tasks names
* revise configuration file paths for the moved files
* replace instances of `builder run <TASK>` with `npm run <TASK>`
* replace instances of `builder run <task>` with `npm run <task>`
* for `builder concurrent <task1> <task2>` tasks, first install the
`concurrently` package and then rewrite to:
`concurrent 'npm run <task1>' 'npm run <task2>'`
Expand Down
20 changes: 16 additions & 4 deletions lib/args.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ var FLAG_QUEUE = {
types: [Number],
default: function (val) { return val > 0 ? val : null; }
};
var FLAG_BUFFER = {
desc: "Buffer output until process end (default: `false`)",
types: [Boolean],
default: false
};

// Option flags.
var FLAGS = {
Expand All @@ -38,10 +43,17 @@ var FLAGS = {
concurrent: {
tries: FLAG_TRIES,
queue: FLAG_QUEUE,
buffer: {
desc: "Buffer output until process end (default: `false`)",
types: [Boolean],
default: false
buffer: FLAG_BUFFER
},

envs: {
tries: FLAG_TRIES,
queue: FLAG_QUEUE,
buffer: FLAG_BUFFER,
"envs-path": {
desc: "Path to JSON env variable array file (default: `null`)",
types: [path],
default: null
}
}
};
Expand Down
38 changes: 37 additions & 1 deletion lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ var run = function (cmd, shOpts, opts, callback) {
var retry = function (cmd, shOpts, opts, callback) {
// Expand options.
var tracker = opts.tracker;
var taskEnv = opts.taskEnv;

// State.
var tries = opts.tries;
Expand All @@ -76,7 +77,8 @@ var retry = function (cmd, shOpts, opts, callback) {

// Check tries.
if (error && tries > 0) {
log.warn("proc:retry", chalk.red(tries) + " tries left, Command: " + cmd);
log.warn("proc:retry", chalk.red(tries) + " tries left, Command: " + chalk.gray(cmd) +
(taskEnv ? ", Environment: " + chalk.magenta(JSON.stringify(taskEnv)) : ""));
}

// Execute without error.
Expand Down Expand Up @@ -183,5 +185,39 @@ module.exports = {
tracker.kill();
callback(err);
});
},

/**
* Run a single task with multiple environment variables in parallel.
*
* @param {String} cmd Shell command
* @param {Object} shOpts Shell options
* @param {Object} opts Runner options
* @param {Function} callback Callback `(err)`
* @returns {void}
*/
envs: function (cmd, shOpts, opts, callback) {
var tracker = new Tracker();
var queue = opts.queue;
var taskEnvs = opts._envs;

// Get mapper (queue vs. non-queue)
var map = queue ?
async.mapLimit.bind(async, taskEnvs, queue) :
async.map.bind(async, taskEnvs);

log.info("envs", "Starting with queue size: " + chalk.magenta(queue || "unlimited"));
map(function (taskEnv, cb) {
// Add each specific set of environment variables.
var taskShOpts = _.merge({}, shOpts, { env: taskEnv });
var taskOpts = _.extend({ tracker: tracker, taskEnv: taskEnv }, opts);

log.info("envs", "Starting environment " + chalk.magenta(JSON.stringify(taskEnv)) +
" run for command: " + chalk.gray(cmd));
retry(cmd, taskShOpts, taskOpts, cb);
}, function (err) {
tracker.kill();
callback(err);
});
}
};
65 changes: 63 additions & 2 deletions lib/task.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

var fs = require("fs");
var path = require("path");
var _ = require("lodash");
var chalk = require("chalk");
Expand Down Expand Up @@ -43,7 +44,7 @@ var Task = module.exports = function (opts) {
}
};

Task.prototype.ACTIONS = ["help", "run", "concurrent", "install"];
Task.prototype.ACTIONS = ["help", "run", "concurrent", "envs"];

Task.prototype.toString = function () {
var cmd = this._command;
Expand Down Expand Up @@ -142,7 +143,7 @@ Task.prototype.run = function (callback) {

var env = this._env.env; // Raw environment object.
var task = this.getCommand(this._command);
var flags = args.concurrent(this.argv);
var flags = args.run(this.argv);
var opts = _.extend({}, flags);

log.info(this._action, this._command + chalk.gray(" - " + task));
Expand Down Expand Up @@ -170,6 +171,66 @@ Task.prototype.concurrent = function (callback) {
runner.concurrent(tasks, { env: env }, opts, callback);
};

Task.prototype._parseJson = function (objStr) {
try {
return JSON.parse(objStr);
} catch (err) {
log.error(this._action + ":json-obj", "Failed to load JSON object: " + objStr);
throw err;
}
};

Task.prototype._parseJsonFile = function (filePath) {
try {
return JSON.parse(fs.readFileSync(filePath));
} catch (err) {
log.error(this._action + ":json-file", "Failed to load JSON file: " + filePath);
throw err;
}
};

/**
* Run multiple environments.
*
* @param {Function} callback Callback function `(err)`
* @returns {void}
*/
Task.prototype.envs = function (callback) {
/*eslint max-statements: [2, 20]*/
// Core setup.
var env = this._env.env;
var task = this.getCommand(this._command);
var flags = args.envs(this.argv);
var opts = _.extend({}, flags);

// Get task environment array.
var envsStr = this._commands[1];
if (envsStr) {
// Try string on command line first:
// $ builder envs <task> '[{ "FOO": "VAL1" }, { "FOO": "VAL2" }]'
opts._envs = this._parseJson(envsStr);
} else if (opts.envsPath) {
// Try JSON file path next:
// $ builder envs <task> --envs-path=my-environment-vars.json
opts._envs = this._parseJsonFile(opts.envsPath);
}

// Validation
var err;
if (_.isEmpty(opts._envs)) {
err = new Error("Empty/null JSON environments array.");
} else if (!_.isArray(opts._envs)) {
err = new Error("Non-array JSON environments object: " + JSON.stringify(opts._envs));
}

if (err) {
log.error("envs:json-error", err);
return callback(err);
}

runner.envs(task, { env: env }, opts, callback);
};

/**
* Execute task or tasks.
*
Expand Down
39 changes: 38 additions & 1 deletion test/server/spec/lib/args.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe("lib/args", function () {
});
});

it("handles valid --tries, --queue --buffer", function () {
it("handles valid --tries, --queue, --buffer", function () {
argv = argv.concat(["--tries=2", "--queue=2", "--buffer"]);
expect(_flags(args.concurrent(argv))).to.deep.equal({
queue: 2,
Expand Down Expand Up @@ -185,4 +185,41 @@ describe("lib/args", function () {
});
});
});

describe("envs", function () {
// envs handles all `concurrent` flags, so just testing some additional
// permutations

it("handles defaults for envs flags", function () {
expect(_flags(args.envs(argv))).to.deep.equal({
queue: null,
envsPath: null,
buffer: false,
tries: 1
});
});

it("handles valid --tries, --queue, --buffer, --envs-path", function () {
// Set to a nonexistent path (note args _doesn't_ check valid path).
var dummyPath = path.join(__dirname, "DUMMY_ENVS.json");
argv = argv.concat(["--tries=2", "--queue=2", "--buffer", "--envs-path=" + dummyPath]);
expect(_flags(args.envs(argv))).to.deep.equal({
queue: 2,
envsPath: dummyPath,
buffer: true,
tries: 2
});
});

it("handles multiple flags", function () {
var flags = ["--queue=-1", "--tries=BAD", "--no-buffer"];
expect(_flags(args.envs(argv.concat(flags)))).to.deep.equal({
queue: null,
envsPath: null,
buffer: false,
tries: 1
});
});
});

});

0 comments on commit 1aef9d2

Please sign in to comment.