Grunt is a great tool for building, running and deploying ‘Single Page Apps’. I have a single grunt command to build and deploy to S3 for production, but recently I added some extra functionality to make deployment safer and even easier:
- Abort if you are not on master branch
- Abort if there are any uncommitted local changes
- Abort if not up to date with the origin repo
- Create a file revision.txt containing the deployed git revision hash, so we can GET it from the server and be sure of which revision is live
- Automatically create a tag with the date and time.
I found a few existing pieces to implement some of these, but not all of them, and I ended up with a set of custom Grunt tasks, which I present here in the hope that they are useful to others. They could perhaps be packaged up into a Grunt plugin.
With no further ado, here is the stripped down Gruntfile, just showing the parts relevant to this post, though the deploy-prod task definition leaves in the other task names for context in the overall flow.
// Load all grunt tasks matching the `grunt-*` pattern
require('load-grunt-tasks')(grunt);
grunt.initConfig({
// Lots of other Grunty things
// ...
// Executing the 'gitinfo' command populates grunt.config.gitinfo with useful git information
// (see https://github.com/damkraw/grunt-gitinfo for details) plus results of our custom git commands.
gitinfo: {
commands: {
'status': ['status', '--porcelain'],
'origin-SHA': ['rev-parse', '--verify', 'origin']
}
},
gittag: {
prod: {
options: {
tag: 'prod-<%= grunt.template.today("ddmmyy-HHMM") %>'
}
}
},
shell: {
gitfetch: {
command: 'git fetch'
},
saverevision: {
// Save the current git revision to a file that we can GET from the server, so we can
// be sure exactly which version is live.
command: 'echo <%= gitinfo.local.branch.current.SHA %> > revision.txt',
options: {
execOptions: {
cwd: 'dist'
}
}
}
},
});
grunt.registerTask('check-branch', 'Check we are on required git branch', function(requiredBranch) {
grunt.task.requires('gitinfo');
if (arguments.length === 0) {
requiredBranch = 'master';
}
var currentBranch = grunt.config('gitinfo.local.branch.current.name');
if (currentBranch !== requiredBranch) {
grunt.log.error('Current branch is ' + currentBranch + ' - need to be on ' + requiredBranch);
return false;
}
});
grunt.registerTask('check-no-local-changes', 'Check there are no uncommitted changes', function() {
grunt.task.requires('gitinfo');
var status = grunt.config('gitinfo.status');
if (status != '') {
grunt.log.error('There are uncommitted local modifications.');
return false;
}
});
grunt.registerTask('check-up-to-date', 'Check code is up to date with remote repo', function() {
grunt.task.requires('gitinfo');
grunt.task.requires('shell:gitfetch');
var localSha = grunt.config('gitinfo.local.branch.current.SHA');
var originSha = grunt.config('gitinfo.origin-SHA');
if (localSha != originSha) {
grunt.log.error('There are changes in the origin repo that you don\'t have.');
return false;
}
});
// Some of these tasks are of course ommitted above, to keep the code sample focussed.
grunt.registerTask('deploy-prod', ['build','prod-deploy-checks','gittag:prod','aws_s3:prod']);
grunt.registerTask('prod-deploy-checks', ['gitinfo','check-branch:master','check-no-local-changes','shell:gitfetch','check-up-to-date']);
};
We rely on a few node modules:
- grunt-git which provides canned tasks for performing a few common git activities. We use it for tagging here.
- grunt-gitinfo which sets up a config hash with handy data from git, and allows adding custom items easily. This helps us to query the current state of things.
- grunt-gitshell which lets us run arbitrary command line tasks. We use it to git fetch (not supported by grunt-git, though we could probably have abused gitinfo to do it) and to save the current revision to file. I hope that the command I use for that is cross-platform, even to Windows, but it’s only tested on Mac so far.
Hence I ended up with the following added to package.json:
"grunt-gitinfo": "~0.1.6",
"grunt-shell": "~0.7.0"