4
votes

Tools

  • MSBuild v14

  • Visual Studio 2013

  • Jenkins v2.111 running on Windows Server 2012

  • Git (bare repo on local file server)

  • Windows Batch

My goal

Build a c# Visual Studio project using MSBuild that pulls back the major and minor version numbers from the projects AssemblyInfo.cs for use during the build. The build would produce something like 1.2.$BUILD_NUMBER resulting in something like 1.2.121, 1.2.122, 1.2.123 and so on. Once the user opts to 'release' the build, a clickonce deployment with correct version in the folder name is copied to its target destination and a tag applied to the Git repository.

Pipeline example

Below is a 'work in progress' of what I've got up to. Any suggestions to improve are welcome. For those that are wondering why I'm coping the codebase out to a temporary folder. I'm using a multi-branch job in Jenkins and the folders that are auto-generated are extremely long! This gave me errors along the lines that my file name, project name or both are too long (because the entire path is above the 255 or so character length). So the only way to get around this was to copy out contents so the build and publish would work.

pipeline {
agent none
stages {
    stage ('Checkout'){
    agent any
        steps 
        {
            checkout scm
        }
    }

    stage ('Nuget Restore'){
    agent any
    steps
    {
        bat 'nuget restore "%WORKSPACE%\\src\\Test\\MyTestSolution.sln"'
    }
    }

    stage('Build Debug') {
    agent any
        steps 
        {
            bat "xcopy %WORKSPACE%\\src\\* /ey d:\\temp\\"
            bat "\"${tool 'MSBuild'}\" d:\\temp\\Test\\MyTestSolution.sln /p:Configuration=Debug /target:publish /property:PublishUrl=d:\\temp\\ /p:OutputPath=d:\\temp\\build\\ /p:GenerateBootstrapperSdkPath=\"C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v8.1A\\Bootstrapper\" /p:VersionAssembly=1.0.$BUILD_NUMBER /p:ApplicationVersion=1.0.$BUILD_NUMBER"         
        }
    }


     stage('Deploy to Dev'){
     agent none
       steps {
            script {
                env.DEPLOY_TO_DEV = input message: 'Deploy to dev?',
                parameters: [choice(name: 'Deploy to dev staging area?', choices: 'no\nyes', description: 'Choose "yes" if you want to deploy this build')]
            }
        }
     }

     stage ('Deploying to Dev')
     {
     agent any
        when {
            environment name: 'DEPLOY_TO_DEV', value: 'yes'
        }
        steps {
            echo 'Deploying debug build...'

       }

     }

     stage ('Git tagging')
     {
     agent any
        steps 
        {
        bat 'd:\\BuildTargets\\TagGit.bat %WORKSPACE% master v1.0.%BUILD_NUMBER%.0(DEV) "DEV: Build deployed."'
        }
     }
}
}

At the moment I've hard coded the major and minor version in the above script. I want to pull these values out of the AssemblyInfo.cs so that developers can control it from there without editing the Jenkinsfile. Any suggestions/best practice to achieve this?

Because I'm doing a clickonce deployment for a winforms app I've had to use MSBuild's VersionAssembly and ApplicationVersion switches to pass in the version. This seems to help with correctly labelling folders when MSBuild publishes the files. Have I have missed something in my setup which would negate these switches and make life simpler?

The last action in my pipeline is to trigger a .bat file to add a tag back into the master branch of the repository. This is another reason that I need to make the major and minor version accessible to the pipeline script.

MSBuild target for editing AssemblyInfo.cs

This code was taken from here: http://www.lionhack.com/2014/02/13/msbuild-override-assembly-version/

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
    <CompileDependsOn>
        CommonBuildDefineModifiedAssemblyVersion;
        $(CompileDependsOn);
    </CompileDependsOn>
