OPTEN, das einzige Umbraco-zertifizierte Unternehmen der Schweiz

Our Octopus Odyssey: Part 2. Build Script

This blog is part 2 of a series:

Part 1. The Basics
Part 3. Variables and Powershell
Part 4. Database
Part 5. web.config
Part 6. NuGet Repository
Part 7. less & CSS

After my first experiments with Octopus Deploy I was very happy how easy it was to publish the solution and create a nuget package. But I wanted to improve some small things.

Build script aims

1. The publish folder was not cleared before republishing. I preferred that it was cleared first so that I could be sure that everything in it was from the current build.

2. A nuget package was created for each web application in my solution. This was annoying because often I would just have made changes affecting one of the web applications, maybe the front end website only. And I did not want to take extra time making nuget packages for all web applications when I did not need to. I wanted a way to publish and specify for example, only create a nuget package of the frontend website.

3. Each nuget package had the same name, the name came from the build version number which was always the same. I wanted a way to chronologically order the packages. My plan was to set the build version number to include the date and time the project was published. This would be unique and be a useful way of ordering the nuget packages.

MSBuild

To achieve these aims I had to delve into the world of MSBuild. Although I have worked with Visual Studio every day for a few years now I had never understood the MSBuild language. So it was very interesting to learn the basics. There is a basic MSBuild introduction from Microsoft here: https://msdn.microsoft.com/en-us/library/ms171479(v=vs.90).aspx

The first thing which I did was to create a custom MSBuild project. It was as simple as creating a new notepad file, naming it Publish.proj and saving it to a sensible place. For me that was a new folder named BuildScripts in my solution root folder.

In this file I created a new Project element, this is the root element for an MSBuild project file. Then to this project element I set the DefaultTargets to be a new target called BuildSolution. A Target in MSBuild is like a method, used to group tasks together. So then I created the BuildSolution target and inside this target added the MSBuild task. Calling this task builds the solution and it is possible to pass in various parameters here which will be used during the build. I also created another MSBuild file for storing the variables which would be used in the build script. I named this Common.props, put it in the same directory as the Publish.proj file and imported it into the project. This meant that I could set properties for the output path, the Octopack publish url and the Octopack publish api key. Then I used these properties as parameters in my MSBuild task.

Here is the simple Common.Props file:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="Http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <ProjectDir>D:\DEVELOPMENT\GIT\SHOPTEN</ProjectDir>
        <SolutionFileName>$(ProjectDir)/SHOPTEN.sln</SolutionFileName>
        <OutputPath>$(ProjectDir)/Publish</OutputPath>
        <OctopusPushUrl>http://auratel.nugetpackages/</OctopusPushUrl>
        <OctopusApiKey>API-HTTWI6GXCHLIU6UMHJJFCRMQLQQ</OctopusApiKey>
    </PropertyGroup>
</Project>

Here is the simple Publish.proj file:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="BuildSolution"
              xmlns="Http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="BuildSolution">
        <MSBuild Projects="$(SolutionFileName)"
                       Properties="Configuration=Release;
                                         OutputPath=$(OutputPath);
                                         OctoPackPublishPackageToHttp=$(OctopusPushUrl);
                                         OctoPackPublishApiKey=$(OctopusApiKey)"/>
    </Target>
    <Import Project="Common.props"/>
</Project>

External Tool (Running the script)

To build the project using this custom MSBuild file I added a custom external tool to Visual Studio. To do this I went to Tools => External Tools => Add. For the command I put in the path to the MSBuild exe on my machine. For the arguments I entered Publish.proj (the name of my custom MSBuild script) for the Initial directory I entered the path to where the Publish.proj file could be found. I also checked the Use Output Window box, so that the Visual Studio output window would be used rather than opening a new cmd window. Then I clicked OK. Now when I selected this tool from the Tools menu my custom MSBuild script was run. This was the first step complete.

Externaltools

1. Clear publish folder before publishing

