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

Implement 'builder envs' action. #44

Merged
merged 1 commit into from
Dec 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This includes the clean up of removing install (the underlying Task was remove previously).


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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BUGFIX!

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
});
});
});

});