Change - Know that nothing stays the same

Deploy a JS project - Part V

July 02, 2015

deploy
shipit
javascript
tasks

Now that we have our webserver configured, our NodeJS consuming Tumblr API and ready to respond to our AngularJS application, we can work with our deploy script.

We are going to use SHIPIT (https://github.com/shipitjs/shipit), this is a tool in Javascript to do a deploy to a server. It has the same philosophy as Caspistrano.

The shipit deploy task is in a different repository (https://github.com/shipitjs/shipit-deploy). We need to write something else in order to have the flow what we need.

Shipit flowStarting from the original flow that we have from Shipit: Checking the Deploy process we need to change some steps.Our shipit config looks like this:

module.exports = {
  options: {
    workspace: '/tmp/path-to-temp',
    source: '/path-to/dist',
    deployTo: '/path-to-server-destination',
    repositoryUrl: '/path-to/dist',
    ignores: ['.git'],
    keepReleases: 5,
    key: '/path-to/.ssh',
    shallowClone: true,
    dependencies: ['node_modules', 'bower_components'],
    symlinks: ['node_modules', 'bower_components']
  },
  development: {
    servers: 'user@ip',
    deployTo: '/path-to-server-destination'
  },
  staging: {
    servers: 'user@ip',
    deployTo: '/path-to-server-destination',
  }
}

InitializationWe need to prepare the code that we need to send to our server, to prepare this code we need to do:

  • Clean any previous distribution code
  • Build the new distribution code
  • Initialize a git repository for the new /dist code

An important note about point 3, we need to create a git repository for the code we are going to deploy to the server, the code that we need to deploy to the server is a compiled version of our application (server+frontend), is not necessary to send this file to an external repository.

We can initialize a git repository in our /dist folder in order to use it as the source to fetch the code in the tmp folder.

Updating with dependencies

After the code is ir our server, it needs to update any bower component or npm module. We need to run ‘bower install’ and ‘npm install’ and share the folders with that content, this is necessary to only install or update new packages.

We need to allow the possibility of rollback, to do this, we have bower.json and package.json per release, with the same idea as before, after update our code in the server -with a new deploy or a rollback- we use those files to do the ‘bower install’ or ‘npm install’. We only can use one release and is not a problem to have only one folder for bower and one folder for npm.

PublishingAs our last step, we need to reinitialize the NodeJS server. To do this we need to cancel our previous forever instance and run a new one with the new app.js file we have in the new release.

Events to connect the dot.

A good thing about Shipit is the possibility to change the original flow or add thing to the flow to achieve our goal.We need to use the events defined in the flow to add our steps.For example, in our case this are the steps we add to the original flow:

  • Clean
  • Build
  • Initialize repository
grunt.shipit.on('deploy', function () { grunt.task.run([ 'clean:dist', 'build:dist', 'init-repository' ]); });Update Node Modules and Bower Components
grunt.shipit.on('updated', function() { grunt.task.run([ 'update-node-modules', 'update-bower-components' ]); });Restart our forever process
grunt.shipit.on('published', function() { grunt.task.run('forever-restart'); });When we create our task we can define to wait until the task its done to continue with the next. To do this we use “async” library.As an example we can see:
grunt.registerTask('init-repository', function() {
  var done    = this.async(),
  source  = grunt.config('shipit.options.source');
  return grunt.shipit.local('cd ' + source + ' && rm -rf .git && git init --quiet && git add . && git commit -am "intial commit"').then(done);
});

The full script to change our shipit flow looks like this:

```javascript
// This script adds tasks to the original Shipit flow
// to run this script we need to use: grunt shipit:environment deploy

'use strict';

module.exports = function(grunt) {
  var async   = require('async');
  var path    = require('path');
  var isNull  = require('lodash').isNull;

  function getEnvironment(){
    var tasks = grunt.cli.tasks[0];
    var environment = tasks.split(":");
    return environment[1];
  }

  // Clean & Build and Initialize a respository with the content in /dist
  grunt.shipit.on('deploy', function () {
    grunt.task.run([
      'clean:dist',
      'build:dist',
      'init-repository'
    ]);
  });

  // Initialize a respository with the content in /dist
  grunt.registerTask('init-repository', function() {
    var done    = this.async(),
        source  = grunt.config('shipit.options.source');
    return grunt.shipit.local('cd ' + source + ' && rm -rf .git && git init --quiet && git add . && git commit -am "intial commit"').then(done);
  });


  // Update Node Modules and Bower Components
  grunt.shipit.on('updated', function() {
    grunt.task.run([
      'update-node-modules',
      'update-bower-components'
    ]);
  });

  // update node_modules
  // This task use a shared folder named node_modules to hold those modules
  grunt.registerTask('update-node-modules', function() {
    var environment   = getEnvironment(),
        done          = this.async(),
        deploy_to     = grunt.config(['shipit', environment, 'deployTo'].join('.')),
        shared        = path.join(deploy_to, 'shared'),
        node_modules  = path.join(shared, 'node_modules'),
        node_bin_path = '/home/deployer/.nvm/v0.10.31/bin/';

    grunt.shipit.remote(['ln -s', node_modules, path.join(grunt.shipit.releasePath, 'node_modules')].join(' ')).then(function(){
      grunt.shipit.remote(['cd', grunt.shipit.releasePath, '&&', node_bin_path + 'npm install'].join(' ')).then(done);
    });
  });

  // update bower_components
  // This task use a shared folder named bower_components to hold those components
  grunt.registerTask('update-bower-components', function() {
    var environment       = getEnvironment(),
        done              = this.async(),
        deploy_to         = grunt.config(['shipit', environment, 'deployTo'].join('.')),
        shared            = path.join(deploy_to, 'shared'),
        bower_components  = path.join(shared, 'bower_components'),
        node_bin_path     = '/home/deployer/.nvm/v0.10.31/bin/';

    grunt.shipit.remote(['ln -s', bower_components, path.join(grunt.shipit.releasePath, 'public', 'bower_components')].join(' ')).then(function(){
      grunt.shipit.remote(['PATH="$PATH:/home/deployer/.nvm/v0.10.31/bin;" && source ~/.nvm/nvm.sh && node --version && cd', grunt.shipit.releasePath, '&&', ' nvm run v0.10.31 bower install', grunt.shipit.releasePath + '/bower_components'].join(' ')).then(done);
    });
  });

  // Restart our nodeJS server
  grunt.shipit.on('published', function() {
    grunt.task.run('forever-restart');
  });

  // We need to stop any previous process to the app.js script and start a new one
  grunt.registerTask('forever-restart', function() {
    var environment   = getEnvironment(),
        done          = this.async(),
        node_bin_path = '/home/deployer/.nvm/v0.10.31/bin/',
        deploy_to     = grunt.config(['shipit', environment, 'deployTo'].join('.'));

    grunt.shipit.remote('source ~/.nvm/nvm.sh && ' + node_bin_path +
                        "forever stop " + deploy_to + "/current/server/app.js"
                       ).then(startServer).catch(startServer);

    function startServer() {
      grunt.shipit.remote('source ~/.nvm/nvm.sh && export NODE_ENV=production && export PORT=9000 && ' + node_bin_path + "forever start " + deploy_to + "/current/server/app.js").then(done);
    }
  });

}

Agustin Vinao
Agustin Vinao.

Paradox: Life is a mystery. Don't waste time trying to figure it out.
Humor: Keep a sense of humor, especially about yourself. It is a strength beyond all measure.
Change: Know that nothing stays the same.