</PropertyGroup>
<Target Name="CommonBuildDefineModifiedAssemblyVersion" Condition="'$(VersionAssembly)' != ''">
    <!-- Find AssemblyInfo.cs or AssemblyInfo.vb in the "Compile" Items. Remove it from "Compile" Items because we will use a modified version instead. -->
    <ItemGroup>
        <OriginalAssemblyInfo Include="@(Compile)" Condition="%(Filename) == 'AssemblyInfo' And (%(Extension) == '.vb' Or %(Extension) == '.cs')" />
        <Compile Remove="**/AssemblyInfo.vb" />
        <Compile Remove="**/AssemblyInfo.cs" />
    </ItemGroup>
    <!-- Copy the original AssemblyInfo.cs/.vb to obj\ folder, i.e. $(IntermediateOutputPath). The copied filepath is saved into @(ModifiedAssemblyInfo) Item. -->
    <Copy SourceFiles="@(OriginalAssemblyInfo)"
          DestinationFiles="@(OriginalAssemblyInfo->'$(IntermediateOutputPath)%(Identity)')">
        <Output TaskParameter="DestinationFiles" ItemName="ModifiedAssemblyInfo"/>
    </Copy>
    <!-- Replace the version bit (in AssemblyVersion and AssemblyFileVersion attributes) using regular expression. Use the defined property: $(VersionAssembly). -->
    <Message Text="Setting AssemblyVersion to $(VersionAssembly)" />
    <RegexUpdateFile Files="@(ModifiedAssemblyInfo)"
                Regex="Version\(&quot;(\d+)\.(\d+)(\.(\d+)\.(\d+)|\.*)&quot;\)"
                ReplacementText="Version(&quot;$(VersionAssembly)&quot;)"
                />
    <!-- Include the modified AssemblyInfo.cs/.vb file in "Compile" items (instead of the original). -->
    <ItemGroup>
        <Compile Include="@(ModifiedAssemblyInfo)" />
    </ItemGroup>
</Target>
<UsingTask TaskName="RegexUpdateFile" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">
    <ParameterGroup>
        <Files ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />
        <Regex ParameterType="System.String" Required="true" />
        <ReplacementText ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
        <Reference Include="System.Core" />
        <Using Namespace="System" />
        <Using Namespace="System.IO" />
        <Using Namespace="System.Text.RegularExpressions" />
        <Using Namespace="Microsoft.Build.Framework" />
        <Using Namespace="Microsoft.Build.Utilities" />
        <Code Type="Fragment" Language="cs">
            <![CDATA[
            try {
                var rx = new System.Text.RegularExpressions.Regex(this.Regex);
                for (int i = 0; i < Files.Length; ++i)
                {
                    var path = Files[i].GetMetadata("FullPath");
                    if (!File.Exists(path)) continue;

                    var txt = File.ReadAllText(path);
                    txt = rx.Replace(txt, this.ReplacementText);
                    File.WriteAllText(path, txt);
                }
                return true;
            }
            catch (Exception ex) {
                Log.LogErrorFromException(ex);
                return false;
            }
        ]]>
        </Code>
    </Task>
</UsingTask>

</Project>

Git tagging

This bat file is kicked off and passed values used to create and push a tag to the defined repository.

echo off
set gitPath=%1
set gitBranchName=%2
set gitTag=%3
set gitMessage=%4
@echo on
@echo Adding tag to %gitBranchName% branch.
@echo Working at path %gitPath%
@echo Tagging with %gitTag%
@echo Using commit message: %gitMessage%

d:
cd %gitPath%
git checkout %gitBranchName%
git pull
git tag -a %gitTag% -m %gitMessage%
git push origin %gitBranchName% %gitTag% 

If there are any other gold nuggests that would help streamline or improve this overall workflow, would welcome those too!

2

2 Answers

3
votes

I recently had the same problem which i solved by creating a Windows Script.

for /f delims^=^"^ tokens^=2 %%i in ('findstr "AssemblyFileVersion" %1\\AssemblyFile.cs') DO SET VERSION=%%i

This script extracts the version number from the AssemblyInfo.cs and put it inside an variable so it can be used later to tag the commit (in the same step though) :

CALL FindAssemblyVersion .\Properties

git tag %VERSION% 
git push http://%gitCredentials%@url:port/repo.git %VERSION%
0
votes

Not exactly from the assembly file but a very handy workaround to get the file version from the DLL while working with Jenkins, and using batch (or powershell) command:

Goto the directory where your DLL exists [CD Foo/Bar ]

FOR /F "USEBACKQ" %F IN (`powershell -NoLogo -NoProfile -Command (Get-Item "myApi.dll").VersionInfo.FileVersion`) DO (SET fileVersion=%F )

echo File version: %fileVersion%