September 22nd, 2013

Web Publish how to automate multi-project publish with file system

Sayed Ibrahim Hashimi
Principal Product Manager

The other day I received an email from a customer with a question which I’ve summarized as below.

I have a solution containing multiple web projects. One of the projects, _RootSite, is the top level website which I want to publish. It’s an MVC project. I also have other web projects in the same solution. These other projects are apps themselves but being published under _RootSite as an area. In some cases I’d like to publish my entire site, in other cases just the areas. Sometimes when I publish an area I need to refresh the assembly for _RootSite. How can I automate this with the most flexibility?

I am publishing by using file copy over a network share.

To help visualize the issue take a look at the screenshot below which is from the samples that I created to accompany this post (download links at the bottom of the post here).

image

In this case both AdminArea and HrArea will be published to _RootSite\Areas under their own folders.

The way that I’d solve this is to break it down into two steps using MSBuild and PowerShell to automate all steps. The first step is to build and publish all projects to the local file system. You can think of this an “intermediate publish folder”. From there you can construct copy commands to publish each part.

In the samples you’ll find publish.proj. This file is used to build and publish each (or all) components. This file is composed of two sections; a build section and a publish section. The build section takes care of building and publishing to that intermediate publish folder. The publish related targets perform the actual publish operations to prod.

Let’s take a look at the build related elements in that file. In this post I’ll use the publish injection technique which I’ve recently blogged. If you haven’t read that you may want to take a look to fully understand what is happening here.

Build related elements from publish.proj

<PropertyGroup>
  <VisualStudioVersion Condition=" '$(VisualStudioVersion)'=='' ">11.0</VisualStudioVersion>
  <Configuration Condition=" '$(Configuration)'=='' ">Release</Configuration>
  <SourceRoot Condition=" '$(SourceRoot)'=='' ">$(MSBuildThisFileDirectory)..\</SourceRoot>
  <!-- Location for build output of the project -->
  <OutputRoot Condition=" '$(OutputRoot)'=='' ">$(MSBuildThisFileDirectory)..\BuildOutput\</OutputRoot>
  <!-- Root for the publish output -->
  <PublishFolder Condition=" '$(PublishFolder)'==''">C:\temp\Publish\MySite\</PublishFolder>
  <DeployOnBuild Condition=" '$(DeployOnBuild)'=='' ">true</DeployOnBuild>
  <BuildBeforePublish Condition=" '$(BuildBeforePublish)'=='' ">true</BuildBeforePublish>
</PropertyGroup>
  
<ItemGroup>
  <ProjectsToBuild Include="$(SourceRoot)RootSite\_RootSite.csproj">
    <AdditionalProperties>publishUrl=$(PublishFolder);</AdditionalProperties>
  </ProjectsToBuild>

  <ProjectsToBuild Include="$(SourceRoot)AdminArea\AdminArea.csproj">
    <AdditionalProperties>publishUrl=$(PublishFolder)Areas\Admin\;</AdditionalProperties>
  </ProjectsToBuild>

  <ProjectsToBuild Include="$(SourceRoot)HrArea\HrArea.csproj">
    <AdditionalProperties>publishUrl=$(PublishFolder)Areas\HR\;</AdditionalProperties>
  </ProjectsToBuild>
</ItemGroup>

<PropertyGroup>
  <BuildAndPublishDependsOn>
    $(BuildAndPublishDependsOn);
    CoreBuildAndPublish;
    Publish
  </BuildAndPublishDependsOn>
</PropertyGroup>
<Target Name="BuildAndPublish" DependsOnTargets="$(BuildAndPublishDependsOn)" />
  
<Target Name="CoreBuildAndPublish">
  <Message Text="Building and publishing all projects in ProjectsToBuild [@(ProjectsToBuild)]"
            Condition=" '$(DeployOnBuild)'=='true' "/>
  <Message Text="Building all projects in ProjectsToBuild [@(ProjectsToBuild)]"
            Condition=" '$(DeployOnBuild)'!='true' "/>
    
  <MSBuild Projects="@(ProjectsToBuild)"
            Properties="
              VisualStudioVersion=$(VisualStudioVersion);
              Configuration=$(Configuration);
              OutputPath=$(OutputRoot);
              WebPublishMethod=FileSystem;
              DeployOnBuild=$(DeployOnBuild);
              DeployTarget=WebPublish;
              PublishProfile=$(MSBuildThisFileFullPath)" />
</Target>

Here you can see that we have defined the main build related target; BuildAndPublish. This is a publish to the intermediate publish folder. In this snippet I define some MSBuild parameters which are required when building the projects. I define the projects which I want to build in the ProjectsToBuild item list. Along with that I declare all the MSBuild properties which are specific to that project in the AdditionalProperties metadata. FYI for more info on Properties vs. AdditionalProperties take a look at my previous post. In this case the only parameter which varies from project to project is the publishUrl. This defines where the project will be published to.

