In my last article, I demonstrated how to use three tools for automating WordPress theme, plugin, and full-site development using Grunt, a taskrunner that uses JavaScript for scripting.
Creating tasks with Grunt to automate repetitive processes is actually quite easy. Not only is there a huge number of pre-existing tasks available, but its use of JavaScript also has its advantages.
In this article I will be showing you how to automate the process of releasing a plugin. This is something I recently did for the Pods Framework, after an incident where I skipped a step doing it manually, causing our admin menu to disappear in a new release.
This is also a good process to understand, because it teaches you how to put together tasks in Grunt. In addition, it covers a wide variety of tasks — from Git, to SVN, to string replacement and translation management.
By design, most Grunt task configuration options are the same, so I won’t go into too much detail on each one. Instead, I will treat each one as an opportunity to show you something different about Grunt scripting.
I love that Grunt is configured in JavaScript. I once tried to learn an automation tool that was scripted in Ruby. This was problematic for me, as I didn’t know the first thing about Ruby (or the tool I was using, for that matter). Trying to understand both at once was just too much for me. As a WordPress developer, however I already know JavaScript, so figuring out Grunt was much easier.
Please note that this article assumes you already have Grunt and Node installed in your computer. It also assumes that you have prior knowledge of Gruntfile and package.json file. If you don’t, please read the first few sections of my last article, in addition to Grunt’s getting started guide.
About grunt tasks
Grunt relies on a JavasScript file called Gruntfile.js, which configures tasks loaded from npm modules and can be used to define new tasks.
In the Grunt WordPress theme and plugin I showed you last week, there was a configured “default” task: All you have to do is type “grunt,” and Grunt will process your CSS and JavaScript. This article will teach you how to create additional tasks.
The main task is called “release,” and it runs the whole process. I actually made “release” out of other smaller tasks that I defined. This wasn’t necessary, however it did help test the process by running only small parts at a time.
The Grunt config object
Before you can register a task with Grunt in your Gruntfile.js, you will need to create an object called, “grunt.initConfig.”
Inside this object, we can set up the options for each of the separate tasks we want to be able to run.
The first thing we usually do in our config object is create the content of our package.json, a file that holds the project’s details into a property of the object. By convention, we call this property “pkg.” By doing so, we are able to use the project’s details in our scripts. This is very useful, as it allows us to dynamically set values, such as name and version.
Here is a basic config object, with the package imported to the property called pkg:
grunt.initConfig({ pkg : grunt.file.readJSON( 'package.json' ), )};
Now we can add a task to the object. For example, we can use Grunt’s file and directory copy module, grunt-contrib-copy, to copy all of the files, with the exception of a few that we may not want to include in our release. This is an excellent first step in creating a packaged release:
copy: {module.exports = function (grunt) { // Project configuration. grunt.initConfig( { pkg: grunt.file.readJSON( 'package.json' ), copy: { main: { options: { mode: true }, src: [ '**', '!node_modules/**', '!build/**', '!.git/**', '!Gruntfile.js', '!package.json', '!.gitignore', '!.gitmodules', '!composer.lock' ], dest: 'build/' } } }); };
If you look at the “src” argument you will see that the first line of the array is “**” . This says “give me all the files.” We don’t want all of these files in the final version that gets pushed to SVN and made into a zip file — however it’s better (and easier) to start with everything and exclude what we do not want than to manually list what we do want.
All of the other lines in that array start with “!” which tells Grunt to exclude those particular files. As you can see, we are telling it not to copy certain files, like our .git directory and composer.lock file.
Including modules and registering tasks
The example in the previous section is missing two important parts involved in setting up a Grunt file: The first, and most important, omission is that I did not include the actual Grunt module that powers the “copy” task. The second is that I did not set up my default task, the task that runs when you type “grunt” in the terminal.
Including the module is actually quite simple. Merely add this line of code after the config object, and before the end of the object that makes up the Grunt file:
grunt.loadNpmTasks( 'grunt-contrib-copy' );
For this to work, we need to have the module ‘grunt-contrib-copy’ and its dependencies in our node_modules folder. To make this process repeatable, we want to list it in our package.json. We can do both with one command in the terminal:
npm install grunt-contrib-copy --save-dev
Once you’ve done that, you can run this task with:
grunt copy
In this tutorial, we are building a custom-defined task called “release,” which is largely comprised over other tasks. This means we will register a new task and add other tasks to it. Let’s start our register task with the copy task.
Custom tasks are defined with the registerTask method of the Grunt object. Here is our updated Grunt file with both the module and the “register” task loaded:
copy: {module.exports = function (grunt) { // Project configuration. grunt.initConfig( { pkg: grunt.file.readJSON( 'package.json' ), copy: { main: { options: { mode: true }, src: [ '**', '!node_modules/**', '!build/**', '!.git/**', '!Gruntfile.js', '!package.json', '!.gitignore', '!.gitmodules', '!composer.lock' ], dest: 'build/' } } }); grunt.loadNpmTasks( 'grunt-contrib-copy' ); grunt.registerTask( 'release', [ 'copy' ] ); };
Managing WordPress translations via GlotPress
GlotPress is a UI tool for managing translations in WordPress. Like many open-source projects in the WordPress space, Pods uses wp-translate.org — a free implementation of GlotPress — to manage translations. Downloading all of the translation files manually from GlotPress, changing their names, and making sure they are in the right place is time consuming.
Instead, we can automate the process of pulling all of the translation files using glotpress-grunt. This is actually very simple. Like most Grunt tasks, this one has an array of options. We will tell the task what the URL to the GlotPress install is, as well as what the slug and text domain is for our plugin:
module.exports = function (grunt) { // Project configuration. grunt.initConfig( { pkg: grunt.file.readJSON( 'package.json' ), glotpress_download: { core: { options: { domainPath: 'languages', url: 'http://wp-translate.org', slug: 'pods', textdomain: 'pods' } } } }); grunt.loadNpmTasks( 'grunt-glotpress' ); }
Updating version numbers with Grunt
One of the most challenging steps in updating a WordPress plugin can be manually updating the version number. This is because the version is listed in at least two places — the stable tag in readme.txt and the version number in the plugin number. In most plugins, it is also defined in a constant and may be listed in the readme.md.
Because the version number is listed in so many different places, it can be easy to miss one. That’s why, at Pods, we have a Grunt task for this.
The task uses Grunt’s replace text module, and updates the version number based on what is set in package.json for the property version. It was a tricky thing to do, as we needed to avoid changing other uses of the current version, such as any @since documentation tags.
This required some regex, which was written by James Golovich, a Pods contributor.
module.exports = function (grunt) { // Project configuration. grunt.initConfig( { pkg: grunt.file.readJSON( 'package.json' ), replace: { reamde_md: { src: [ 'README.md' ], overwrite: true, replacements: [{ from: /~Current Version:\s*(.*)~/, to: "~Current Version: <%= pkg.version %>~" }, { from: /Latest Stable Release:\s*\[(.*)\]\s*\(https:\/\/github.com\/pods-framework\/pods\/releases\/tag\/(.*)\s*\)/, to: "Latest Stable Release: [<%= pkg.git_tag %>](https://github.com/pods-framework/pods/releases/tag/<%= pkg.git_tag %>)" }] }, reamde_txt: { src: [ 'readme.txt' ], overwrite: true, replacements: [{ from: /Stable tag: (.*)/, to: "Stable tag: <%= pkg.version %>" }] }, init_php: { src: [ 'init.php' ], overwrite: true, replacements: [{ from: /Version:\s*(.*)/, to: "Version: <%= pkg.version %>" }, { from: /define\(\s*'PODS_VERSION',\s*'(.*)'\s*\);/, to: "define( 'PODS_VERSION', '<%= pkg.version %>' );" }] } }, }); };
In the above example, you will notice that there are three separate sub-tasks in the “replace” task — one for each file. We can do this with most Grunt modules. Since there are sub-tasks, grunt replace would run all three, however grunt:readme_txt would only run the sub-task “readme_txt.”
For each of the replacements, be sure to adjust the “src” value to the correct file name in your plugin. Also, be sure that the “to” and “from” match your package configuration. For example, our main plugin file is “init.php,” which is the “src” argument for the task replace:init_php. In that task there are two replacements: The first updates the “Version:” tag in the plugin header. The second, changes the value of the constant “PODS_VERSION,” which, if you have a similar constant, you will want to use, but with your constant’s name instead of ours.
The same logic carries over to using grunt.registerTask(). This is very useful when you want to run different subtasks as different parts of the process.
Automating version control
In order to release a plugin on WordPress.org you must manage your development using SVN. This can be problematic, as many developers are accustomed to using Git. Most plugin developers just push one big commit that includes all of the changes since the last release to SVN.
When added to the task of updating version numbers, committing that last change to Git, and then moving the files into the SVN repo and tagging them, it turns into a lot of moving pieces. Rather than manually carrying this out, we can automate it using Git.
The first step occurs in Git. You should stage the files that were updated by the version number updating. Then, those changes will need to be committed, a tag will need to be applied, and all of those changes will need to be pushed to Github. There is an excellent module for managing Git that can handle all of these steps.
Here is a the Gruntfile.js with the Git tag, commit, and push:
module.exports = function (grunt) { // Project configuration. grunt.initConfig( { pkg: grunt.file.readJSON( 'package.json' ), gittag: { addtag: { options: { tag: '<%= pkg.version %>', message: 'Version <%= pkg.version %>' } } }, gitcommit: { commit: { options: { message: 'Version <%= pkg.version %>', noVerify: true, noStatus: false, allowEmpty: true }, files: { src: [ 'README.md', 'readme.txt', 'init.php', 'package.json', 'languages/**' ] } } }, gitpush: { push: { options: { tags: true, remote: 'origin', branch: 'master' } } } }); grunt.loadNpmTasks( 'grunt-git' ); };
As I said earlier, since the contents of package.json are now in the property pkg, we can use information from it. We can use this property in a string, using “<%= key %>” notation. For example, we use Version <%= pkg.version %> to form a commit message that includes the version number.
Adding the SVN steps is particularly important, because SVN tags are not markers in a branch, like Git tags, but rather they are directories that have been separated from the trunk.
As a result, once we checkout the SVN repo using Grunt, we will need to use the module grunt-copy to copy the files we want to push to both the SVN repo’s trunk and to a subdirectory of “tags,” which have been named for the current version.
In order to do all of this, we will check out the SVN repo, then refactor the copy task I demonstrated earlier into two tasks — one for the trunk and one for the tag:
module.exports = function (grunt) { // Project configuration. grunt.initConfig( { pkg: grunt.file.readJSON( 'package.json' ), copy: { svn_trunk: { options : { mode :true }, src: [ '**', '!node_modules/**', '!build/**', '!.git/**', '!Gruntfile.js', '!package.json', '!.gitignore', '!.gitmodules', '!composer.lock' ], dest: 'build/<%= pkg.name %>/trunk/' }, svn_tag: { options : { mode :true }, src: [ '**', '!node_modules/**', '!build/**', '!.git/**', '!Gruntfile.js', '!package.json', '!.gitignore', '!.gitmodules', '!composer.lock' ], dest: 'build/<%= pkg.name %>/tags/<%= pkg.version %>/' } }, svn_checkout: { make_local: { repos: [ { path: [ 'build' ], repo: 'http://plugins.svn.wordpress.org/pods' } ] } }, push_svn: { options: { remove: true }, main: { src: 'release/<%= pkg.name %>', dest: 'http://plugins.svn.wordpress.org/pods', tmp: 'build/make_svn' } }, clean: { post_build: [ 'build' ] } }); //load modules grunt.loadNpmTasks( 'grunt-contrib-copy' ); grunt.loadNpmTasks( 'grunt-contrib-clean' ); grunt.loadNpmTasks( 'grunt-svn-checkout' ); grunt.loadNpmTasks( 'grunt-push-svn' ); };
If you look at the arguments here, you will see that SVN is checked out into the directory build, and the temporary directory needed for the “push_svn” task is located there. At the end of the config, I added an extra task called “clean.” Clean removes files and directories. It would be really bad to commit your SVN repo into your Git repo, so we use “clean” to remove that build folder when we are done with it.
Pulling it all together and defining subtasks
I’ve been posting each part of the process separately. That way it’s easier to read, and you can more easily pull out the parts you need.
Now I want to show you the whole thing — but first we need to update the task “release” with all of the tasks, in the right order.
You could register one task, and do everything with it. Personally, I prefer to register multiple tasks for related steps, and then have our task called “release” do those tasks. I like this, as it allows me to easily run different parts of the process at a time. It also helps with debugging. Here is how I put the tasks together:
//release tasks grunt.registerTask( 'version_number', [ 'replace:reamde_md', 'replace:reamde_txt', 'replace:init_php' ] ); grunt.registerTask( 'pre_vcs', [ 'version_number', 'glotpress_download' ] ); grunt.registerTask( 'do_svn', [ 'svn_checkout', 'copy:svn_trunk', 'copy:svn_tag', 'push_svn' ] ); grunt.registerTask( 'do_git', [ 'gitcommit', 'gittag', 'gitpush' ] ); grunt.registerTask( 'release', [ 'pre_vcs', 'do_svn', 'do_git', 'clean:post_build' ] );
If you want, you can see the whole thing put together here.
Other things you can do
There are a lot of other things that you might want to work into this process. For example, do you want to check out all of your Composer dependencies? There’s a module for that. Want to create a zipped version of your plugin? No problem, there’s a module for that, too.
In this tutorial I showed you how to manage plugin updates to WordPress.org’s plugin directory with Grunt, and demonstrated some extra tasks you may like. Many of the parts of this process could be applied to other problems that you might want to solve with Grunt.
6 Comments