Task dependency in Gulp

At Cermati, we use Gulp as our task runner to run most of our build tasks. My first two weeks of working were spent on implementing build-related stuff. Two of which were auto-revert on broken build and auto-tag on sucessful build on production branch. For these two tasks, I used gulp-git plugin, which, as the name implies, handles Git-related tasks using Gulp. At first, my implementation of those two tasks was as follows:

var git = require('gulp-git');
var gulp = require('gulp');

// ensure ci tasks are run inside ci environment
gulp.task('ciSafeguard', function () {
  if (!process.env.TRAVIS) {
    throw new Error('This task should only be run by Travis CI');
  }
});

// this task should only be run by travis
gulp.task('ciGitPrepare', ['ciSafeguard'], function () {
  var username = process.env.ROBOT_USERNAME;
  var email = process.env.ROBOT_EMAIL;
  var password = process.env.ROBOT_PASSWORD;
  var repoSlug = process.env.TRAVIS_REPO_SLUG;
  var branch = process.env.TRAVIS_BRANCH;
  var gitUrl = 'https://' + username + ':' + password + '@github.com/' + repoSlug + '.git';

  git.checkout(branch);
  git.exec({args: 'config --global user.name ' + username});
  git.exec({args: 'config --global user.email ' + email});
  git.exec({args: 'remote set-url origin ' + gitUrl});
});

// this task should only be run by travis
gulp.task('ciRevert', ['ciGitPrepare'], function () {
  git.exec({args: 'log -n 1 --pretty=format:"%ce"'}, function (_, stdout) {
  // prevent reverting commit by travis
  if (stdout === process.env.ROBOT_EMAIL) {
    return true;
  }
  git.exec({args: 'revert --no-edit HEAD'}, function () {
    git.push('origin', process.env.TRAVIS_BRANCH);
  });
 });
});

// this task should only be run by travis
gulp.task('ciTag', ['ciGitPrepare'], function () {
  var pkg = require('./package.json');
  var buildNumber = process.env.TRAVIS_BUILD_NUMBER;
  var version = pkg.version + '+build' + buildNumber;
  git.tag(version, 'Auto-generated tag version ' + version, function () {
    git.push('origin', process.env.TRAVIS_BRANCH, {args: '--tags'});
  });
});

We use Travis CI at Cermati so in our build environment there are several variables like TRAVIS, TRAVIS_BRANCH, etc. and there are also some variables defined in our .travis.yml file like ROBOT_USERNAME, ROBOT_EMAIL, etc. which store our cermati-robot Github account.

I thought this code was perfectly okay and I have set the task dependencies correctly, like ciTag depends on ciGitPrepare which depends on ciSafeGuard. So I expected that only after ciGitPrepare completely finish, ciTag will be run. However, I notice that sometimes the build errored because Git cannot acquire the lock; it seemed that there were two Git processes running simultaneously. This shouldn’t be possible since all git commands were run sequentially, right? Taking a better look at the output, I found that the git commands inside ciGitPrepare sometimes were executed after ciGitPrepare had finished. So, when ciTag was running git push, it errored since another Git process was running git config. How could this happen? Didn’t I already set the dependency correctly?

Reading the Gulp documentation on task dependency once again, I realized that it is not enough to only declare task dependency; we also need to explicitly state that our task dependency is finished by accepting a callback, returning a promise, or returning a stream, as the documentation says.

Modifying the code a little bit, I ended up with this:

var async = require('async');
var git = require('gulp-git');
var gulp = require('gulp');

// ensure ci tasks are run inside ci environment
gulp.task('ciSafeguard', function (callback) {
  if (!process.env.TRAVIS) {
    return callback(new Error('This task should only be run by Travis CI'));
  }
  return callback();
});

// this task should only be run by travis
gulp.task('ciGitPrepare', ['ciSafeguard'], function (callback) {
  var username = process.env.ROBOT_USERNAME;
  var email = process.env.ROBOT_EMAIL;
  var password = process.env.ROBOT_PASSWORD;
  var repoSlug = process.env.TRAVIS_REPO_SLUG;
  var branch = process.env.TRAVIS_BRANCH;
  var gitUrl = 'https://' + username + ':' + password + '@github.com/' + repoSlug + '.git';

  async.series([
    function (cb) {
      git.checkout(branch, cb);
    },
    function (cb) {
      git.exec({args: 'config --global user.name ' + username}, cb);
    },
    function (cb) {
      git.exec({args: 'config --global user.email ' + email}, cb);
    },
    function (cb) {
      git.exec({args: 'remote set-url origin ' + gitUrl}, cb);
    }
  ], callback);
});

// this task should only be run by travis
gulp.task('ciRevert', ['ciGitPrepare'], function () {
  git.exec({args: 'log -n 1 --pretty=format:"%ce"'}, function (_, stdout) {
    // prevent reverting commit by travis
    if (stdout === process.env.ROBOT_EMAIL) {
      return true;
    }
    git.exec({args: 'revert --no-edit HEAD'}, function () {
      git.push('origin', process.env.TRAVIS_BRANCH);
    });
  });
});

// this task should only be run by travis
gulp.task('ciTag', ['ciGitPrepare'], function () {
  var pkg = require('./package.json');
  var buildNumber = process.env.TRAVIS_BUILD_NUMBER;
  var version = pkg.version + '+build' + buildNumber;
  git.tag(version, 'Auto-generated tag version ' + version, function () {
    git.push('origin', process.env.TRAVIS_BRANCH, {args: '--tags'});
  });
});

Now all the tasks that are depended on are accepting callbacks. Note that I used async library in ciGitPrepare task to avoid ugly nested callbacks (known as callback hell) when writing sequential code. Also note that ciRevert and ciTag does not accept callback nor return anything since there are no tasks dependent on them.

With this code, the problem is now solved. The tasks are run in the correct order and there are no more errors. Hope this helps anyone struggling with similar problem.