For the _RootSite project publishUrl is $(PublishFolder), the intermediate publish folder. In my case this is c:\temp\Publish\MySite\. For AdminArea publishUrl is $(PublishFolder)Areas\Admin and similarly for HrArea. This will ensure the contents are published to the correct location.

Inside CoreBuildAndPublish I invoke the MSBuild task to build each project and publish to the local intermediate publish folder.  After invoking that all projects will be built and the $(PublishFolder) will be populated.

Now let’s take a look at the publish related elements in publish.proj.

Publish elements from publish.proj

<PropertyGroup>
  <ProdPublishFolder Condition=" '$(ProdPublishFolder)'=='' ">C:\temp\Publish\MySite-Prod\</ProdPublishFolder>

  <!-- If /p:BuildBeforPublish is true then invoke build before any publish operations. -->
  <PublishComputeItemsDependsOn Condition=" '$(BuildBeforePublish)'=='true' ">$(PublishComputeItemsDependsOn);BuildAndPublish</PublishComputeItemsDependsOn>
    
  <PublishComputeItemsDependsOn>$(PublishComputeItemsDependsOn);</PublishComputeItemsDependsOn>
  <PublishMySiteBinDependsOn>$(PublishMySiteBinDependsOn);PublishComputeItems</PublishMySiteBinDependsOn>
  <PublishMySiteAllDependsOn>$(PublishMySiteAllDependsOn);PublishComputeItems</PublishMySiteAllDependsOn>
  <PublishAdminAreaDependsOn>$(PublishAdminAreaDependsOn);PublishComputeItems</PublishAdminAreaDependsOn>
  <PublishHrAreaDependsOn>$(PublishHrAreaDependsOn);PublishComputeItems</PublishHrAreaDependsOn>  
</PropertyGroup>

  
<!-- 
This target populates the publish items so that they can be
used in the Publish* targets.
-->
<Target Name="PublishComputeItems" DependsOnTargets="$(PublishComputeItemsDependsOn)">
  <ItemGroup>
    <PubMySiteBinFiles Include="$(PublishFolder)bin\**\*"/>

    <PubMySiteAll Include="$(PublishFolder)**\*"/>

    <PubMySiteAdminArea Include="$(PublishFolder)Areas\Admin\**\*"/>
      
    <PubMySiteHrArea Include="$(PublishFolder)Areas\HR\**\*"/>
  </ItemGroup>
</Target>

<!-- This is just a placeholder target so that we can specify /p:PublishTargets from the cmd line -->
<Target Name="Publish" DependsOnTargets="$(PublishTargets.Split('|'))"></Target>
  
<Target Name="PublishMySiteBin" 
        DependsOnTargets="$(PublishMySiteBinDependsOn)"
        Inputs="@(PubMySiteBinFiles)" 
        Outputs="@(PubMySiteBinFiles->'$(ProdPublishFolder)bin\%(RecursiveDir)%(FileName)%(Extension)')">
  <Copy SourceFiles="@(PubMySiteBinFiles)" 
        DestinationFiles="@(PubMySiteBinFiles->'$(ProdPublishFolder)bin\%(RecursiveDir)%(FileName)%(Extension)')"/>
</Target>
  
<Target Name="PublishMySiteAll" 
        DependsOnTargets="$(PublishMySiteAllDependsOn)"
        Inputs="@(PubMySiteAll)"
        Outputs="@(PubMySiteAll->'$(ProdPublishFolder)%(RecursiveDir)%(FileName)%(Extension)')">
  <Copy SourceFiles="@(PubMySiteAll)"
        DestinationFiles="@(PubMySiteAll->'$(ProdPublishFolder)%(RecursiveDir)%(FileName)%(Extension)')"/>
</Target>

<Target Name="PublishAdminArea" 
        DependsOnTargets="$(PublishAdminAreaDependsOn)"
        Inputs="@(PubMySiteAdminArea)"
        Outputs="@(PubMySiteAdminArea->'$(ProdPublishFolder)Areas\Admin\%(RecursiveDir)%(FileName)%(Extension)')">
  <Copy SourceFiles="@(PubMySiteAdminArea)"
        DestinationFiles="@(PubMySiteAdminArea->'$(ProdPublishFolder)Areas\Admin\%(RecursiveDir)%(FileName)%(Extension)')"/>
</Target>
  
<Target Name="PublishHrArea" 
        DependsOnTargets="$(PublishHrAreaDependsOn)"
        Inputs="@(PubMySiteHrArea)"
        Outputs="@(PubMySiteHrArea->'$(ProdPublishFolder)Areas\Hr\%(RecursiveDir)%(FileName)%(Extension)')">
  <Copy SourceFiles="@(PubMySiteHrArea)"
        DestinationFiles="@(PubMySiteHrArea->'$(ProdPublishFolder)Areas\Hr\%(RecursiveDir)%(FileName)%(Extension)')"/>
</Target>

