Every time I create a new version of one of my themes, there’s a lot of steps I’m going through. I have to update version numbers, minifying CSS/JS files, commit the changes, tag the release, create a zip file (and make sure no hidden files are left in there). These are all mostly manual steps which can be quite time-consuming to go though, and more importantly: remember.
In my latest theme, Cycnus, I decided to automate my theme release workflow using Grunt, which is want to tell you about in this post.
I’m assuming what you’re a bit similar with Grunt. You don’t need to be an expert – I know I’m not. But with relatively little work, it now takes me only a few seconds to create a new release and have a zip-file already to upload.
There’s two Grunt tasks I use when releasing a new version:
- A release task that runs other tasks to update version numbers, commit the changes, tag the release in git, and push them to a remote repository.
grunt.registerTask( 'release', [ 'version', 'gitcommit:version', 'gittag:version', 'gitpush' ]);
- A build task that creates a build folder, and copy the needed files over to it, minifies the CSS and creates a zip-file
grunt.registerTask( 'build', [ 'clean:build', 'copy:build', 'cssmin:build', 'compress:build' ]);
I generally have more tasks such as compiling Sass but I’ve left them out of this post.
Here’s a gif of me running the build task:
Getting Started
When you create a new Grunt project there’s two files that’s important: package.json
and Gruntfile.js
. You can read the Grunt Getting Started guide to learn more about these files (and how to install Grunt if you haven’t already).
The Gruntfile
contains all the tasks and configuration.
Here’s a basic Gruntfile, already containing the release and build tasks.
module.exports = function(grunt) { // Project configuration. grunt.initConfig({ pkg: grunt.file.readJSON( 'package.json' ), // Tasks here }); // Load all grunt plugins here // [...] // Release task grunt.registerTask( 'release', [ 'version', 'gitcommit:version', 'gittag:version', 'gitpush' ]); // Build task grunt.registerTask( 'build', [ 'clean:build', 'copy:build', 'cssmin:build', 'compress:build' ]); };
grunt-version – (Homepage)
I use the version task to update the version numbers in files such as style.css and functions.php to reflect the version in package.json
.
Command to install:
npm install grunt-version --save-dev
Then add this to Gruntfile.js
to load the it:
grunt.loadNpmTasks('grunt-version');
Inside the grunt.initConfig()
we have to add a section called version:
// Bump version numbers version: { css: { options: { prefix: 'Version\\:\\s' }, src: [ 'style.css' ], }, php: { options: { prefix: '\@version\\s+' }, src: [ 'functions.php' ], } },
It contains two targets:
css
: Updates the version number in style.css. The prefix is matching theVersion: x.x.x
header.php
: Updates the version number in functions.php. In my case it’s in a PHPDocs header so the prefix matches@version: x.x.x
The command can be run from the command line using grunt version
or with a target grunt version:css
grunt-git – (Homepage)
I use this task to automatically commit the files changes by the version
task, tag the new version and push both the commit and tag to a remote repository (in my case Github).
// Commit, tag, and push the new version gitcommit: { version: { options: { message: 'New version: <%= pkg.version %>' }, files: { // Specify the files you want to commit src: ['style.css', 'package.json', 'functions.php'] } } }, gittag: { version: { options: { tag: '<%= pkg.version %>', message: 'Tagging version <%= pkg.version %>' } } }, gitpush: { version: {}, tag: { options: { tags: true } } },
The gitpush task is separated into two so the commit is pushed before the the tag.
grunt-contrib-copy – (Homepage)
When running the build task, all the needed files and folders are moved to a folder called build
. This is the folder that will be compressed to the zip-file the user will download.
// Copy to build folder copy: { build: { src: ['**', '!node_modules/**', '!Gruntfile.js', '!package.json'], dest: 'build/', }, },
The exclamation mark means that the file/folder will be excluded. I use this so the grunt files won’t end in the build folder (files such as .gitignore are excluded by default).
Tip: Add the build folder to your .gitignore file to hide it from version control.
grunt-contrib-clean – (Homepage)
Just before running the copy task, the clean task takes care of emptying the folder to make sure old files aren’t left in it.
// Clean the build folder clean: { build: { src: ['build/'] } },
grunt-contrib-cssmin – (Homepage)
In some of my themes I add a minified version of the each CSS file in a file called NAME.min.css
. I don’t want these files in version control so I’ve added it to the build process.
// Minify CSS files into NAME-OF-FILE.min.css cssmin: { build: { expand: true, src: ['*.css', '!*.min.css'], dest: 'build/', ext: '.min.css' } },
grunt-contrib-compress – (Homepage)
The last task is the compress task to create a zip-file from the build folder. It uses the name from package.json to name the zip-file and the folder inside it.
// Compress the build folder into an upload-ready zip file compress: { build: { options: { archive: 'build/<%= pkg.name %>.zip' }, cwd: 'build/', src: ['**/*'], dest: '<%= pkg.name %>/' } }
The full code
I’ve created a gist with the whole Gruntfile.js:
[This download is no longer available]
Other useful tasks
Cycnus is a quite simple theme so I don’t need more tasks than this but that doesn’t mean you might not. Here’s a short list of other tasks that can be very useful.
- grunt-contrib-imagemin to minify images
- grunt-contrib-uglify to minify JS files
- grunt-wp-i18n to generate a .pot file for translators to use when translating your theme if you use WordPress
- grunt-cssjanus to convert CSS stylesheets between left-to-right and right-to-left
If you know more, please add a comment and I’ll add it to the list.
I hope you enjoyed this post and it will help you to a better theme workflow. The release workflow is one of the things I hate about creating themes but it’s really improved it for me.
I’m using Grunt too, but haven’t used Git plugin. Looks quite promising! Thanks for sharing.
Thank you. The Git plugin is quite awesome and saves some time 🙂
Thanks for this great article. It was a good place to start. After playing with this more, I have come to a conlusion that copy and clean tasks are redundant, as you can do that with compress.
Thank you, Ismayil.
Looks like you’re right. Of course this depends on what other tasks you run.
For example, in my theme workflows I compress
style.css
intostyle.min.css
and add that only in the build folder before thecompress
task runs. So in that case I think the copy and clean tasks are necessary.If you want the theme/plugin to be wrapped in it’s folder (instead “build” naming the folder “my-theme-name”) I can’t find a solution without copy and clean modules. The current post was the solution to that – thanks a bunch for the author!
Thanks for the explanation.
I’m looking into doing a similar setup. Has the process changed much in the past couple years…since you originally posted this?
Hello Josh,
I’m not sure as I haven’t used it much since then. But I don’t think Grunt has had any major changes so most, if not all, the code should still work (maybe with some minor changes in the plugins used)