#!/usr/bin/env node
/**
* The appveyor-status command.
*
* @copyright Copyright 2017-2019 Kevin Locke <kevin@kevinlocke.name>
* @license MIT
* @module appveyor-status/bin/appveyor-status
*/
'use strict';
const ansiStyles = require('ansi-styles');
const Yargs = require('yargs/yargs');
const fs = require('fs');
const readAllStream = require('read-all-stream');
const { supportsColor } = require('supports-color');
const appveyorStatus = require('..');
const packageJson = require('../package.json');
/** Exit codes returned by {@link module:appveyor-status/bin/appveyor-status}
* (as a bi-directional map).
*
* @constant
* @static
* @enum {number}
*/
const ExitCode = {
/** Success. */
SUCCESS: 0,
/** Failed for an unspecified reason. */
FAIL_OTHER: 1,
/** Failed due to build status. */
FAIL_STATUS: 2,
/** Failed due to commit mismatch. */
FAIL_COMMIT: 3,
/** Failed due to invalid arguments. */
FAIL_ARGUMENTS: 4,
};
// Add mapping from code to name
Object.keys(ExitCode).forEach((codeName) => {
const code = ExitCode[codeName];
ExitCode[code] = codeName;
});
/** Maps AppVeyor build status to an ansi-styles color name.
*
* @constant
* @type {object<string, string>}
* @private
*/
const statusColor = {
failed: 'red',
success: 'green',
};
function coerceWait(arg) {
const val = arg === true ? Infinity : Number(arg);
if (Number.isNaN(val)) {
throw new TypeError(`Invalid number "${arg}"`);
}
return val;
}
/** Gets the AppVeyor build status, handles errors, and writes the result to
* output or error streams.
*
* @private
*/
function checkStatus(options, callback) {
appveyorStatus.getStatus(options, (err, status) => {
if (err) {
if (err.name === 'CommitMismatchError') {
let expected = options.commit;
if (options.commit !== err.expected) {
expected += ` (${err.expected})`;
}
options.err.write(`Error: Last build commit ${err.actual} `
+ `did not match ${expected}\n`);
// eslint-disable-next-line unicorn/no-null
callback(null, ExitCode.FAIL_COMMIT);
} else {
options.err.write(`${err}\n`);
// eslint-disable-next-line unicorn/no-null
callback(null, ExitCode.FAIL_OTHER);
}
return;
}
if (options.verbosity >= 0) {
let statusColored;
if (options.color) {
const colorName = statusColor[status] || 'gray';
const ansiStyle = ansiStyles[colorName];
statusColored = `${ansiStyle.open}status${ansiStyle.close}`;
} else {
statusColored = status;
}
options.out.write(`AppVeyor build status: ${statusColored}\n`);
}
callback(
null, // eslint-disable-line unicorn/no-null
status === 'success' ? ExitCode.SUCCESS : ExitCode.FAIL_STATUS,
);
});
}
/** Options for command entry points.
*
* @static
* @typedef {{
* in: (module:stream.Readable|undefined),
* out: (module:stream.Writable|undefined),
* err: (module:stream.Writable|undefined)
* }} CommandOptions
* @property {module:stream.Readable=} in Stream from which input is read.
* (default: <code>process.stdin</code>)
* @property {module:stream.Writable=} out Stream to which output is written.
* (default: <code>process.stdout</code>)
* @property {module:stream.Writable=} err Stream to which errors (and
* non-output status messages) are written.
* (default: <code>process.stderr</code>)
*/
// var CommandOptions;
/**
* Entry point for this command.
*
* @param {!Array<string>} args Command-line arguments.
* @param {module:appveyor-status/bin/appveyor-status.CommandOptions=} options
* Options.
* @param {?function(Error, number=)=} callback Callback for the exit code
* or an <code>Error</code>. Required if <code>global.Promise</code> is
* not defined.
* @returns {Promise<module:appveyor-status/bin/appveyor-status.ExitCode>
* |undefined} If <code>callback</code> is not given and
* <code>global.Promise</code> is defined, a <code>Promise</code> with the
* exit code or <code>Error</code>.
*/
module.exports = function appveyorStatusCmd(args, options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = undefined;
}
if (!callback && typeof Promise === 'function') {
return new Promise((resolve, reject) => {
appveyorStatusCmd(args, options, (err, result) => {
if (err) { reject(err); } else { resolve(result); }
});
});
}
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function');
}
try {
if (args === undefined || args === null) {
args = [];
} else if (typeof args !== 'object'
|| Math.floor(args.length) !== args.length) {
throw new TypeError('args must be Array-like');
} else if (args.length < 2) {
throw new RangeError('args must have at least 2 elements');
} else {
args = Array.prototype.slice.call(args, 2).map(String);
}
if (options !== undefined && typeof options !== 'object') {
throw new TypeError('options must be an object');
}
options = {
in: process.stdin,
out: process.stdout,
err: process.stderr,
...options,
};
if (!options.in || typeof options.in.on !== 'function') {
throw new TypeError('options.in must be a stream.Readable');
}
if (!options.out || typeof options.out.write !== 'function') {
throw new TypeError('options.out must be a stream.Writable');
}
if (!options.err || typeof options.err.write !== 'function') {
throw new TypeError('options.err must be a stream.Writable');
}
} catch (err) {
process.nextTick(() => {
callback(err);
});
return undefined;
}
// Workaround for https://github.com/yargs/yargs/issues/783
// Necessary because mocha package.json overrides .parserConfiguration()
require.main = module;
const yargs = new Yargs(undefined, undefined, require)
.parserConfiguration({
'parse-numbers': false,
'duplicate-arguments-array': false,
'flatten-duplicate-arrays': false,
})
.usage('Usage: $0 [options]')
.help()
.alias('help', 'h')
.alias('help', '?')
.option('badge', {
alias: 'B',
describe:
'Status Badge ID of project (from badge URL, exclusive with commit)',
nargs: 1,
})
.option('branch', {
alias: 'b',
description: 'Query latest build for a branch',
defaultDescription: '(current)',
})
.option('color', {
description: 'Colorize the output',
default: undefined,
defaultDescription: '(to TTY)',
type: 'boolean',
})
.option('commit', {
alias: 'c',
description:
'Require build to be for named commit (requires project or token)',
defaultDescription: 'HEAD',
})
.option('project', {
alias: 'p',
describe: 'AppVeyor project to query (as $user/$proj)',
nargs: 1,
})
.option('quiet', {
alias: 'q',
describe: 'Print less output',
count: true,
})
.option('repo', {
alias: 'r',
describe: 'Repository to query (URL or path)',
defaultDescription: '.',
nargs: 1,
})
.option('token', {
alias: 't',
describe: 'API access token',
defaultDescription: '$APPVEYOR_API_TOKEN env var',
nargs: 1,
})
.option('token-file', {
alias: 'T',
describe: 'file containing API access token',
nargs: 1,
})
.conflicts('token-file', 'token')
.option('verbose', {
alias: 'v',
describe: 'Print more output',
count: true,
})
.option('wait', {
alias: 'w',
describe: 'Wait if build is pending (timeout in seconds)',
defaultDescription: 'Infinity',
coerce: coerceWait,
})
.option('webhook', {
alias: 'W',
/* Undocumented. Deprecated in favor of --badge
* 'Webhook ID of project (from badge URL, exclusive with commit)' */
nargs: 1,
})
.version(`${packageJson.name} ${packageJson.version}`)
.alias('version', 'V')
.strict();
yargs.parse(args, (err, argOpts, output) => {
if (err) {
options.err.write(output ? `${output}\n`
: `${err.name}: ${err.message}\n`);
// eslint-disable-next-line unicorn/no-null
callback(null, ExitCode.FAIL_ARGUMENTS);
return;
}
if (output) {
options.out.write(`${output}\n`);
}
if (argOpts.help || argOpts.version) {
// eslint-disable-next-line unicorn/no-null
callback(null, ExitCode.SUCCESS);
return;
}
if (argOpts._.length !== 0) {
options.err.write('Error: Unexpected non-option arguments.\n');
// eslint-disable-next-line unicorn/no-null
callback(null, ExitCode.FAIL_ARGUMENTS);
return;
}
argOpts.verbosity = (argOpts.verbose || 0) - (argOpts.quiet || 0);
delete argOpts.quiet;
delete argOpts.verbose;
if (argOpts.color === undefined) {
argOpts.color = supportsColor(options.out).hasBasic;
}
if (argOpts.commit === true) {
argOpts.commit = 'HEAD';
}
if (argOpts.wait) {
argOpts.wait *= 1000;
}
argOpts.statusBadgeId = argOpts.badge;
delete argOpts.badge;
argOpts.webhookId = argOpts.webhook;
delete argOpts.webhook;
const statusOpts = { ...options, ...argOpts };
if (argOpts.tokenFile !== undefined) {
const tokenFileStream = argOpts.tokenFile === '-' ? options.in
: fs.createReadStream(argOpts.tokenFile);
readAllStream(tokenFileStream, (errRead, token) => {
if (errRead) {
options.err.write('Error: Unable to read API token file: '
+ `${errRead.message}\n`);
// eslint-disable-next-line unicorn/no-null
callback(null, ExitCode.FAIL_ARGUMENTS);
return;
}
statusOpts.token = token.trim();
checkStatus(statusOpts, callback);
});
} else {
statusOpts.token = statusOpts.token !== undefined ? statusOpts.token
: process.env.APPVEYOR_API_TOKEN;
checkStatus(statusOpts, callback);
}
});
return undefined;
};
module.exports.default = module.exports;
module.exports.ExitCode = ExitCode;
if (require.main === module) {
// This file was invoked directly.
/* eslint-disable no-process-exit */
const mainOptions = {
in: process.stdin,
out: process.stdout,
err: process.stderr,
};
module.exports(process.argv, mainOptions, (err, exitCode) => {
if (err) {
process.stderr.write(`${err.stack}\n`);
exitCode = ExitCode.FAIL_OTHER;
}
process.exit(exitCode);
});
}