Setting Up Continuous Deployment of an ASP.NET app with Gulp from GitHub to an Azure Web App

I just spent some time trying to figure out how to set up continuous deployment to an Azure Web App from GitHub, including running Gulp as part of the build. It seems that there are a lot blog posts and instructions on how to set up continuous deployment, but none of them seem to take into account that people actually use things like Gulp to generate client side resources during the build

The application

So to kick it off, I needed a web app to deploy. And since I just need something simple, I just opened Visual Studio and created an empty ASP.NET app. However, as I knew that I would need to add some extra stuff to my repo, I decided to add my project in a subfolder called Src. Leaving me with a folder structure like this

Repo Root
      Src
            DeploymentDemo <----- Web App Project
                  [Project files and folders]
                  Scripts
                  Styles
                  DeploymentDemo.csproj
            DeploymentDemo.sln

The functionality of the actual web application doesn’t really matter. The only important thing is the stuff that is part of the “front-end build pipeline”. In this case, that would be the Less files that are placed in the Styles directory, and the TypeScript files that are placed in the Scripts directory.

Adding Gulp

Next, I needed a Gulpfile.js to do the transpilation, bundling and minification of my Less and TypeScript. So I used npm to install the following packages

gulp
bower
gulp-less
gulp-minify-css
gulp-order
gulp-concat
gulp-rename
gulp-typescript
gulp-uglify

making sure that I added --save-dev, adding them to the package.json file so that I could restore them during the build phase.

I then used bower to install

angular
less
bootstrap

once again, adding --save-dev to that I could restore the Bower dependencies during build.

Finally, I created a gulpfile.js that looks like this

var gulp = require('gulp');
var ts = require('gulp-typescript');
var order = require("gulp-order");
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require("gulp-rename");
var less = require('gulp-less');
var minifyCSS = require('gulp-minify-css');

var jslibs = [
'bower_components/angular/angular.min.js'
];

var csslibs = [
'bower_components/bootstrap/dist/css/bootstrap.min.css'
];

gulp.task('default', ['build:scripts', 'build:styles']);

gulp.task('build:scripts', ['typescript:bundle', 'jslibs:copy'], function () {
return gulp.src('build/*.min.js')
.pipe(order([
"angular.min.js",
"deploymentdemo.min.js"
]))
.pipe(concat('deploymentdemo.min.js'))
.pipe(gulp.dest('dist/'));
});

gulp.task('typescript:bundle', function () {
return gulp.src('Scripts/**/*.ts')
.pipe(ts({
noImplicitAny: true,
target: 'ES5'
}))
.pipe(order([
"!App.js",
"App.js"
]))
.pipe(concat('deploymentdemo.js'))
.pipe(gulp.dest('build/'))
.pipe(uglify())
.pipe(rename("deploymentdemo.min.js"))
.pipe(gulp.dest('build/'));
});

gulp.task('jslibs:copy', function () {
return gulp.src(jslibs)
.pipe(gulp.dest('build/'));
});

gulp.task('build:styles', ['css:bundle', 'csslibs:copy'], function () {
return gulp.src('build/*.min.css')
.pipe(order(["bootstrap.min.css", "deploymentdemo.min.css"]))
.pipe(concat('deploymentdemo.min.css'))
.pipe(gulp.dest('dist/'));
});

gulp.task('css:bundle', function () {
return gulp.src('Styles/**/*.less')
.pipe(less())
.pipe(concat('deploymentdemo.css'))
.pipe(gulp.dest('build/'))
.pipe(minifyCSS())
.pipe(rename("deploymentdemo.min.css"))
.pipe(gulp.dest('build/'));
});

gulp.task('csslibs:copy', function () {
return gulp.src(csslibs)
.pipe(gulp.dest('build/'));
});

Yes, that is a long code snippet. The general gist is pretty simple though. The default task will transpile the TypeScript into JavaScript, minify it, concatenate it with the angular.min.js file from Bower, before adding it to the directory called dist in the root of my application. Naming it deploymentdemo.min.js. It will also transpile the Less to CSS, minify it, concatenate it with the bootstrap.min.css from Bower, before adding it to the same dist folder with the name deploymentdemo.min.css.

Running the default Gulp task produces a couple of new things, making the directory look like this

Repo Root
      Src
            DeploymentDemo
                  [Project files and folders]
                  build
                          deploymentdemo.css
                          deploymentdemo.js
                          deploymentdemo.min.css

                          deploymentdemo.min.js

                  dist
                          deploymentdemo.min.css

                          deploymentdemo.min.js

                  Scripts
                  Styles
                  DeploymentDemo.csproj
            DeploymentDemo.sln

The files placed under the build directory, is just for debugging, and are not necessarily required. But they do make it a bit easier to debug potential issues from the build…

Ok, so now I have a web app that I can use to try out my continuous deployment!

Adding Continuous Deployment

