Creating and releasing Umbraco packages for multiple platforms using Grunt

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

Creating and releasing Umbraco packages for multiple platforms using Grunt

If you ever tried creating a package for Umbraco, there might be quite a few steps involved when pushing a new release, taking up time you could use for development instead.

When I create packages for Umbraco, I usually release my package via three different platforms: a NuGet package to be installed via Visual Studio, a ZIP file of the relevant files uploaded to GitHub, and an actual Umbraco package uploaded to Our Umbraco. Targeting three different platforms then obviously involves even more steps.

So can we do something to automate this? YES!

The power of Grunt

There are multiple ways of obtaining the same result - eg. some build servers supports creating a NuGet package for each build.

But since we at Skybrud.dk already are using Grunt for our frontend stuff, I've opted for using Grunt for this as well.

Getting started
For creating a release for each platform, we'll need some information about our package. We can declare this in Gruntfile.js, but to keep things a bit separate, I've gone with appending this to package.json instead.

So for our Umbraco GridData package, the package.json looks like this:

{
  "name": "Skybrud.Umbraco.GridData",
  "url": "https://github.com/skybrud/Skybrud.Umbraco.GridData",
  "license": {
    "name": "MIT",
    "url": "https://github.com/skybrud/Skybrud.Umbraco.GridData/blob/master/LICENSE.md"
  },
  "author": {
    "name": "Anders Bjerner, René Pjengaard",
    "url": "http://www.skybrud.dk/"
  },
  "readme": "Skybrud.Umbraco.GridData is a small package with a strongly typed model for the new grid in Umbraco 7.2.\n\nThe package makes it easy to use the model in your MVC views, master pages or even in your custom logic - eg. to index the grid data in Examine for better searches.",
  "devDependencies": {
    "grunt": "~0.4.5",
    "grunt-contrib-copy": "~0.4.1",
    "grunt-nuget": "~0.1.4",
    "grunt-zip": "~0.17.0",
    "grunt-umbraco-package": "1.0.0",
    "grunt-contrib-clean": "^0.6.0"
  }
}

Since the the release for each platform will be based on a Visual Studio project, it would make sense to get as much information from the DLL of that project. Since I haven't found a way to do this directly using either npm or Grunt, I have created a small tool called UpdateAssemblyInfoJson that generates a JSON file with information about the DLL.

The .csproj file of our Grid project is located at /src/Skybrud.Umbraco.GridData/Skybrud.Umbraco.GridData.csproj. To generate the mentioned JSON file, I've added a post build event to the Visual Studio project:

"$(SolutionDir)..\.skybrud\UpdateAssemblyInfoJson.exe" "$(ProjectPath)" "$(TargetPath)"

So when a build completes, the information will be saved to /src/Skybrud.Umbraco.GridData/Properties/AssemblyInfo.json, and contain something like this:

{
  "title": "Skybrud.Umbraco.GridData",
  "description": "Skybrud.Umbraco.GridData is a small package with a strongly typed model for the grid in Umbraco 7.2 and above.",
  "company": "Skybrud.dk",
  "product": "Skybrud.Umbraco.GridData",
  "copyright": "Copyright © 2015",
  "version": "1.5.0.0",
  "informationalVersion": "1.5.0",
  "fileVersion": "0.0.347.2"
}

Setting up the Grunt file
With the JSON file in place, we can now read it from Gruntfile.js. The starting point of the Gruntfile.js could look like:

module.exports = function(grunt) {

    // Load the package JSON file
    var pkg = grunt.file.readJSON('package.json');

    // get the root path of the project
    var projectRoot = 'src/' + pkg.name + '/';

    // Load information about the assembly
    var assembly = grunt.file.readJSON(projectRoot + 'Properties/AssemblyInfo.json');

    // Get the version of the package
    var version = assembly.informationalVersion ? assembly.informationalVersion : assembly.version;

    grunt.initConfig({
        pkg: pkg
    });

    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-nuget');
    grunt.loadNpmTasks('grunt-zip');
    grunt.loadNpmTasks('grunt-umbraco-package');

    grunt.registerTask('dev', []);
    grunt.registerTask('default', ['dev']);

};

Determining relevant files for the packages
For the GitHub ZIP file and the Umbraco ZIP package, I setup /files as the source directory, and copy relevant files here using the grunt-contrib-copy task. To make sure the directory is empty before copying, I'm also using the grunt-contrib-clean task.

The updated Gruntfile.js now looks like this:

module.exports = function(grunt) {

    // Load the package JSON file
    var pkg = grunt.file.readJSON('package.json');

    // get the root path of the project
    var projectRoot = 'src/' + pkg.name + '/';

    // Load information about the assembly
    var assembly = grunt.file.readJSON(projectRoot + 'Properties/AssemblyInfo.json');

    // Get the version of the package
    var version = assembly.informationalVersion ? assembly.informationalVersion : assembly.version;

    grunt.initConfig({
        pkg: pkg,
        clean: {
            files: [
                'files/**/*.*'
            ]
        },
        copy: {
            release: {
                files: [
                    {
                        expand: true,
                        cwd: projectRoot + 'bin/Release/',
                        src: [
                            pkg.name + '.dll',
                            pkg.name + '.xml'
                        ],
                        dest: 'files/bin/'
                    }
                ]
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-nuget');
    grunt.loadNpmTasks('grunt-zip');
    grunt.loadNpmTasks('grunt-umbraco-package');

    grunt.registerTask('dev', ['clean', 'copy']);
    grunt.registerTask('default', ['dev']);

};

NuGet
For creating the NuGet package, I'm using the grunt-nuget package. It adds tasks for a number of purposes, but most importantly the nugetpack task.

The task is then based on /src/Skybrud.Umbraco.GridData/Skybrud.Umbraco.GridData.csproj. When the pack task is based on a .csproj file, NuGet will automatically look for a .nuspec file with the same basename. So in this example, NuGet will find /src/Skybrud.Umbraco.GridData/Skybrud.Umbraco.GridData.nuspec, which looks like:

<?xml version="1.0"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>$version$</version>
    <title>$title$</title>
    <authors>Anders Bjerner, René Pjengaard</authors>
    <owners>Anders Bjerner, René Pjengaard</owners>
    <licenseUrl>https://github.com/skybrud/Skybrud.Umbraco.GridData/blob/master/LICENSE.md</licenseUrl>
    <projectUrl>https://github.com/skybrud/Skybrud.Umbraco.GridData</projectUrl>
    <iconUrl>http://www.skybrud.dk/img/5431sk/icon/favicon.ico</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>$description$</description>
    <tags>Umbraco, Grid, JSON</tags>
  </metadata>
</package>

The values of id, version and title are automatically populated based on the DLL of the project.

The configuration of the task will look like:

nugetpack: {
    release: {
        src: 'src/' + pkg.name + '/' + pkg.name + '.csproj',
        dest: 'releases/nuget/'
    }
}

GitHub
For creating a ZIP archive to be uploaded to GitHub, I'm using the grunt-zip package, which gives me the zip task. The task can be set up like:

zip: {
    release: {
        cwd: 'files/',
        src: [
            'files/**/*.*'
        ],
        dest: 'releases/github/' + pkg.name + '.v' + version + '.zip'
    }
}

If the version of the DLL file is 1.5.0, running the above task will create a new archive at /releases/github/Skybrud.Umbraco.GridData.v1.5.0.zip.

Umbraco package
For creating the Umbraco package, I'm using grunt-umbraco-package by @tomnfulton.

When setting up the task with the configuration shown below, the task will automatically generate the manifest, and add the files that we earlier copied to /files. The ZIP archive is then saved to /releases/umbraco/Skybrud.Umbraco.GridData.v1.5.0.zip.

umbracoPackage: {
    release: {
        src: 'files/',
        dest: 'releases/umbraco',
        options: {
            name: pkg.name,
            version: version,
            url: pkg.url,
            license: pkg.license.name,
            licenseUrl: pkg.license.url,
            author: pkg.author.name,
            authorUrl: pkg.author.url,
            readme: pkg.readme,
            outputName: pkg.name + '.v' + version + '.zip'
        }
    }
}

Finale

To summarize the various tasks, the final Gruntfile.js will now look like:

module.exports = function(grunt) {

    // Load the package JSON file
    var pkg = grunt.file.readJSON('package.json');

    // get the root path of the project
    var projectRoot = 'src/' + pkg.name + '/';

    // Load information about the assembly
    var assembly = grunt.file.readJSON(projectRoot + 'Properties/AssemblyInfo.json');

    // Get the version of the package
    var version = assembly.informationalVersion ? assembly.informationalVersion : assembly.version;

    grunt.initConfig({
        pkg: pkg,
        clean: {
            files: [
                'files/**/*.*'
            ]
        },
        copy: {
            release: {
                files: [
                    {
                        expand: true,
                        cwd: projectRoot + 'bin/Release/',
                        src: [
                            pkg.name + '.dll',
                            pkg.name + '.xml'
                        ],
                        dest: 'files/bin/'
                    }
                ]
            }
        },
        nugetpack: {
            release: {
                src: 'src/' + pkg.name + '/' + pkg.name + '.csproj',
                dest: 'releases/nuget/'
            }
        },
        zip: {
            release: {
                cwd: 'files/',
                src: [
                    'files/**/*.*'
                ],
                dest: 'releases/github/' + pkg.name + '.v' + version + '.zip'
            }
        },
        umbracoPackage: {
            release: {
                src: 'files/',
                dest: 'releases/umbraco',
                options: {
                    name: pkg.name,
                    version: version,
                    url: pkg.url,
                    license: pkg.license.name,
                    licenseUrl: pkg.license.url,
                    author: pkg.author.name,
                    authorUrl: pkg.author.url,
                    readme: pkg.readme,
                    outputName: pkg.name + '.v' + version + '.zip'
                }
            }
        }
    });

    grunt.loadNpmTasks('grunt-contrib-clean');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-nuget');
    grunt.loadNpmTasks('grunt-zip');
    grunt.loadNpmTasks('grunt-umbraco-package');

    grunt.registerTask('dev', ['clean', 'copy', 'nugetpack', 'zip', 'umbracoPackage']);
    grunt.registerTask('default', ['dev']);

};

In my example for version 1.5.0, the following files will be generated:

  • /releases/nuget/Skybrud.Umbraco.GridData.1.5.0.nupkg
  • /releases/github/Skybrud.Umbraco.GridData.v1.5.0.zip
  • /releases/umbraco/Skybrud.Umbraco.GridData.v1.5.0.zip

I have then manually uploaded the files to NuGet, GitHub and Our Umbraco, which is fine for me.

But you could take it further to automatically deploy the files as well. grunt-nuget for instance also has a nugetpush task.

Anders Bjerner

Anders is on Twitter as