lib/build.js

/* global sails,Passport */

var exec = require('child_process').exec;
var path = require('path');
var azure = require('./azure');


/**
 * Build engine module
 * 
 * @module build
 */
module.exports = {
	
	/**
   * Takes a command template and a model, tokenizes the model,
   * runs the command, and calls the callback.
   *
   * The following tokens are availble: owner, repository, branch,
   * token (GitHub access token), source (temporary build directory),
   * destination (final destination for build site).
   *
   * The source directory should be deleted after build completes.
   *
   * @param {Array} cmd - array of string templates, each item is a command
   * @param {Build} model - build model to parse
   * @param {Function(error)} done - callback function
   */
  _run: function (cmd, model, done) {
    var service = this;
    var defaultBranch = model.branch === model.site.defaultBranch;
    var tokens = {
      branch: model.branch,
      branchURL: defaultBranch ? '' : '/' + model.branch,
      root: defaultBranch ? 'site' : 'preview',
      config: model.site.config
    };
    // Temporary until workaround for single line IF EXIST logic is implemented
    var template = _.template(cmd.filter(function onFilter(val) {
      return val;
    }).join(' & '));

    // Populate user's passport
    Passport.findOne({ user: model.user.id }).exec(function onFind(err, passport) {

      // End early if error
      if (err) {
        return done(err, model);
      }

      model.user.passport = passport;

      // Continue run process with populated model
      next(model);

    });
		
		/**
		 * Execute build command in child process and
		 * initiate publishing
		 * 
		 * @param {Build} model - build model to parse
		 */
    function next(model) {

      // Set populated token values
      tokens.repository = model.site.repository;
      tokens.owner = model.site.owner;
      tokens.token = (model.user.passport) ? model.user.passport.tokens.accessToken : '';
      tokens.baseurl = '';

      // Set up source and destination paths
      tokens.source = sails.config.build.tempDir + '/source/' +
      tokens.owner + '/' + tokens.repository + '/' + tokens.branch;
      tokens.destination = sails.config.build.tempDir + '/destination/' +
      tokens.owner + '/' + tokens.repository + '/' + tokens.branch;
      tokens.publish = sails.config.build.publishDir + '/' + tokens.root + '/' +
      tokens.owner + '/' + tokens.repository + tokens.branchURL;

      // Remove leading slash and normalize path for Windows
      tokens.source = path.normalize(tokens.source.replace(/^\//, ''));
      tokens.destination = path.normalize(tokens.destination.replace(/^\//, ''));
      tokens.publish = path.normalize(tokens.publish.replace(/^\//, ''));
      
      // Run command in child process and
      // call callback with error and model
      exec(template(tokens), function onExecute(err, stdout, stderr) {
        if (stdout) sails.log.verbose('stdout: ' + stdout);
        if (stderr) sails.log.verbose('stderr: ' + stderr);
        if (err) return done(err, model);
        service.publish(tokens, model, done);
      });
    }
  },
  
  /**
	 * Jekyll build task for execution on Windows
	 * 
	 * @param {Build} model - build model to parse
	 * @param {Function(error)} done - callback function
	 */
  jekyll: function (model, done) {
    this._run([
      'echo. removing existing source directory ${source}',
      'RMDIR ${source} /S /Q 2> nul',
      'echo creating source directory ${source}',
      'MKDIR ${source}',
      'echo cloning branch ${branch} from owner ${owner} at repository ${repository}',
      'git clone -b ${branch} --single-branch ' +
      'https://${token}@github.com/${owner}/${repository}.git ${source} 2>&1',
      'echo baseurl: ${baseurl} > ${source}\\_config_base.yml',
      'echo branch: ${branch} >> ${source}\\_config_base.yml',
      
      // This command conditionally added since an empty model.site.config object
      // results an execution of echo by itself which leads to miscellaneous
      // output in _config_base.yml
      (model.site.config) ? 'echo ${config} >> ${source}\\_config_base.yml' : null,
      
      'jekyll build --safe --config ${source}\\_config.yml,${source}\\_config_base.yml ' +
      '--source ${source} --destination ${source}\\_site 2>&1',
      'echo removing existing destination directory ${destination}',
      'RMDIR ${destination} /S /Q 2> nul',
      'echo creating destination directory ${destination}',
      'MKDIR ${destination}',
      'echo recursively copying source directory ${source}\\_site to destination directory ${destination}',
      'XCOPY ${source}\\_site ${destination} /E /I /Q 2>&1',
      'XCOPY ' + path.normalize('node_modules/sails-hook-federalist-ms/templates/webapp/web.config') + ' ${destination} /E /I /Q 2>&1',
      'echo removing source directory ${source}',
      'RMDIR ${source} /S /Q 2> nul',
    ], model, done);
  },
	
	/**
	 * Hugo build task for execution on Windows
	 * 
	 * @param {Build} model - build model to parse
	 * @param {Function(error)} done - callback function  
	 */
  hugo: function (model, done) {
    this._run([
      'RMDIR ${source} /S /Q 2> nul',
      'MKDIR ${source}',
      'git clone -b ${branch} --single-branch ' +
      'https://${token}@github.com/${owner}/${repository}.git ${source} 2>&1',
      'hugo --baseUrl=${baseurl} ' +
      '--source=${source} 2>&1',
      'RMDIR ${destination} /S /Q 2> nul',
      'MKDIR ${destination}',
      'XCOPY ${source}\\public ${destination} /E /I /Q 2>&1',
      'RMDIR ${source} /S /Q 2> nul',
    ], model, done);
  },
	
	/**
	 * Static build task for execution on Windows
	 * 
	 * @param {Build} model - build model to parse
	 * @param {Function(error)} done - callback function  
	 */
  static: function (model, done) {
    this._run([
      'RMDIR ${source} /S /Q 2> nul',
      'MKDIR ${source}',
      'git clone -b ${branch} --single-branch ' +
      'https://${token}@github.com/${owner}/${repository}.git ${source} 2>&1',
      'RMDIR ${destination} /S /Q 2> nul',
      'XCOPY ${source} ${destination} /E /I /Q 2>&1',
      'RMDIR ${source} /S /Q 2> nul'
    ], model, done);
  },
	
	/**
	 * Publish a built site by copiting it to its publish directory
	 * or pushing it to an Azure Web App
	 * 
	 * @param {Object} tokens - tokens from the _run command
	 * @param {Build} model - build model to parse
	 * @param {Function(error, model)} done - callback function
	 */
  publish: function (tokens, model, done) {
     
    // If an Azure configuration and/or S3 configuration is defined, publish site accordingly
    if (sails.config['federalist-ms'].azure) {
      var rgName = 'federalist-' + tokens.owner;
      // Temporary hardcoding path
      var rgTemplatePath = 'node_modules/sails-hook-federalist-ms/templates/webapp/azuredeploy.json';
      var rgDeploymentName = rgName + '-deployment-' + model.id;
      var webAppName = tokens.owner + '-' + tokens.repository;
      var appHostingPlanName = rgName + '-web';

      var publishConfig = {
        directory: tokens.destination,
        rgName: rgName,
        rgTemplatePath: rgTemplatePath,
        rgDeploymentName: rgDeploymentName,
        webAppName: webAppName,
        appHostingPlanName: appHostingPlanName
      };

      sails.log.verbose('Publishing job: ', model.id,
        ' => ', sails.config.build.azure);
      azure.publish(publishConfig, function onPublish(err) {
        done(err, model);
      });
    } else {
      var cmd = _.template([
        'RMDIR ${publish} /S /Q 2> nul',
        'MKDIR ${publish}',
        'XCOPY ${destination} ${publish} /E /I /Q 2>&1'
      ].join(' & '));

      sails.log.verbose('Publishing job: ', model.id,
        ' => ', tokens.publish);
      exec(cmd(tokens), function onExecute(err, stdout, stderr) {
        if (stdout) sails.log.verbose('stdout: ' + stdout);
        if (stderr) sails.log.verbose('stderr: ' + stderr);
        done(err, model);
      });
    }
  }

};