Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Fhc allows for non-blocking build requests. #15

Closed
wants to merge 9 commits into from

2 participants

@skalee
fhc build …           # works as before
fhc build make …      # same as fhc …
fhc build start …     # starts build job, returns cache key
fhc build status …    # polls for job status
@dberesford

no longer required, closing this pull request.

@dberesford dberesford closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 10, 2012
  1. @skalee

    Gitignore.

    skalee authored
  2. @skalee
  3. @skalee

    Build separated.

    skalee authored
Commits on Feb 15, 2012
  1. @skalee

    Works

    skalee authored
  2. @skalee
  3. @skalee

    Docs.

    skalee authored
  4. @skalee

    Minor.

    skalee authored
  5. @skalee

    Nicer help.

    skalee authored
  6. @skalee

    API fixed. Nice output.

    skalee authored
This page is out of date. Refresh to see the latest.
Showing with 214 additions and 104 deletions.
  1. +1 −0  .gitignore
  2. +5 −5 bin/fhc.js
  3. +33 −3 doc/build.md
  4. +126 −60 lib/build.js
  5. +49 −36 lib/common.js
View
1  .gitignore
@@ -3,6 +3,7 @@ dist/
output/
node_modules/
*~
+*.swp
.settings/
.project
fhc-debug.log
View
10 bin/fhc.js
@@ -52,16 +52,16 @@ if (conf.usage && fhc.command !== "help") {
// now actually fire up fhc and run the command.
// this is how to use fhc programmatically:
conf._exit = true;
-fhc.load(conf, function (err) {
+fhc.load(conf, function (err) {
if (err) return errorHandler(err);
var cmd = fhc.commands[fhc.command];
cmd(fhc.argv, function(err, data) {
if (err) return errorHandler(err);
if (data === undefined) {
- output.write("",errorHandler);
+ output.write("",errorHandler);
} else {
- // display table if both requested and supported..
+ // display table if both requested and supported..
if (!conf.json && conf.table && cmd.table) {
console.log(cmd.table.toString());
output.write("", errorHandler);
@@ -72,14 +72,14 @@ fhc.load(conf, function (err) {
}else {
if (typeof data === 'string') return output.write(data, errorHandler);
if (conf.filter) {
- var script = "output.write(data." + conf.filter + ", errorHandler)";
+ var script = "output.write(data." + conf.filter + ", errorHandler)";
eval(script);
}else{
return output.write(data, errorHandler);
}
}
}
- }
+ }
});
})
})()
View
36 doc/build.md
@@ -3,15 +3,45 @@ fhc-build(1) -- Build FeedHenry Applications
## SYNOPSIS
- fhc build app=<appId> destination=<destination> version=<version> config=<config> keypass=<private-key-password> certpass=<certificate-password> download=<true|false>
+ fhc build <make-params>
+ fhc build make <make-params>
+ fhc build start <make-params>
+ fhc build status <status-params>
+
+ where <make-params> is app=<appId> destination=<destination> version=<version> config=<config> keypass=<private-key-password> certpass=<certificate-password> download=<true|false>
+ where <status-params> is cacheKey=<cache-key> start=<start>
where <destination> is one of: android, iphone
where <version> is specific to the destination, see http://www.feedhenry.com/TODO
where <config> is either 'debug' (default) or 'release'
'keypass' and 'certpass' only needed for 'release' builds
-
+ where <cache-key> is a cache key of remote build job to poll
+ where <start> is number incremented in subsequent calls
+
## DESCRIPTION
This command can be used to build your FeedHenry applications.
-e.g.
+### MAKE
+
+ fhc build make <make-params>
+ fhc build <make-params>
+
+Performs whole build process and will exit when build is ready to download (or when download completes if <download>=true).
+
+e.g.
fhc build app=mfLkParVTDcr80-uEk8OhEfT destination=iphone config=distribution keypass=password certpass= version=4.0
+
+### START
+
+ fhc build start <make-params>
+
+Starts remote build process and exits immediately. Returns one or more cache keys which represent remote jobs started. Use fhc build status to poll for jobs status.
+
+### STATUS
+
+ fhc build status <status-params>
+
+Polls for status of the remote job identified by <cache-key>. The <cache-key>s are returned by fhc build start command.
+
+Pass 0 as <start> when you are calling this command for the first time for given build. Pass subsequent numbers for subsequent calls. (?)
+
View
186 lib/build.js
@@ -1,7 +1,14 @@
module.exports = build;
-build.usage = "\nfhc build app=<appId> destination=<destination> version=<version> config=<config> keypass=<private-key-password> certpass=<certificate-password> download=<true|false> provisioning=<path-to-provisioning-profile>"
+build.usage = "\nfhc build <make-parameters>"
+ + "\nfhc build make <make-parameters>"
+ + "\nfhc build start <make-parameters>"
+ + "\nfhc build status <status-parameters>"
+ + "\n"
+ + "\nwhere: <make-parameters> means: app=<appId> destination=<destination> version=<version> config=<config> keypass=<private-key-password> certpass=<certificate-password> download=<true|false> provisioning=<path-to-provisioning-profile>"
+ + "\nwhere: <status-parameters> means: cacheKey=<cache-key> start=<start>"
+ + "\n"
+ "\nwhere <destination> is one of: andriod, iphone, ipad, blackberry, windowsphone7"
+ "\nwhere <version> is specific to the destination, see supported destinations here: http://www.feedhenry.com/TODO"
+ "\nwhere <config> is either 'debug' (default), 'distribution', or 'release'"
@@ -21,25 +28,39 @@ var url = require("url");
var path = require("path");
var fs = require("fs");
var events = require("events");
+var validateArgs = {};
+var commands = {};
// main build entry point
function build (args, cb) {
- try{
+ try {
var argObj = parseArgs(args);
- validateArgs(argObj);
+ validateArgs[argObj.command](argObj);
} catch (x) {
return cb("Error processing args: " + x + "\nUsage: " + build.usage);
}
- doBuild(argObj, cb);
+ argObj.callback = cb; //function(data){ cb(null, data); };
+ log(Object.keys(argObj));
+ commands[argObj.command](argObj);
};
-// expects all args to be in
+// expects all args to be in
function parseArgs(args) {
- var opts = new Object();
- for(var i=0; i<args.length; i++) {
- var arg = args[i];
- if(arg.indexOf('=') == -1) throw new Error('Invalid argument format: ' + arg);
- var kv = arg.split("=");
+ var opts = new Object(),
+ additionalArgsStart;
+
+ if (Object.keys(commands).indexOf(args[0]) >= 0) {
+ opts.command = args[0];
+ additionalArgsStart = 1;
+ } else {
+ opts.command = 'make';
+ additionalArgsStart = 0;
+ }
+
+ for(var i = additionalArgsStart ; i < args.length ; i++) {
+ var arg = args[i],
+ kv = arg.split("=", 2);
+ if (kv.length < 2) throw new Error('Invalid argument format: ' + arg);
log.silly(kv, 'build arg');
opts[kv[0]] = (kv[1] == undefined ? "" : kv[1]);
}
@@ -49,7 +70,7 @@ function parseArgs(args) {
// runs through the args we got and validates as per our build rules
// TODO - may need to be more specific depending on build target type
-function validateArgs(args) {
+validateArgs.make = function(args) {
//looks for set alias
if(args.app !== undefined ){/*look for alias*/
args.app = fhc.appId(args.app);
@@ -59,69 +80,104 @@ function validateArgs(args) {
if (args.version == undefined) throw new Error("Missing 'version' parameter");
if (args.config == undefined) args.config = 'debug';
-
+
if (args.config == 'release' || args.config == 'distribution') {
if (args.keypass == undefined) throw new Error("Missing 'keypass' parameter");
if (args.certpass == undefined) throw new Error("Missing 'certpass' parameter");
args.privateKeyPass = args.keypass; // naff..
}
-
+
if(args.destination == "iphone" || args.destination == "ipad") {
args.deviceType = args.destination;
}
-}
+};
+validateArgs.start = validateArgs.make;
+validateArgs.status = function(args) {
+};
-// convert our args..
-function argsToPayload(args) {
- var pl = "generateSrc=false";
- for (var i in args) {
- pl = pl + "&" + i + "=" + args[i];
- }
- return pl;
-}
-// do the build..
-function doBuild(args, cb) {
- var uri = "box/srv/1.1/wid/" + fhc.domain + "/" + args.destination + "/" + args.app + "/deliver";
- var doCall = function(){
- var payload = argsToPayload(args);
- common.doApiCall(fhreq.getFeedHenryUrl(), uri, payload, "Error reading app: ", function(err, data) {
- if(err) return cb(err);
- var keys = [];
- if(data.cacheKey) keys.push(data.cacheKey);
- if(data.stageKey) keys.push(data.stageKey);
- if(keys.length > 0) {
- async.map(keys, common.waitFor, function(err, results) {
- if (err) return cb(err);
- if (results[0] !== undefined) {
- var build_asset = results[0][0].action.url;
- build.message = "Download URL: " + build_asset;
- downloadBuild(args, build_asset, null, function() {
- return cb(err, results);
- });
- }
- });
- } else {
- return cb(err, data);
- }
+commands.start = function(args) {
+ startBuild(args, args.callback);
+};
+commands.status = function(args) {
+ //TODO Consider making it standalone command. Some "status" method may be useful in future. On the other hand, some output should be context specific (here: build specific)
+ //TODO Consider handling multiple cacheKeys
+ common.isJobDone(args.cacheKey, args.start, function(err, response) {
+ if (err) return args.callback(err);
+ if (response.done) {
+ //build.message = "Job " + args.cacheKey + " is done.";
+ return doSomethingWithCompleteJob(args, [response.data]);
+ } else if (response.progress) {
+ build.message = "Job " + args.cacheKey + " is " + response.progress + "% done.";
+ } else {
+ build.message = "Job " + args.cacheKey + " is in progress.";
+ }
+ return args.callback(undefined, response.data);
+ })
+};
+commands.make = function(args) {
+ startBuild(args, function(err_cant_appear_here, keys) {
+ async.map(keys, common.waitFor, function(err, results) {
+ if (err) return args.callback(err);
+ return doSomethingWithCompleteJob(args, results);
});
+ });
+};
+
+function doSomethingWithCompleteJob(args, results) {
+ if (results[0] !== undefined) {
+ var build_asset = results[0][0].action.url;
+ if (build_asset) {
+ build.message = "Download URL: " + build_asset;
+ downloadBuild(args, build_asset, null, function() {
+ return args.callback(undefined, results);
+ });
+ } else {
+ build.message = "Download URL is not specified in this job. Try the other cacheKey.";
+ return args.callback(undefined, results);
+ }
}
-
- if((args.destination === "iphone" || args.destination === "ipad") && args.provisioning){
- var resourceUrl = "/box/srv/1.1/dev/account/res/upload";
- var fields = {dest: args.destination, resourceType: 'provisioning', buildType: args.config, templateInstance: args.app};
- fhreq.uploadFile(resourceUrl, args.provisioning, fields, "application/octet-stream", function(err, data){
- if(data.result && data.result === "ok"){
- log.info("Provisioning profile uploaded");
- doCall();
- } else {
- cb(err, "Failed to upload provisioning profile. Response is " + JSON.stringify(data));
- }
- })
+}
+
+function startBuild(args, cb) {
+ var afterProvisioning = function() { requestBuildStart(args, cb); };
+ if((this.destination === "iphone" || this.destination === "ipad") && this.provisioning){
+ uploadProvisioning(args, afterProvisioning);
} else {
- doCall();
+ afterProvisioning();
}
-};
+}
+
+function uploadProvisioning(args, cb) {
+ var resourceUrl = "/box/srv/1.1/dev/account/res/upload";
+ var fields = {dest: args.destination, resourceType: 'provisioning', buildType: args.config, templateInstance: args.app};
+ fhreq.uploadFile(resourceUrl, args.provisioning, fields, "application/octet-stream", function(err, data){
+ if(data.result && data.result === "ok"){
+ log.info("Provisioning profile uploaded");
+ cb();
+ } else {
+ args.callback(err, "Failed to upload provisioning profile. Response is " + JSON.stringify(data));
+ }
+ });
+}
+
+function requestBuildStart(args, cb) {
+ var uri = "box/srv/1.1/wid/" + fhc.domain + "/" + args.destination + "/" + args.app + "/deliver",
+ payload = argsToPayload(args);
+ common.doApiCall(fhreq.getFeedHenryUrl(), uri, payload, "Error reading app: ", function(err, data) {
+ if(err) return args.callback(err);
+ var keys = [];
+ if(data.cacheKey) keys.push(data.cacheKey);
+ if(data.stageKey) keys.push(data.stageKey);
+ if (keys.length > 0) {
+ build.message = "Build started. Watch for following keys: " + keys.join(', ');
+ return cb(undefined, keys);
+ } else {
+ build.message = "Build started. No keys returned however, but following data: " + JSON.stringify(data || {});
+ return cb(undefined, data);
+ }
+ });
+}
function downloadBuild(args, asset_url, path, callback) {
if (args.download !== 'true') { return callback(); }
@@ -161,3 +217,13 @@ function downloadBuild(args, asset_url, path, callback) {
});
});
}
+
+// convert our args..
+function argsToPayload(args) {
+ var pl = "generateSrc=false";
+ for (var i in args) {
+ pl = pl + "&" + i + "=" + args[i];
+ }
+ return pl;
+}
+
View
85 lib/common.js
@@ -20,49 +20,62 @@ exports.doApiCall = function(host, uri, payload, errorMsg, cb) {
if (remoteData.address["login-status"] == 'none'){
return cb("Error, check your login details.. \n" + remoteData.msg);
}
- }
+ }
if (remoteData.status != undefined && remoteData.status !== 'ok'){
var msg = remoteData.message || remoteData.msg;
return cb(errorMsg + msg, remoteData);
}
return cb(undefined, remoteData);
- });
+ });
};
+exports.isJobDone = function(cacheKey, start, cb) {
+ log.verbose("polling for cacheKey: " + cacheKey + " start: " + start + " to complete", "Polling");
+ var cc = new Object();
+ cc.cacheKey = cacheKey;
+ cc.start = start;
+ var params = [cc];
+
+ var uri = 'box/srv/1.1/dat/log/read?cacheKeys=' + JSON.stringify(params);
+ uri = uri.replace(/"/g,'%22');
+
+ fhreq.GET(fhreq.getFeedHenryUrl(), uri, function (err, remoteData, raw, respose){
+
+ if (err) return cb(err);
+ if (remoteData[0] && remoteData[0].status != undefined && remoteData[0].status == 'pending') {
+ log.silly(remoteData[0].log, "waitforjob");
+ return cb(undefined, {done: false, progress: (remoteData[0] && remoteData[0].progress)});
+ }else {
+ if(remoteData[0] && remoteData[0].status && remoteData[0].status === 'error') {
+ return cb(remoteData);
+ }else {
+ return cb(undefined, {done: true, data: remoteData});
+ }
+ }
+ });
+}
+
// Poll remote job until its finished
exports.waitForJob = function(cacheKey, start, cb) {
var wantJson = ini.get('json');
setTimeout(function() {
log.verbose("polling for cacheKey: " + cacheKey + " start: " + start + " to complete", "Polling");
- var cc = new Object();
- cc.cacheKey = cacheKey;
- cc.start = start;
- var params = [cc];
-
- var uri = 'box/srv/1.1/dat/log/read?cacheKeys=' + JSON.stringify(params);
- uri = uri.replace(/"/g,'%22');
-
- fhreq.GET(fhreq.getFeedHenryUrl(), uri, function (err, remoteData, raw, respose){
-
- if (err) return cb(err);
- if (remoteData[0] && remoteData[0].status != undefined && remoteData[0].status == 'pending') {
- log.silly(remoteData[0].log, "waitforjob");
+ exports.isJobDone(cacheKey, start, function(err, result) {
+ if (err) {
+ return cb(err);
+ } else if (result.done) {
+ return cb(undefined, result.data);
+ } else {
start++;
if(!wantJson) {
- if(remoteData[0].progress){
+ if(result.progress){
console.log("Progress: " + remoteData[0].progress + "%");
}else {
process.stdout.write('.');
}
}
return exports.waitForJob(cacheKey, start++, cb);
- }else {
- if(remoteData[0] && remoteData[0].status && remoteData[0].status === 'error') {
- return cb(remoteData);
- }else {
- return cb(undefined, remoteData);
- }
}
});
}, 500);
@@ -73,7 +86,7 @@ exports.getAppName = function(appId){
var fhcluster = fhc.config.get("fhcluster");
//
- // Note: we replace _ and - with 0's
+ // Note: we replace _ and - with 0's
//
var app = appId.replace(/_/g, '0');
app = app.replace(/-/g, '0');
@@ -81,7 +94,7 @@ exports.getAppName = function(appId){
log.silly(appId, 'cf appId');
var appName = fhcluster + "-" + app;
- return appName.toLowerCase();
+ return appName.toLowerCase();
};
// common strlen function
@@ -91,7 +104,7 @@ function strlen(str) {
if (typeof str === 'object') s = str.toString();
var len = 0;
if (s) len = s.length;
- return len;
+ return len;
};
// used in conjunction with async.map to wait for multiple jobs to finish
@@ -123,23 +136,23 @@ exports.createTableForApps = function(apps) {
if(strlen(app.title) > maxTitle) maxTitle = strlen(app.title);
if(strlen(app.description) > maxDescription) maxDescription = strlen(app.description);
}
-
+
if (maxDescription > exports.maxTableCell) maxDescription = exports.maxTableCell;
if (maxTitle > exports.maxTableCell) maxTitle = exports.maxTableCell;
// create our table
- var table = new Table({
+ var table = new Table({
head: ['Id', 'Title', 'Description','Alias'],
colWidths: [maxId +2 , maxTitle + 2, maxDescription + 2, maxAlias + 2],
style: exports.style()
});
-
+
// populate our table
for (var b in apps) {
var app = apps[b];
table.push([app.id, app.title, app.description, app.alias]);
- }
- return table;
+ }
+ return table;
};
exports.createTableForTemplates = function (apps) {
@@ -200,7 +213,7 @@ function createNVTable(pairs) {
}
if (maxValue > exports.maxTableCell) maxValue = exports.maxTableCell;
- var table = new Table({
+ var table = new Table({
head: ['Name', 'Value'],
colWidths: [maxName+2, maxValue+2],
style: exports.style()
@@ -225,7 +238,7 @@ function createObjectTable(obj) {
colWidths.push(width);
}
- var table = new Table({
+ var table = new Table({
head: heads,
colWidths: colWidths,
style: exports.style()
@@ -243,14 +256,14 @@ function createTableForAppProps(app) {
Title: app.inst.title,
Description: app.inst.description,
W3C: app.app.w3cid,
- Widg: app.app.guid,
+ Widg: app.app.guid,
Config: JSON.stringify(app.inst.config)
};
var scm = app.app.config.scm;
if (scm) {
- if (scm.url) pairs["Git Url"] = scm.url;
- if (scm.branch) pairs["Git Branch"] = scm.branch;
- if (scm.key) pairs["Key"] = scm.key;
+ if (scm.url) pairs["Git Url"] = scm.url;
+ if (scm.branch) pairs["Git Branch"] = scm.branch;
+ if (scm.key) pairs["Key"] = scm.key;
}
return createNVTable(pairs);
Something went wrong with that request. Please try again.