Building to Github Pages with Grunt
A while ago I created a bookmarklet to anonymize Facebook for screenshots, the Afonigizer. To distribute it, I chose to use Github Pages, Github's free hosting service. To automate the process of getting updates to my bookmarklet from a javascipt file in my repository to a page on github.io
, I used Grunt. Besides building and distributing distributing bookmarklets, I am sure there are other reasons to build to github pages (or another branch on your repo), so I'm sharing my workflow here.
The following example assumes you are familiar with bookmarklets, Github Pages, and that you've used Grunt. We'll use a modified version of my bookmarklet build as an example.
So, we have our bookmarklet written, linted, minified, and committed to the master
branch, and we're ready to publish it to the web. From a high level, this means checking out the gh-pages
branch, rebasing onto master to get the latest javascript, interpolating the javascript file into the template, committing the new index.html
file, and checking out master
again to wrap up. Switching branches and rebasing are peculiar tasks for a build, but it can be done (even if it shouldn't be) and the following Gruntfile snippet explains how.
Prerequisites and setup
We need a minified javascript file:
(function(){var a="@",b="_",c="sequoia";alert(a+b+c);})();
And a template file to serve the bookmarklet:
<!-- filename: index.html.tpl -->
<html><body>
<a href='javascript:void(<%= marklet %>);'>Bookmarklet!</a>
</body></html>
The Gruntfile
abridged; see full version here
//we'll need `fs` to read the bookmarklet file
fs = require('fs');
module.exports = function(grunt) {
// Project configuration.
grunt.initConfig({
/* ... */
gitcheckout: {
//note that (non-"string") object keys cannot contain hyphens in javascript
ghPages : { options : { branch : 'gh-pages' } },
master : { options : { branch : 'master' } }
},
gitcommit: {
bookmarkletUpdate : {
//add <config:pkg.version> or something else here
//for a more meaningful commit message
options : { message : 'updating marklet' },
files : { src: ['index.html'] }
}
},
gitrebase: {
master : { options : { branch : 'master' } }
},
template : {
'bookmarkletPage' : {
options : {
data : function(){
return {
//the only "data" are the contents of the javascript file
marklet : fs.readFileSync('dist/afonigizer.min.js','ascii').trim()
};
}
},
files : {
'index.html' : ['index.html.tpl']
}
}
}
});
/* ... */
grunt.loadNpmTasks('grunt-git');
grunt.loadNpmTasks('grunt-template');
//git rebase will not work if there are uncommitted changes,
//so we check for this before getting started
grunt.registerTask('assertNoUncommittedChanges', function(){
var done = this.async();
grunt.util.spawn({
cmd: "git",
args: ["diff", "--quiet"]
}, function (err, result, code) {
if(code === 1){
grunt.fail.fatal('There are uncommitted changes. Commit or stash before continuing\n');
}
if(code <= 1){ err = null; } //codes 0 & 1 are expected, not errors
done(!err);
});
});
//this task is a wrapper around the gitcommit task which
//checks for updates before attempting to commit.
//Without this check, an attempt to commit with no changes will fail
//and exit the whole task. I didn't feel this state (no changes) should
//break the build process, so this wrapper task just warns & continues.
grunt.registerTask('commitIfChanged', function(){
var done = this.async();
grunt.util.spawn({
cmd: "git",
args: ["diff", "--quiet", //just exists with 1 or 0 (change, no change)
'--', grunt.config.data.gitcommit.bookmarkletUpdate.files.src]
}, function (err, result, code) {
//only attempt to commit if git diff picks something up
if(code === 1){
grunt.log.ok('committing new index.html...');
grunt.task.run('gitcommit:bookmarkletUpdate');
}else{
grunt.log.warn('no changes to index.html detected...');
}
if(code <= 1){ err = null; } //code 0,1 => no error
done(!err);
});
});
grunt.registerTask('bookmarklet', 'build the bookmarklet on the gh-pages branch',
[ 'assertNoUncommittedChanges', //exit if working directory's not clean
'gitcheckout:ghPages', //checkout gh-pages branch
'gitrebase:master', //rebase for new changes
'template:bookmarkletPage', //(whatever your desired gh-pages update is)
'commitIfChanged', //commit if changed, otherwise warn & continue
'gitcheckout:master' //finish on the master branch
]
);
/* ... */
};
That's it! 😊
Additional Notes
Grunt tasks used here were grunt-template
and grunt-git
(the latter of which I contributed the rebase task to, for the purpose of this build).
Why use rebase?: We're using rebase
here instead of merge
because it keeps all the gh-pages
changes at the tip of the gh-pages
branch, which makes the changes on that branch linear and easy to read. The drawback is that it requires --force
every time you push your gh-pages branch, but it allows you to easily roll back your gh-pages
stuff (roll back to the last version of your index.html.tpl
e.g.) and this branch is never shared or merged back into master, so it seems a worthwhile trade.
Is it realy a good idea to be switching branches, rebasing, etc. as part of an automated build? Probably not. :) But it's very useful in this case!
Please let me know if you found this post useful or if you have questions or feedback.
📝 Comments? Please email them to sequoiam (at) protonmail.com