Using Grunt to automate theme releases

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:

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 the Version: 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:

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.

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.

Posted in on

8 responses

  1. 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.

    {
    compress: {
    			release: {
    				options: {
    					archive: '../../releases//-.zip'
    				},
    				src: ['**',
    					'!node_modules/**',
    					'!Gruntfile.js',
    					'!package.json',
    					'!composer.*',
    					'!bower.*',
    					'!.*',
    					'!sass/**',
    					'!tests/**',
    					'!phpunit.xml',
    					'!nbproject/**',
    					'!config.rb'
    				],
    				dest: '/'
    			}
    		},
    }
    
    1. 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 into style.min.css and add that only in the build folder before the compress task runs. So in that case I think the copy and clean tasks are necessary.

    2. 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!

  2. I’m looking into doing a similar setup. Has the process changed much in the past couple years…since you originally posted this?

    1. 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)

Leave a Reply