Here we define leaf targets like, PublishMySiteAll, PublishMySiteBin, etc. These targets contain a simple Copy command. All of these targets have Inputs and Outputs so that these are all incremental. Up to date files will be skipped by MSBuild automatically by using Inputs and Outputs. All of these targets depend on the PublishComputeItems target. This target is responsible for populating the item lists used by other publish targets. So if I want to publish the entire site I can execute the PublishMySiteAll target. If I just want to publish the Hr area I can invoke the PublishHrArea target and so forth. Pretty straight forward stuff here.

Now let’s tie it all together with PowerShell.

In the samples you’ll find publish.ps1. This is the file that will be used to simplify the user experience and drive the entire process. The full contents are below.

param(
    [switch]
    $BuildProjects,

    [switch]
    $PublishAll,

    [switch]
    $PublishMySiteBin,

    [switch]
    $PublishAdminArea,

    [switch]
    $PublishHrArea
)

function Get-ScriptDirectory
{
    $Invocation = (Get-Variable MyInvocation -Scope 1).Value
    $path = Split-Path $Invocation.MyCommand.Path
    return ("{0}\" -f $path)
}

# we need to build the msbuild command
function Get-MSBuildCommandArgs(){
    param(
        [Parameter(Mandatory=$true)]
        [string]
        $logsDir
    )

    $cmdArgs = @()
    $cmdArgs += ("{0}publish.proj" -f $scriptDir)
    $cmdArgs += '/p:Configuration=release'
    $cmdArgs += '/p:VisualStudioVersion=11.0'

    if($BuildProjects){
        $cmdArgs += "/p:BuildBeforePublish=true"
    }
else {
$cmdArgs += "/p:BuildBeforePublish=false"
}
    foreach($target in (Get-PublishTargetsFromParams)){
        $cmdArgs += ("/t:{0}" -f $target)
    }

    $logFileBaseName = ("publish-$(get-date -f yyyy.MM.dd.ss.ff)")

    # add logging parameters here
    # /flp1:"v=d;logfile=logs\%~n0.d.log" /flp2:"v=diag;logfile=logs\%~n0.diag.log"
    $cmdArgs += ('/flp1:"v=d;logfile={0}{1}.d.log" ' -f $logsDir, $logFileBaseName)
    $cmdArgs += ('/flp2:"v=diag;logfile={0}{1}.diag.log" ' -f $logsDir, $logFileBaseName)

    return $cmdArgs
}

function Get-PublishTargetsFromParams(){
    $pubTargets = @()
        
    if($PublishAll){
        # if PublishAll is passed we do not need to add any additional targets
        # for any other publish targets
        $pubTargets += "PublishMySiteAll"
    }
    else{
        if($PublishMySiteBin){
            $pubTargets += "PublishMySiteBin"
        }
        if($PublishAdminArea){
            $pubTargets += "PublishAdminArea"
        }
        if($PublishHrArea){
            $pubTargets += "PublishHrArea"
        }
    }

    return $pubTargets
}
function Get-MSBuildPath(){
    return "$env:windir\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe"
}

# begin script

$scriptDir = (Get-ScriptDirectory)
$logsDir = (Join-Path -Path $scriptDir -ChildPath "logs\")
if(!(Test-Path $logsDir)){
    mkdir $logsDir
}
$msbuildCmd = (Get-MSBuildPath)
$allArgs  = (Get-MSBuildCommandArgs -logsDir $logsDir)
"Calling msbuild.exe with the following parameters. [$msbuildCmd] [$allArgs] " | Write-Host
& $msbuildCmd $allArgs

 

This script will build up the msbuild command based on the switches above and then invoke the correct targets based on the inputs. This is pretty straight forward PowerShell as well. It just interprets the switches that are passed in and builds the correct set of properties/targets to be passed to msbuild.exe.

.\publish.ps1 -BuildProjects -PublishAll

This will build all projects and publish them.

.\publish.ps1 -BuildProjects -PublishHrArea

This will build all projects and only publish the HR area

.\publish.ps1 -BuildProjects -PublishAdminArea -PublishMySiteBin

This will build all projects and publish the bin\ folder of MySite as well as the Admin area

.\publish.ps1 -PublishAdminArea -PublishMySiteBin

This will skip the build and pub to intermediate publish folder (must have been performed earlier at some point) and publish the Admin area and the bin\ folder of MySite.

 

You can find all the samples at https://dl.dropboxusercontent.com/u/40134810/blog/SubappPublish.zip. You can find the latest version in my publish-samples repository at SubappPublish.

If you have any comments on this please let us know by replying below.

 

Sayed Ibrahim Hashimi | http://msbuildbook.com | @SayedIHashimi

Author

Sayed Ibrahim Hashimi
Principal Product Manager

Sayed Ibrahim Hashimi has a computer engineering degree from the University of Florida. He works at Microsoft as a Principal Program Manager creating better .NET Core, and ASP.NET Core developers.

0 comments

Discussion are closed.