Next, I needed a GitHub repo to deploy from. So I went to GitHub and set one up. I then opened a command line at the root of my solution (Repo Root in the above directory structure), and initialized a new Git repo. However, to not get too much stuff into my repo, I added a .gitignore file that excluded all unnecessary files.

I got my .gitignore from a site called gitignore.io. On this site, I just searched for “VisualStudio” and then downloaded the .gitignore file that it generated to the root of my repo.

However, since I am also generating some files, that I don’t want to commit to Git, on the fly. I added the following 2 lines to the ignore file

build/
dist/

Other than that, the gitignore-file seems to be doing what it should. So I created my first commit to my repo, and then pushed to my new GitHub repo.

Next, I needed to set up the actual continuous deployment. This is done through the Azure portal. So opened up the portal (https://portal.azure.com/), and navigated to the Web App that I wanted to enable CD for. Under the settings for the app, there is a Continuous Deployment option that I clicked

image

After clicking this, you are asked to choose your source. Clicking the “choose source” button, gives you a list of supported source control solutions.

image

In this case, I chose GitHub (for obvious reasons). If you haven’t already set up CD from GitHub at some point before, you are asked to authenticate yourself. Once that is done, you get to choose what project/repo you want to deploy from, as well as what branch you want to use.

As soon as all of this is configured, and you click “Done”, Azure will start its first deployment. However, since it wouldn’t run the Gulp-stuff that I required, the build is a massive fail. Tthe deployment will succeed, but your app won’t work, as it doesn’t have the required CSS and JavaScript.

Modifying the build process to run Gulp

Ok, so now that I had the actual CD configured in Azure, it was time to set up the build to run the required Gulp-task. To do this, I needed to modify the build process. Luckily, this is a simple thing to do…

When doing CD from GitHub, the system doing the actual work is called Kudu. Kudu is a fairly “simple” deployment engine that can be used to deploy pretty much anything you can think of to an Azure Web App. It also happens to be very easily modified. All you need is a .deployment file in the root of your repo to tell Kudu what to do. Or rather what command to run. In this case, the command to run was going to be a CMD-file. This CMD-file will replace the entire build process. However, you don’t have to go and re-create/re-invent the whole process. You can quite easily get a baseline of the whole thing created for you using the Azure CLI.

The Azure CLI can be installed in a couple of different ways, including downloading an EXE or by using npm. I hit some bumps trying to install it through npm though, so I recommend just getting the EXE, which is available here: https://azure.microsoft.com/en-us/documentation/articles/xplat-cli-install/.

Once that is installed, you can generate a .deployment and deployment.cmd file using a command that looks like this

azure site deploymentscript -s <PathToSln> --aspWAP <PathToCsProj>

So in this case, I ran the following command in the root of my repo

azure site deploymentscript --s Src\DeploymentDemo.sln --aspWAP \Src\DeploymentDemo\DeploymentDemo.csproj

Note: It does not verify that the sln or csproj file even exists. The input parameters are just used to set some stuff in the generated deploy.cmd file.

The .deployment-file contains very little. Actually, it only contains

[config]
command = deploy.cmd

which tells Kudu that the command to run during build, is deploy.cmd. The deploy.cmd on the other hand, contains the whole build process. Luckily, I didn’t have to care too much about that, even if it is quite an interesting read. All I had to do, was to find the right place to interfere with the process, and do my thing.

Scrolling through the command file, I located the row where it did the actual build. It happened to be step number 2, and what it does, is that it builds the defined project, putting the result in %DEPLOYMENT_TEMP%. So all I had to do, was to go in and do my thing, making sure that the generated files that I wanted to deploy was added to the %DEPLOYMENT_TEMP% directory as well.

So I decided to add my stuff to the file after the error level check, right after step 2.

First off I made sure to move the working directory to the correct directory, which in my case was Src\DeploymentDemo. I did this using the command pushd, which allows me to return to the previous directory by just calling popd.

After having changed the working directory, I added code to run npm install. Luckily, npm is already installed on the Kudu agent, so that is easy to do. However, I made sure to call it in the same way that all the other code was called in the file, which meant calling :ExecuteCmd. This just makes sure that if the command fails, the error is echoed to the output automatiacally.

So the code I added so far looks like this

echo Moving to source directory
pushd "Src\DeploymentDemo"

echo Installing npm packages: Starting %TIME%
call :ExecuteCmd npm install
echo Installing npm packages: Finished %TIME%
IF !ERRORLEVEL! NEQ 0 goto error

As you can see, I also decided to add some echo calls in the code as well. This makes it a bit easier to debug any potential problems using the logs.

Once npm was done installing, it was time to run bower install. And once again, I just called it using :ExecuteCmd like this

echo Installing bower packages: Starting %TIME%
call :ExecuteCmd "bower" install
echo Installing bower packages: Finished %TIME%
IF !ERRORLEVEL! NEQ 0 goto error

And with my Bower-packages in place, it was time to run Gulp. And once again, same deal, :ExecuteCmd gulp. However, I also need to make sure that any files generated by Gulp in the dist directory was added to the %DEPLOYMENT_TEMP% directory. Which I did by just calling xcopy, copying the dist directory to the temporary deployment directory. Like this

echo Running Gulp: Starting %TIME%
call :ExecuteCmd "gulp"
echo Running Gulp: Finished %TIME%

echo Publishing dist folder files to temporary deployment location
call :ExecuteCmd "xcopy" "%DEPLOYMENT_SOURCE%\Src\DeploymentDemo\dist\*.*" "%DEPLOYMENT_TEMP%\dist" /S /Y /I
echo Done publishing dist folder files to temporary deployment location

IF !ERRORLEVEL! NEQ 0 goto error

And finally, I just “popped” back to the original directory by calling popd… So the code I added between step 2 and 3 in the file ended up being this

// "Step 2" in the original script

echo Moving to source directory
pushd "Src\DeploymentDemo"

echo Installing npm packages: Starting %TIME%
call :ExecuteCmd npm install
echo Installing npm packages: Finished %TIME%
IF !ERRORLEVEL! NEQ 0 goto error

echo Installing bower packages: Starting %TIME%
call :ExecuteCmd "bower" install
echo Installing bower packages: Finished %TIME%
IF !ERRORLEVEL! NEQ 0 goto error

echo Running Gulp: Starting %TIME%
call :ExecuteCmd "gulp"
echo Running Gulp: Finished %TIME%

echo Publishing dist folder files to temporary deployment location
call :ExecuteCmd "xcopy" "%DEPLOYMENT_SOURCE%\Src\DeploymentDemo\dist\*.*" "%DEPLOYMENT_TEMP%\dist" /S /Y /I
echo Done publishing dist folder files to temporary deployment location
IF !ERRORLEVEL! NEQ 0 goto error

echo Moving back from source directory
popd

// "Step 3" in the original script

That’s it! After having added the new files, and modified the deployment.cmd file to do what I wanted, I commited my changes and pushed them to GitHub. And as soon as I did that, Azure picked up the changes and deployed my code, including the Gulp generated files.

Simplifying development

However, there was still one thing that I wanted to solve… I wasn’t too happy about having my site using the bundled and minified JavaScript files at all times. During development, it made it hard to debug, and since VS is already transpiling my TypeScript to JavaScript on the fly, even adding source maps, why not use those files instead… That makes debugging much easier…

So In my cshtml-view, I add the following code where I previously just added an include for the minified JavaScript

@if (HttpContext.Current.IsDebuggingEnabled)
{
<script src="~/bower_components/angular/angular.js"></script>
<script src="~/bower_components/less/dist/less.min.js"></script>
<script src="~/Scripts/MainCtrl.js"></script>
<script src="~/Scripts/App.js"></script>
}
else
{
<script src="/dist/deploymentdemo.min.js"></script>
}

This code checks to see if the app is running in debug, and if it is, it uses the “raw” JavaScript files instead of the minified one. And also, it includes Angular in a non-minified version, as this adds some extra debugging capabilities. As you can see, I have also included less.min.js in the list of included JavaScript files. I will get back to why I did this in a few seconds…

Note: Yes, this is a VERY short list of files, and in a real project, this would be a PITA to work with. However, this can obviously be combined with smarter code to generate the list of files to include in a more dynamic way.

Next, I felt that having to wait for Gulp to transpile all my LESS to CSS was a bit of a hassle. Ever so often, I ended up making a change to the LESS, and then refreshing the browser too fast, not giving Gulp enough time to run. So why not let the browser do the LESS transpiling on the fly during development?

To enable this, I changed the way that the view included the CSS. In a very similar way to what I did with the JavaScript includes

@if (HttpContext.Current.IsDebuggingEnabled)
{
<link href="~/bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"/>
<link rel="stylesheet/less" type="text/css" href="~/Styles/Site.less" />
}
else
{
<link href="/dist/deploymentdemo.min.css" rel="stylesheet" />
}

So, if the app is running in debug, it includes bootstrap.min.css, and my less file. If not it just includes the deployment.min.css. However, to include the LESS file like this, you need to make sure that it is added with the rel attribute set to stylesheet/less, and that the less.min.js file is included, which I did in the previous snippet I showed.

That’s it! Running this app in debug will now give me a good way of handling my resources for development. And running it in the cloud will give the users a bundled and minified version of both the required JavaScript and CSS.

As usual on my blog, you can download the code used in this post. However, it is dependent on setting up a few things in Azure as well… So you can’t just download and try it out unfortunately. But, the code should at least give you a good starting point.

Code is available here: DeploymentDemo.zip (58.1KB)

I also want to mention that you might want to put in some more error checks and so on in there, but I assume that you know that code from blogs are generally just quick demos of different concepts, not complete solutions… And if you didn’t, now you do! Winking smile

Cheers!

PS: I have also blogged about how to upload the generated files to blob storage as part of the build. That is available here.

Pingbacks and trackbacks (2)+

Add comment