I recently wrote a couple of blog posts (first post, second post) about setting up CD of an ASP.NET web app with Gulp from GitHub to an Azure Web App. However, what if we aren’t using GitHub? What if we are using Visual Studio Tem Service (former Visual Studio Online)? Well, in that case, it is a whole different ballgame. There is actually not too much that is the same at all…
The Application
The application I’ll be working with is the same on that I have been using in the previous posts. If you haven’t read them, I suggest you go back and have a look. Not necessarily reading all of it, but at least the first part of the first post, as that includes the description of the application in use.
If you don’t feel like that, the basics are: ASP.NET web app using TypeScript and LESS and Gulp for generating the transpiled bundled and minified files. The files are read from the Styles and Scripts directories, and built to a dist directory using the default task in Gulp. The source is placed in a Src directory in the root of the repo…and the application is called DeploymentDemo.
Connecting the Team Services account to the Azure Portal
The first step that you need to do, is to connect your Visual Studio Team Services account to your Azure portal… To do this, you actually have to use the “old” portal at the moment. So you have to head over to https://manage.windowsazure.com/ and locate the “Visual Studio Online” entry in the left menu, and click it.
This will give you a list of all the currently connected VSTS accounts, if you have any… Next, click “New” and then “Link To Existing”
Then fill out the required information in the form, and click “Link Account”.
Once, this is done, you can head back to the “new” portal at https://portal.azure.com/.
Setting Up Continuous Deployment
In the portal, locate the Web App that you want to enable CD for. Then go to the settings blade and select “Continuous Deployment”
In the next blade, choose the source to be “Visual Studio Online” (yes, it says Visual Studio Online, even if it is now called Visual Studio Team Services). When selecting “Team project”, you should now get a list of projects in the newly connected VSTS account.
After having selected the correct project, you need to select the repository you want to use, as well as the branch to build… In the end, you should have something that looks like this
Configuring the Build Definition
Ok, so what did all that work in the portal do? Well, it created a new build definition for you, which you can see in 2 ways. You can either go to https://www.visualstudio.com/ and log into your account then go to the “Build” tab.
The name of the deployment will be whatever your Web App is called, and then “_CD”.
Or you can see it through the Team Explorer in Visual Studio under Builds
However, if you want to modify the build definition, which I’ll need to do in this case just a bit, you have to do it through Visual Studio…
So by right-clicking the build definition and choosing “Edit Build Definition…” in Visual Studio Team Explorer, I get the following screen
In here, I can change the name of the build definition, the trigger, which defines when the build should run, as well as all other important things… Most of them are already set properly due to the fact that we created the definition through the portal, however, there is one thing that needs to be modified in the “Process” part of the definition for the next part of my deployment to work smoothly.
We need to make sure that we build the application using the correct configuration. In my case, I want to build it using “Any CPU|Release”. This makes it possible for me to do some conditional stuff during the build, based on the “Release” build “flag”.
Running Gulp as Part of the Build
Ok, so we now have a build definition that will build our application and deploy it to Azure as it should. However, it still won’t run our Gulp task… To do this, we need to modify the way that MSBuild does things, and get it to do some extra stuff. And by extra stuff, I mean run Gulp…
Note: I have already blogged about setting up this targets file before, but not really in the context of automated builds. So if you want more specific information about how it works, I suggest looking at: https://chris.59north.com/post/Integrating-a-front-end-build-pipeline-in-ASPNET-buildshttps://chris.59north.com/post/Integrating-a-front-end-build-pipeline-in-ASPNET-builds
There are a couple of ways to do this. One is to unload the project in Visual Studio and edit the csproj file. This will work fine, but it will also make your project a bit “magical”. It is pretty hard to figure out what is happening in the project, as people might not know that you have modified the project file.
Another way is to just modify the project file to include an external targets file in the build. This makes it a bit more explicit, and easier to understand. It also allows you to place the new targets file somewhere where other devs can see it.
The way I like to do it for some reason, is to create a targets file called [ProjectName].wpp.targets, in the root of the project. This will automatically be picked up by MSBuild, so you don’t need to modify the project file.
Inside the targets file, I add the following XML
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Target Name="BeforeBuild">
<Message Text="Running Front-End Build Pipeline!" Importance="high"/>
</Target>
<Target Name="RunGulp" AfterTargets="BeforeBuild" DependsOnTargets="BowerInstall">
<Message Text="Running gulp task $(Configuration)" Importance="high" />
<Exec Command="node_modules\.bin\gulp" WorkingDirectory="$(ProjectDir)" />
<OnError ExecuteTargets="DeletePackages" />
</Target>
<Target Name="BowerInstall" DependsOnTargets="NpmInstall">
<Message Text="Running bower install" Importance="high"/>
<Exec Command="node_modules\.bin\bower install" WorkingDirectory="$(ProjectDir)" />
<OnError ExecuteTargets="DeletePackages" />
</Target>
<Target Name="NpmInstall" Condition="'$(Configuration)' != 'Debug'">
<Message Text="Running npm install" Importance="high"/>
<Exec Command="npm install" WorkingDirectory="$(ProjectDir)" />
<OnError ExecuteTargets="DeletePackages" />
</Target>
<Target Name="DeletePackages" Condition="'$(Configuration)' != 'Debug'" AfterTargets="RunGulp">
<Message Text="Downloaded packages" Importance="high" />
<Exec Command="..\..\tools\delete_folder node_modules" WorkingDirectory="$(ProjectDir)\" />
<Exec Command="..\..\tools\delete_folder bower_components" WorkingDirectory="$(ProjectDir)\" />
</Target>
<Target Name="CleanGulpFiles" AfterTargets="Clean">
<Message Text="Cleaning up node files" Importance="high" />
<ItemGroup>
<GulpGenerated Include=".\dist\**\*" />
<GulpGenerated Include=".\build\**\*" />
</ItemGroup>
<Delete Files="@(GulpGenerated)" />
<RemoveDir Directories=".\dist;" />
<RemoveDir Directories=".\build;" />
</Target>
<Target Name="AddGulpFiles" BeforeTargets="CopyAllFilesToSingleFolderForPackage;CopyAllFilesToSingleFolderForMsdeploy">
<Message Text="Adding gulp-generated files to deploy" Importance="high"/>
<ItemGroup>
<CustomFilesToInclude Include=".\dist\**\*.*" />
<FilesForPackagingFromProject Include="%(CustomFilesToInclude.Identity)">
<DestinationRelativePath>.\dist\%(RecursiveDir)%(Filename)%(Extension)</DestinationRelativePath>
</FilesForPackagingFromProject>
</ItemGroup>
<OnError ExecuteTargets="DeletePackages" />
</Target>
</Project>
That is a LOT of XML to take in, I know. But it isn’t very complicated. A Target element, is something that needs to execute at some point during the build. When it executes, it based on a couple of different things…
First, by naming an element the right thing, it will be called at a certain time. In this case, I have a target named “BeforeBuild”. This will run before the project is built.
By adding a AfterTargets or BeforeTargets attribute, you can specify that a target should be run before or after a specific target. In this case for example, I have a target called RunGulp that should run after the target called “BeforeBuild” (which is built in thing).
You can also make sure that a target has been run before another target, by specifying the DependsOnTargets attribute. In this case I am using this in a few places. For example, the RunGulp target, depends on the BowerInstall target, which depends on the NpmInstall target. This means that whenever the RunGulp target is run, the BowerInstall, and NpmInstall, will be run first.
The NpmInstall target also has a Condition attribute set. This can make the execution of a target conditional. In this case, the NpmInstall target will only run if the build is not a Debug build. (Remember that we modified the build definition to make sure that we ran a release build…)
Ok, so that’s pretty much it. The new targets file will make sure that npm install and bower install is run before Gulp is run. And Gulp is run before the project is built. However, just running Gulp isn’t enough. That will just create the new files, but they won’t be added to the deployment by default. The build definition tells MSBuild to generate a web deploy package, which is deployed to the Web App. The package will only contain files that are part of the project, or created during the build by MSBuild. So the files generated by Gulp won’t be included by default. Instead, we have to manually tell MSBuild to include our newly generated files, which is what is done in that last task called AddGulpFiles. This is a target that is run before the required files are sent to the right place for packaging. In the target, I use the FilesForPackagingFromProject element to add all files in the dist directory to the deployment.
Note: Since I was adding a targets file anyway for this to work, I also included a “clean” task that removes the Gulp generated directories when doing a clean in VS… Seemed like a nice feature to have…
The last thing to note is that all targets will automatically call the target called DeletePackages if they fail. This target till use a bat-file called delete folder to delete the folders called node_modules and bower_components. The reason for this is to not leave extra stuff on the build agent if something goes wrong, as this can cause some issues in future builds… The DeletePackages is also called after the RunGulp target.
So why do I need to use a bat-file to delete the folders? Well, node has a tendency to create VERY deep folder structures, that interfere with Windows’ limitation of 260 character in a path. So we can’t just delete it like we normally would. Instead, we have to use some other tool. In my case, I have opted to use RoboCopy to do it. So the delete_folder.bat file looks like this
@echo off
if \{%1\}==\{\} @echo Syntax: delete_folder FolderPath&goto :EOF
if not exist %1 @echo Syntax: delete_folder FolderPath - %1 NOT found.&goto :EOF
setlocal
set folder=%1
set MT="%TEMP%\delete_folder_%RANDOM%"
MD %MT%
RoboCopy %MT% %folder% /MIR
RD /S /Q %MT%
RD /S /Q %folder%
endlocal
That is actually all there is to it! This will force MSBuild to run the Gulp stuff for us as a part of the build, and everything should be sweet! As for including the actual resources in the page, I once again suggest having a look at my previous posts. They include all the information you need about how to set it up to work well for both local development and for production.
I have as usual created a sample project that you can have a look at. However, in this case, you still need to set up CD in Azure and so on, but I thought it might still be nice to have a sample to look at. So here it is: XamlDeploymentDemo.zip (87.5KB)
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.