Git Auto Created Release Branches

A little over 5 years I started working for a small company which had created a proprietary deployment system for updates to its website. The company had and maintained 3 servers (Development, Staging, and Live), and all the developers coded directly on these machines.

When you only have 2 or 3 people coding, you can get away with this model with out too much headache. When you start adding more people into the mix, this becomes a very large problem very quickly. So finally six months ago, I convinced the company that we needed to move to a proper Source Control system. This is when I started playing with GitHub and GitLab, among other systems. Because I had the most experience with working with these kinds of systems, I was somewhat put in charge of developing these systems and workflows...

Yes, that's right... I am Psudo-DevOps of the company!

Now I have git installed on all our developers computers, and have them interfacing with GitLab, and have GitLad CI handling my deployments to various servers. Not quiet the setup I want, but its a step in the right direction.

My immediate challenge is now getting a team very much not used to working with standards to start working with standards. The best way I can think to do this is with Task Automation. This is where Grunt comes in.

Grunt JS Logo

if you have no idea what Grunt and / or Gulp are, they are task automation platforms written in Node, and you really should check them out. Even if all you do is play with a small website.

The Proposed Workflow

Consider the previous configuration: Dev, Staging, and Live.

I remove the Dev server from the workflow, its now redundant as I have vagrant running a local environment for each developer on there own workstations. This leaves me with the Staging and Live servers. At this point, the Staging Server becomes my Beta Testing or Quality Assurance (QA) Server, and Live is what it always has been; the Production Server.

Now that I have developers working with git, It need to control the work being done somehow, or I am going to have a lot of branches trying to merge into the production branch all the time. This is when I introduce Semantic Versioning. This allows me to control the work being done, and when work gets deployed to QA and Production.

This now brings me back to standards and task automation. I want my developers to work from and merge to a "Release Branch". What is that? Lets say I want to start a new project. I create a simple site, and with semantic versioning, I tag it at v0.0.0. My next release contains a bunch of features, but I only want one deployment to my servers, so I create a Release Branch called v0.1.0 which is branched off v0.0.0. All my developers branch of my Release Branch, and Merge there working code back into that Release. When my Release is completed, I merge it into QA for testing and requirements assessment. Assuming those passed, I complete the merge to master, and deploy the code to Production. I then tag master at that merge commit as v0.1.0.

The part I am working on automating is that deployment and new release branch processes.

How Grunt Helps

To help maintain the standards I want to put in place, I need task automation to take care of a bunch of steps for me. In this case, I grunt to "checkout master", find the version number, create a new branch based on the next version number, goto that branch, edit the package and "read me" files, and commit those changes to the branch.

I am not asking for a lot am I?

so I have created the following grunt task as a preliminary prototype of that task. run with the command grunt createReleaseBranch:patch.

Terminal Screenshot

// Gruntfile.js
module.exports = function(grunt) {  
    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),

        jshint: {
            gruntfile: {
                src: ['gruntfile.js']
            }
        }, // close jshint

        gitcheckout: {
            master: {
                options: {
                    branch: "master"
                }
            },
            newVersionBranch: {
                options: {
                    branch: "v<%= pkg.version %>",
                    create: true
                }
            }
        }, // close gitcheckout

        gitcommit: {
            newReleaseBranch: {
                options: {
                    message: 'Updated package.json and Readme File',
                    noVerify: false,
                    noStatus: false
                },
                files: {
                    src: ['package.json','ReadMe.md']
                }
            }
        }, // close gitcommit

        watch: {
            gruntfile: {
                files: ['gruntfile.js'],
                tasks: ['jshint:gruntfile'],
                options: {
                    spawn: false
                }
            }
        } // close watch
    }); // close grunt.initConfig

    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-git');
    grunt.loadNpmTasks('grunt-contrib-jshint');

    grunt.registerTask('createReleaseBranch','a task to create a release branch', function(iterum) {
        // Settings
        var _acceptedIterumValues = ["major","minor","patch"]; // only allow these values to increment
        var _readMeFileName = "ReadMe.md";

        // check that a valid "iterum" is passed,
        if (!iterum || (_acceptedIterumValues.indexOf(iterum) == -1)) {
            // invalid argument, print out valid options
            grunt.log.error('Oops! no value found');
            grunt.log.writeln('Accepted Values:');
            for (var i=0; i<_acceptedIterumValues.length; i++) {
                grunt.log.writeln('grunt createReleaseBranch:' + _acceptedIterumValues[i]);
            }
            return false;
        } // close if iterum

        // git checkout master
        grunt.task.run('gitcheckout:master');

        // Get the current Version Number
        var pkg = grunt.file.readJSON('package.json');
        var newVersion = pkg.version;
        grunt.log.writeln('Current Release Version: v' + newVersion);

        // Increment the current version number based on the iterum value
        var _newVersionSplit = newVersion.split('.');
        if (iterum == _acceptedIterumValues[0]) {
            // if major...
            _newVersionSplit[0] = parseInt(_newVersionSplit[0])+1;
            _newVersionSplit[1] = 0;
            _newVersionSplit[2] = 0;
        } else if (iterum == _acceptedIterumValues[1]) {
            // if minor...
            _newVersionSplit[1] = parseInt(_newVersionSplit[1])+1;
            _newVersionSplit[2] = 0;
        } else {
            // if patch...
            _newVersionSplit[2] = parseInt(_newVersionSplit[2])+1;
        }
        pkg.version = _newVersionSplit.join(".");
        grunt.log.writeln('Creating Release Branch For: v' + pkg.version);

        // Create new branch and check it out
        // git checkout -b v[pkg.version]
        grunt.config.set('gitcheckout.newVersionBranch.options.branch', "v" + pkg.version);
        grunt.task.run('gitcheckout:newVersionBranch');
        grunt.log.writeln('git checkout -b v' + pkg.version);

        // Update Package.Json File with new Version Number
        grunt.log.writeln('Updating package.json');
        grunt.file.write("package.json", JSON.stringify(pkg,null,"\t"));

        // If Read Me File Exists...
        if (grunt.file.exists(_readMeFileName)) {
            grunt.log.writeln('Updating ReadMe');
            var readmeText = grunt.file.read(_readMeFileName);

            var _date = new Date();
            var _underline = Array( (pkg.version.length+2) ).join("-");

            readmeText = readmeText.replace(/(={3,}(?:\n|\r))/,"$1\nv" + pkg.version + "\n" + _underline + "\n - New " + iterum + " branch created on " + _date.toLocaleDateString() + "\n" );
            grunt.file.write(_readMeFileName,readmeText);
        } else {
            grunt.log.error('Oops! ' + _readMeFileName + ' Could Not Found!');
        } // close if grunt.file.exists

        // Commit Readme File and Package.json File to newly created branch
        grunt.task.run('gitcommit:newReleaseBranch');
    }); // close createReleaseBranch
}; // close module.exports

Next will be the task to auto tag my Releases once deployed.