Now that the custom build script worked I could start working on the improvements. The first was to clear the publish directory before publishing. To do this I added a new Target named CleanDir and set the BuildSolution to depend on this new CleanDir target. This would mean that the CleanDir target would always be run before the BuildSolution target. To delete the directory is very simple. I called the RemoveDir task and pass in the OutputPath variable which is also used in the MSBuild task. I also added a condition to check that the directory exists before trying to remove it.

Here is the new CleanDir target:

<Target Name="CleanDir">
    <RemoveDir Directories="$(OutputPath)" Condition="Exists($(OutputPath))" />
</Target>

 2. Use Octopack to pack only one project as nuget package from a solution with multiple projects

The next improvement was to only package the web application which I specified instead of all in the solution. The trick to do this involved changing the .csproj file for each project in the solution. There is a property which Octopack checks before running. It is called (appropriately) RunOctoPack. In order to control whether a web application is packed I added a condition to this property which is different for each web application. So for the WebAdmin project I add the condition '$(OctoPackWebAdmin)' == 'true' whereas for the Web project I add the condition '$(OctoPackWeb)' == 'true'. This tip is courtesy of Paul Stovell in an answer I found on the Octopus Deploy help forum: http://help.octopusdeploy.com/discussions/questions/727-octopacking-1-project-in-a-solution-with-multiple-octopack-enabled-projects.

So here is the complete property group which I added to the WebAdmin .csproj file:

<PropertyGroup>
    <RunOctoPack Condition="'$(OctoPackWebAdmin)' == 'true'">true</RunOctoPack>
</PropertyGroup>

Once this property group was added I had to pass in this property to the build script so that it would know which project to pack. To do this I added a new property to the Common.props file named ProjectToPack. Then in my External tool I changed the Arguments to pass in this property and created a new External tool for each web application which I wanted to publish, each passing in the appropriate ProjectToPack property. For example here is the Arguments path for the Publish Web Admin external tool: Publish.proj /p:ProjectToPack=OctoPackWebAdmin and for the Publish Web tool: Publish.proj /p:ProjectToPack=OctoPackWeb. In each case I had to make sure that the value assigned to the ProjectToPack variable matched the name of the variable in the RunOctoPack condition in the .csproj file for that project. Then it was simple to set this to true in the MSBuild properties in the Publish.proj file. I did this by adding this line: $(ProjectToPack)=true;

3. Set custom assembly version numbers when building project

The final improvement was to set the build version to inclue the date and time of the build. I decided to use the following format for the build version:

MajorVersion.MinorVersion.CurrentDate.CurrentTime

There is a powerful set of extension methods to MSBuild which can be downloaded from Nuget: https://www.nuget.org/packages/MSBuild.Extension.Pack/ I installed this and imported it into the Publish.proj file:

<Import Project="$(ExtensionTasksPath)MSBuild.ExtensionPack.tasks"/> 

Here I also used a new property which I had created in Common.props named ExtensionTasksPath which I used when importing this extension. I also created two other new properties MajorVersion and MinorVersion. Then I made a new Target named GetCurrentBuildVersionNumber in a new file named Common.targets, also in the same folder as Publish.proj and Common.props, to allow for the possibility of this target being used in other places. This target got the current date and time and then set the BuildVersionNumber property.

Here is the code:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="Http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="GetCurrentBuildVersionNumber">
        <PropertyGroup>
            <CurrentDate>$([System.DateTime]::Now.ToString(yyyyMMdd))</CurrentDate>
            <CurrentTime>$([System.DateTime]::Now.ToString(HHmmss))</CurrentTime>
            <BuildVersionNumber>
                $(MajorVersion).$(MinorVersion).$(CurrentDate).$(CurrentTime)
            </BuildVersionNumber>
        </PropertyGroup>
    </Target>
</Project>

Then in the Publish.proj file I made a new target named SetVersions which the BuildSolution depended on.  In the SetVersions target I made an ItemGroup of all AssemblyInfo.cs files and then called the AssemblyInfo target from the MSBuild ExtensionPack, passing in the assembly info files and the build version number.

Here is the code from the SetVersions target:  

