fh-fhc
Version:
A Command Line Interface for FeedHenry
376 lines (351 loc) • 11.7 kB
JavaScript
/* globals i18n */
var fhreq = require("../../utils/request");
var common = require("../../common.js");
var log = require("../../utils/log");
var fhc = require("../../fhc");
var async = require('async');
var http = require("http");
var https = require("https");
var url = require("url");
var fs = require("fs");
var _ = require("underscore");
var qrcode = require("qrcode-terminal");
const DEPRECATION_NOTICE = "The fhc build command is DEPRECATED. For more details, please see https://redhatmobilestatus.com/2018/09/26/immediate-deprecation-of-the-rhmap-client-binary-build-service-build-farm/. ";
module.exports = {
'desc' : i18n._('[DEPRECATED] Builds a client application'),
'deprecation': i18n._(DEPRECATION_NOTICE),
'examples' :
[{
cmd : '[DEPRECATED] fhc build --project=<project> --app=<app-id> --cloud_app=<cloud-app-id> --tag=<tag> --environment=<environment> --destination=<destination> --version=<version> --config=<release|distribution> --privateKeyPass=<private-key-password> --certpass=<certificate-password> --download=<true|false> --bundleId=<credential bundle id> --cordova_version=<cordova version> --git-tag=<tag name> --git-branch=<branch name> --git-commit=<commit hash>',
desc : "[DEPRECATED] Build a client <app> of the <project> connect to the <cloud_app> deployed into the <environment>"
}],
'demand' : ['project','app','cloud_app','destination','environment'],
'alias' : {
'project': 'p',
'app': 'a',
'cloud_app': 'ca',
'tag': 't',
'environment': 'e',
'destination': 'd',
'version': 'v',
'config': 'c',
'privateKeyPass': 'pk',
'certpass': 'cp',
'download': 'dw',
'bundleId': 'b',
'cordova_version': 'cv',
'git-tag': 'gt',
'git-branch': 'gb',
'git-commit': 'gc',
'json' : 'j',
0: 'project',
1: 'app',
2: 'cloud_app',
3: 'destination',
4: 'environment'
},
'describe' : {
'project' : i18n._("Unique 24 character GUID of the project."),
'app' : i18n._("Unique 24 character GUID of the client app which will be built."),
'cloud_app' : i18n._("Unique 24 character GUID of the cloud app which the client app will connect to."),
'tag' : i18n._("The name of a connection tag for the cloud app, must be in Semantic Version format, e.g. 0.0.1. See: http://semver.org."),
'environment' : i18n._("The id of a target environment, e.g. dev."),
'destination' : i18n._("One of: android, iphone, ipad, ios(for universal binary)"),
'version' : i18n._("Specific to the destination (e.g. Android version 4.0)."),
'config' : i18n._("Either 'debug' (default), 'distribution', or 'release'."),
'privateKeyPass' : i18n._("Private key password of the bundleId, only needed for 'release' builds."),
'certpass' : i18n._("Certificate password."),
'download' : i18n._("Boolean value to inform to download or not the built when it finish."),
'bundleId' : i18n._("The unique bundle identifier of the credential bundle. You can get it using 'fhc credentials list'. Required for all iOS builds."),
'cordova_version' : i18n._("For specifying which version of Cordova to use. Currently supported: either 2.2 or 3.3. Only valid for Android for now."),
'git-tag' : i18n._("Long value of URL that you want shorter for the henr.ie URL."),
'git-branch' : i18n._("The name of the git branch to build, defaults to 'master'."),
'git-commit' : i18n._("The full hash of the commit to build, if not the head of the branch. Must be used with <branch name>."),
'json' : i18n._('Output into json format')
},
'preCmd' : function(params,cb) {
if (!params.config) {
params.config = 'debug';
} else if (params.config === 'release' || params.config === 'distribution') {
if (!params.privateKeyPass) {
return cb(i18n._("Missing 'privateKeyPass' parameter"));
}
if (!params.certpass) {
return cb(i18n._("Missing 'certpass' parameter"));
}
}
if (isIosBuild(params)) {
params.deviceType = params.destination;
if (!params.bundleId) {
return cb(i18n._("Missing 'bundleId' parameter"));
}
}
// set default build version if none specified. Note this is hardcoded as not available from millicore API.
// note these values were gotten from studio/static/js/application/destinations/destination_*
if (!params.version) {
if (params.destination === 'android') {
params.version = '4.0';
} else if (isIosBuild(params)) {
params.version = '6.0';
}
}
params.generateSrc=false;
return cb(null, params);
},
'customCmd' : function(params,cb) {
try {
mutateGitRefArgs(params);
doBuild(params, cb);
} catch (x) {
log.silly(x);
return cb(i18n._("Error processing params: ") + x + "\n");
}
}
};
/**
* Check if the destination is iphone, ipad or ios
* @param params
* @returns {boolean}
*/
function isIosBuild(params) {
return params.destination === "iphone" || params.destination === "ipad" || params.destination === "ios";
}
/**
* Mutates the arguments object in order to support --git-branch --git-commit and --git-tag, turning them into gitRef
* @param {Object} params Arguments object
*/
function mutateGitRefArgs(params) {
// Workaround for current millicore behavior:
// If no git- params are present, build from branch:master
var gitParams = [
'git-commit',
'git-tag',
'git-branch'
];
if (!_.any(gitParams, _.propertyOf(params))) {
params.gitRef = {
type: 'branch',
value: 'master'
};
return;
}
function clearGitParams(params) {
_.each(gitParams, function(p) {
delete params[p];
});
}
// priority is commit > tag > branch
if (params['git-commit']) {
params.gitRef = {
type: 'commit',
value: params['git-commit'],
hash: params['git-commit']
};
clearGitParams(params);
return;
}
if (params['git-tag']) {
params.gitRef = {
type: 'tag',
value: params['git-tag']
};
clearGitParams(params);
return;
}
if (params['git-branch']) {
params.gitRef = {
type: 'branch',
value: params['git-branch']
};
clearGitParams(params);
return;
}
}
/**
* To create the URL to perform the build
* @param params
* @returns {string}
*/
function createUri(params) {
return "box/srv/1.1/wid/" + fhc.curTarget + "/" + params.destination + "/" + params.app + "/deliver";
}
function createMessageWithOtaUrl(message, buildResults) {
message = message + i18n._("\nOTA URL: ") + buildResults.ota_url;
qrcode.generate(buildResults.ota_url, {small: true}, function(code) {
message = message + "\n" + code;
});
return message;
}
/**
* Download the artefact built into the local directory
* @param params
* @param build_asset
* @param cb
* @param buildResults
* @param message
*/
function doDownload(params, build_asset, cb, buildResults, message) {
downloadBuild(params, build_asset, './', function(err, data) {
if (err) {
return cb(err);
}
if (data) {
buildResults.download = data;
message = message + i18n._("\nDownloaded file: ") + data.file;
}
return cb(null, message);
});
}
/**
* Create and print the console message
* @param message
* @param buildResults
* @param params
* @param cb
*/
function doConsoleMessage(message, buildResults, params, cb) {
message = i18n._("\nDownload URL: ") + buildResults.url;
if (buildResults.ota_url) {
message = createMessageWithOtaUrl(message, buildResults);
}
doDownload(params, buildResults.url, cb, buildResults, message);
}
/**
* Get the data returned and perform the success outcome
* @param results
* @param cb
* @param params
*/
function doSuccessOutcome(results, cb, params) {
var message = "";
var artifactUrls = results[0][0].action;
getArtifactUrl(artifactUrls, function(err, buildResults) {
if (err) {
return cb(err);
}
if (params.json) {
return cb(null, buildResults);
}
doConsoleMessage(message, buildResults, params, cb);
});
}
/**
* Perform the build
* @param params
* @param cb
*/
function doBuild(params, cb) {
var doCall = function() {
common.doApiCall(fhreq.getFeedHenryUrl(), createUri(params), params, i18n._("Error reading app: "), function(err, data) {
if (err) {
return cb(err);
}
var keys = [];
if (data.cacheKey) {
keys.push(data.cacheKey);
}
if (keys.length > 0) {
async.map(keys, common.waitFor, function(err, results) {
if (err) {
if ( err[0].error === "Error status returned from SCM: 500") {
return cb(i18n._("Git repository not found. Please check that all provided Git parameters are valid."));
}
return cb(err);
}
if (results[0] && results[0][0] && results[0][0].action) {
doSuccessOutcome(results, cb, params);
} else {
return cb(err, results);
}
});
} else {
return cb(err, data);
}
});
};
doCall();
}
/**
* Create the URL of the built artifact
* @param buildResults
* @param cb
* @returns {*}
*/
function getArtifactUrl(buildResults, cb) {
if (buildResults.buildId) {
//the self-managed build farm will return buildId. Call the artifacts endpoint to get the full url for downloading
var buildId = buildResults.buildId;
var uri = "box/srv/1.1/artifacts/" + buildId;
fhreq.GET(fhreq.getFeedHenryUrl(), uri, function(err, data, raw, response) {
if (err) {
return cb(err);
}
if (response.statusCode !== 200 ) {
return cb(raw);
}
buildResults.url = data.downloadurl;
if (data.otaurl && data.otaurl.indexOf(".plist") >= 0) {
//use the ios html file for ios OTA installation
buildResults.ota_url = fhreq.getFeedHenryUrl() + "box/api/mas/storeitem/iosotahtml?url=" + data.otaurl;
} else {
buildResults.ota_url = data.otaurl;
}
return cb(null, buildResults);
});
} else {
return cb(null, buildResults);
}
}
/**
* Download the app if the download param is === true
* @param params
* @param assetUrl
* @param path
* @param cb
* @returns {*}
*/
function downloadBuild(params, assetUrl, path, cb) {
if (params.download !== 'true') {
return cb();
}
var uri = url.parse(assetUrl);
var fileName = path + uri.pathname.split('/').pop();
var proto = uri.protocol === 'https:' ? https : http;
log.silly(uri, "download uri");
log.silly(fileName, "download filename");
log.silly(proto, "download protocol");
var req = proto.get(uri, function(res) {
var closedAlready = false;
if (res.statusCode !== 200) {
return cb(i18n._("Unexpected response code for file download: ") + res.statusCode + i18n._(" message: ") + res.body);
}
var stream = fs.createWriteStream(fileName);
res.on('data', function(chunk) {
stream.write(chunk);
});
function finishOutoutStream() {
if (!closedAlready) {
closedAlready = true;
return cb(undefined, {
url: assetUrl,
file: fileName
});
}
}
stream.on('finish', function() {
log.silly(i18n._('Finish stream event received'));
finishOutoutStream();
});
stream.on('close', function() {
log.silly(i18n._('close stream event received'));
finishOutoutStream();
});
res.on('end', function() {
log.silly(i18n._('End of netork stream received'));
stream.end();
});
});
req.on('error', function(err) {
return cb(i18n._("Error downloading build: ") + err.message);
});
}