<Target Name="SetVersions" DependsOnTargets="GetCurrentBuildVersionNumber">
    <ItemGroup>
        <AssemblyInfoFiles Include="$(ProjectDir)\**\AssemblyInfo.cs" />
    </ItemGroup>
    <MSBuild.ExtensionPack.Framework.AssemblyInfo
         AssemblyInfoFiles="@(AssemblyInfoFiles)"
         AssemblyFileVersion="$(BuildVersionNumber)" />
</Target>

 

Then I could also pass the build version number into the MSBuild task, assigning it to the OctoPackPackageVersion property.

Summary

So by using the power of MSBuild I was able to create a build script which improved the build process in the 3 ways I wanted. MSBuild is a powerful companion to Octopus Deploy. The automation of the build process is the first step in automating the deploy process.

Complete build script

Here is the final version of the code for Common.props:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="Http://schemas.microsoft.com/developer/msbuild/2003">
    <PropertyGroup>
        <ProjectDir>D:\DEVELOPMENT\GIT\SHOPTEN</ProjectDir>
        <ExtensionTasksPath>
            $(ProjectDir)\Packages\MSBuild.Extension.Pack.1.5.0\tools\net40\
        </ExtensionTasksPath>
        <SolutionFileName>$(ProjectDir)/SHOPTEN.sln</SolutionFileName>
        <OutputPath>$(ProjectDir)/Publish</OutputPath>
        <ProjectToPack></ProjectToPack>
        <OctopusPushUrl>http://auratel.nugetpackages/</OctopusPushUrl>
        <OctopusApiKey>API-HTTWI6GXCHLIU6UMHJJFCRMQLQQ</OctopusApiKey>
        <MajorVersion>3</MajorVersion>
        <MinorVersion>0</MinorVersion>
    </PropertyGroup>
</Project>

Here is the final version of the code for Common.targets:

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="Http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="GetCurrentBuildVersionNumber">
        <PropertyGroup>
            <CurrentDate>$([System.DateTime]::Now.ToString(yyyyMMdd))</CurrentDate>
            <CurrentTime>$([System.DateTime]::Now.ToString(HHmmss))</CurrentTime>
            <BuildVersionNumber>
                $(MajorVersion).$(MinorVersion).$(CurrentDate).$(CurrentTime)
            </BuildVersionNumber>
        </PropertyGroup>
    </Target>
</Project>

Here is the final version of the code for Publish.proj:

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="BuildSolution"
xmlns="Http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="CleanDir">
        <RemoveDir Directories="$(OutputPath)" Condition="Exists($(OutputPath))" />
    </Target>

    <Target Name="BuildSolution" DependsOnTargets="CleanDir;SetVersions;CompileLess">
        <MSBuild Projects="$(SolutionFileName)"
                        Properties="Configuration=Release;
                        OutputPath=$(OutputPath);
                        $(ProjectToPack)=true;
                        OctoPackPackageVersion=$(BuildVersionNumber);
                        OctoPackPublishPackageToHttp=$(OctopusPushUrl);
                        OctoPackPublishApiKey=$(OctopusApiKey);
                        OctoPackReleaseNotesFile=ReleaseNotes.txt"/>
    </Target>

    <Target Name="SetVersions" DependsOnTargets="GetCurrentBuildVersionNumber">
        <ItemGroup>
            <AssemblyInfoFiles Include="$(ProjectDir)\**\AssemblyInfo.cs" />
        </ItemGroup>
        <MSBuild.ExtensionPack.Framework.AssemblyInfo
            AssemblyInfoFiles="@(AssemblyInfoFiles)"
            AssemblyFileVersion="$(BuildVersionNumber)" />
    </Target>

    <Import Project="Common.props"/>
    <Import Project="Common.targets"/>
    <Import Project="$(ExtensionTasksPath)MSBuild.ExtensionPack.tasks"/>
</Project>

Next: Powershell

Now that the build script was working in the next blog I detail the next step which was to back up the target web directory on the server before deployment and set an under construction page in IIS whilst deploying. This was achieved by using powershell which is another technology which perfectly complements Octopus Deploy. 


kommentieren


0 Kommentar(e):