版本控制 .NET 构建

13

想知道.NET构建版本控制的最佳方法是什么?

我使用:

  • TFS 2013进行版本控制
  • TFS门禁检查
  • Wix 3.8将代码打包到MSI文件中

我想设置以下内容的版本号:

  • 程序集(AssemblyInfo.cs,或在所有项目中引用的共享文件)
  • MSI包(在Wix代码中)
  • 文档(例如在构建的最终输出中的readme.txt文件内部)
  • 等等

理想的版本号应该允许追溯安装的软件到确切的源代码。类似于:

<Major>.<Minor>.<TFS_changeset_number>

我希望将我想要存储的版本的前两个部分存储在简单的文本\ XML文件中,与解决方案一起放入版本控制中,因为我认为它们应该在一起。开发人员将手动更新此文件(例如遵循语义化版本控制方法)。每个构建都将读取此版本文件,从调用CI工具获取版本的第三部分,并使用该版本更新所有必要的文件。

最佳实现方式是什么?

我过去尝试了一些方法:

1)NAnt \ MsBuild包装器执行版本控制工作,然后调用MsBuild解决方案。它可以从CI工具(Jenkins \ TeamCity \等)调用。

问题 - 与TFS门控提交的集成不美观,因为我需要两次构建解决方案。

2)自定义TFS构建过程模板

问题 - 它不太简单,并会导致TFS升级时进行一些合并工作。此外,在门控提交中还不存在变更集编号,因此我们只能使用先前的变更集id。

3)解决方案中的单独的MsBuild项目,仅执行此版本控制任务,并配置为在VS解决方案的Project Build顺序中首先运行。

问题 - 必须在所有其他项目(包括所有未来的项目)中引用此元项目,感觉不美观

我知道各种MsBuild和TFS扩展包可以简化更新。这个主题不是关于哪一个扩展包最好。这个问题比较方法论而不是技术性的。我还认为,如果Microsoft在其标准TFS构建模板中包含一些版本控制功能,那将是理想的选择。其他CI工具已经具备了这个功能(AssemblyInfo patcher)。




11/09/2014更新

我决定明确表达符合敏捷\持续交付最佳实践的版本控制原则

1)能够重现任何历史构建

2)根据CD原则,作为第1点的结果,每个东西(源代码、测试、应用程序配置、环境配置、构建\打包\部署脚本等)都存储在版本控制下,因此分配了一个版本号

3)版本号紧密地与其适用的源代码一起存储

4)人们能够根据他们的业务\营销逻辑更新版本

5)只有一个主要版本副本,用于自动化构建\打包过程的所有部分

6)您可以轻松地说出目标系统当前安装的软件的版本号

7)安装的软件版本必须明确地标识用于构建它的源代码

8)很容易比较版本号以确定哪个版本较低或较高-控制允许哪些升级\降级方案及其实现细节




15/09/2014更新

请参见我的答案以下
我很幸运找到了符合所有要求的解决方案!


你有研究过 T4 文本模板吗?一个中央模板可以提供默认和生成的值。项目本地文件将提供特定于程序集的属性。 - Darcara
Darcara,感谢您提出这个有趣的想法。我刚刚了解了T4——您是指使用构建时模板来插入版本号吗?我需要更多地了解这个并仔细思考)) - Ivan
8个回答

6

天啊,那些复杂的答案。

在 TFS 2013 中,这很简单。Community TFS Build Extensions site 提供了一个简单的 PowerShell 脚本,用于从 TFS 分配的构建名称中提取构建号码。

enter image description here

你需要配置构建号格式为"$(BuildDefinitionName)_6.0.0$(Rev:.r)",这将生成类似于"6.0.0.1"的版本号,其中的"1"会在每次构建时递增。
接下来,你需要将PowerShell版本脚本添加到构建中,它会自动获取上述版本号并将其应用于构建文件夹中的所有AssemblyInfo.*文件。你可以通过更新脚本来添加其他文件类型。

1
MrHinsh,如果您使用当前的构建定义重新构建旧代码,则会得到一个较新的版本,这是不好的。版本应该与源代码修订历史对齐,而不是构建历史。我希望能够重现任何历史构建并获得相同的结果。 - Ivan
在TFS 2013中使用ds19重新构建旧代码 - 队列新的构建...> 参数选项卡> 获取版本。无需编辑\克隆构建定义。 - Ivan
MrHinsh,我不想无限期地保留构建。在活动项目中,这将占用太多空间。因此,需要从源代码重新构建。分支与持续交付是一个单独的宗教讨论,我不想去那里。能够重现构建对两种方式都有好处。 - Ivan
我通常使用Visual Studio的Release Manager,并自动设置保留时间为无限期。您可以使用PowerShell来支持DeployIt、Octopus或其他工具... - MrHinsh - Martin Hinshelwood
如果您想要产品的一致和可靠部署,您应该使用发布管理工具。任何发布管理工具都能够在构建输出沿着发布管道流动被视为可行时无限期地更新保留。RM是Visual Studio的一部分,并包含在您的MSDN中。 - MrHinsh - Martin Hinshelwood
显示剩余6条评论

3
我想到了一个解决方案,满足了我所有的要求,而且还非常简单!

想法

将所有自定义版本控制工作放入自定义的 Version.proj MsBuild 脚本中,在 TFS 构建定义中在.sln之前调用它。脚本将版本注入源代码(SharedAssemblyInfo.cs、Wix 代码、readme.txt),然后解决方案构建构建该源代码。

版本由存储在 TFS 中与源代码一起的 Version.xml 文件中的 Major 和 Minor 数字以及由父 TFS Build 过程提供的 Changeset Number 作为 TF_BUILD_SOURCEGETVERSION 环境变量组成。

enter image description here

感谢 Microsoft:

  • TFS 2013 - 将 TF_BUILD environment variables 传递给构建过程,这是我获取当前正在构建的代码的 changeset number 的方法
  • MsBuild 允许在 C# 中使用 inline tasks - 使用 Regex C# 类替换源文件中的版本

因此,不需要使用任何 MsBuild 或 TFS 社区/扩展包/插件等。也没有必要修改标准的 TFS 构建过程模板。简单的解决方案导致了高可维护性!

实施

Version.proj

<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

<!--
Run this script for every build in CI tool before building the main solution
If you build in TFS, simply add script as the first item in a list of projects under Process tab > Build > Projects
-->

  <PropertyGroup>
    <VersionFile>..\Version.xml</VersionFile>
    <MainProjectDir>... set this to main solution directory ...</MainProjectDir>
  </PropertyGroup>

  <Import Project="$(VersionFile)"/>
  <Import Project="Common.proj"/>

  <Target Name="GetMajorMinorNumbers">
    <Error Text="ERROR: MajorVersion is not set in $(VersionFile)" Condition="'$(MajorVersion)' == ''" />
    <Message Text="MajorVersion: $(MajorVersion)" />

    <Error Text="ERROR: MinorVersion is not set in $(VersionFile)" Condition="'$(MinorVersion)' == ''" />
    <Message Text="MinorVersion: $(MinorVersion)" />
  </Target>


  <Target Name="GetChangesetNumber">
    <Error Text="ERROR: env var TF_BUILD_SOURCEGETVERSION is not set, see http://msdn.microsoft.com/en-us/library/hh850448.aspx" Condition="'$(TF_BUILD_SOURCEGETVERSION)' == ''" />
    <Message Text="TF_BUILD_SOURCEGETVERSION: $(TF_BUILD_SOURCEGETVERSION)" />
  </Target>


  <Target Name="FormFullVersion">
    <PropertyGroup>
        <FullVersion>$(MajorVersion).$(MinorVersion).$(TF_BUILD_SOURCEGETVERSION.Substring(1))</FullVersion>
    </PropertyGroup>
    <Message Text="FullVersion: $(FullVersion)" />
  </Target>


  <Target Name="UpdateVersionInFilesByRegex">
    <ItemGroup>
        <!-- could have simplified regex as Assembly(File)?Version to include both items, but this can update only one of them if another is not found and operation will still finish successfully which is bad -->
        <FilesToUpdate Include="$(MainProjectDir)\**\AssemblyInfo.cs">
            <Regex>(?&lt;=\[assembly:\s*Assembly?Version\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
            <Replacement>$(FullVersion)</Replacement>
        </FilesToUpdate>
        <FilesToUpdate Include="$(MainProjectDir)\**\AssemblyInfo.cs">
            <Regex>(?&lt;=\[assembly:\s*AssemblyFileVersion\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
            <Replacement>$(FullVersion)</Replacement>
        </FilesToUpdate>
        <FilesToUpdate Include="CommonProperties.wxi">
            <Regex>(?&lt;=&lt;\?define\s+ProductVersion\s*=\s*['"])(\d+\.){2,3}\d+(?=["']\s*\?&gt;)</Regex>
            <Replacement>$(FullVersion)</Replacement>
        </FilesToUpdate>
    </ItemGroup>

    <Exec Command="attrib -r %(FilesToUpdate.Identity)" />
    <Message Text="Updating version in %(FilesToUpdate.Identity)" />
    <RegexReplace Path="%(FilesToUpdate.Identity)" Regex="%(Regex)" Replacement="%(Replacement)"/>
  </Target>



  <Target Name="WriteReadmeFile">
    <Error Text="ERROR: env var TF_BUILD_BINARIESDIRECTORY is not set, see http://msdn.microsoft.com/en-us/library/hh850448.aspx" Condition="'$(TF_BUILD_BINARIESDIRECTORY)' == ''" />
    <WriteLinesToFile
        File="$(TF_BUILD_BINARIESDIRECTORY)\readme.txt"
        Lines="This is version $(FullVersion)"
        Overwrite="true"
        Encoding="Unicode"/>
  </Target>

  <Target Name="Build">
    <CallTarget Targets="GetMajorMinorNumbers" />
    <CallTarget Targets="GetChangesetNumber" />
    <CallTarget Targets="FormFullVersion" />
    <CallTarget Targets="UpdateVersionInFilesByRegex" />
    <CallTarget Targets="WriteReadmeFile" />
  </Target>

</Project>

Common.proj

<Project xmlns='http://schemas.microsoft.com/developer/msbuild/2003' ToolsVersion="12.0">
<!-- based on example from http://msdn.microsoft.com/en-us/library/dd722601.aspx -->
  <UsingTask TaskName="RegexReplace" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll">
    <ParameterGroup>
      <Path ParameterType="System.String" Required="true" />
      <Regex ParameterType="System.String" Required="true" />
      <Replacement ParameterType="System.String" Required="true" />
    </ParameterGroup>
    <Task>
      <Reference Include="System.Core" />
      <Using Namespace="System" />
      <Using Namespace="System.IO" />
      <Using Namespace="System.Text.RegularExpressions" />
      <Code Type="Fragment" Language="cs"><![CDATA[
            string content = File.ReadAllText(Path);
            if (! System.Text.RegularExpressions.Regex.IsMatch(content, Regex)) {
                Log.LogError("ERROR: file does not match pattern");
            }
            content = System.Text.RegularExpressions.Regex.Replace(content, Regex, Replacement);
            File.WriteAllText(Path, content);
            return !Log.HasLoggedErrors;
]]></Code>
    </Task>
  </UsingTask>

  <Target Name='Demo' >
    <RegexReplace Path="C:\Project\Target.config" Regex="$MyRegex$" Replacement="MyValue"/>
  </Target>
</Project>

Version.xml

<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <MajorVersion>1</MajorVersion>
    <MinorVersion>1</MinorVersion>
  </PropertyGroup>
</Project>

4
看起来一点也不简单。 - MrHinsh - Martin Hinshelwood

2
注入变更集编号很好,但这并不能满足我的所有需求。例如,如果我知道由我的构建系统生成的构建的变更集编号,我真的知道这个可执行文件中包含了什么吗?不,我不知道,因为构建可能是私有构建,甚至是失败的构建。
相反,我们信任构建编号(实际上是BuildID),以便我们可以在事后通过查询TFS系统来获取尽可能多的关于该构建的数据。这样,我们就可以确定构建是否为私有构建,是否为传递了特殊命令行参数的构建以及其他相关细节。
我们使用以下设置:
  1. Set the build format in the build definition to be something like: 1.0.0.$(BuildId)

  2. In the build process template, in the MSBuild task, inject the following to the MSBuildArguments item

    String.format("/p:BuildNumber={0}", BuildDetail.BuildNumber)
    ...ensure you leave what was already there.

  3. In your projects (or ideally, a common props file included in all your projects) defined a property called build number that defaults to something like 0.0.0.1.

    <PropertyGroup><BuildNumber Condition="'$(BuildNumber)'==''">0.0.0.1</BuildNumber></PropertyGroup>
    note that you can further break this down however you like using property functions. We use this to get the Major number for instance:
    <MajorVersionNumber>$(BuildNumber.Split('.')[0])</MajorVersionNumber>
    and yes, this does put a dependency on the build number format in our builds!

  4. In your project, you can make use of the build number property to inject into various files during build time. You can use custom build tasks (I use 'sed' and this macro to inject a version number into a .h file for instance - the same could be done with any text-based file type).

  5. If you have more complex versioning requirements you can make use of custom MSBuild targets that inject the build number into other file types. I have done exactly that with versioning for NuGet packages that our builds automatically create for our common-library CS projects for example.

若要通过其构建编号查询构建,则可以在PowerShell中执行以下操作(需要安装Visual Studio团队基础服务器Power Tools):
Add-PSSnapin Microsoft.TeamFoundation.PowerShell # you must install the VS TFS Power tools with the powershell option enabled to get this... a must have IMHO
$tfs = Get-TfsServer http://yourtfsserver:8080/tfs/YourProjectCollectionName
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.TeamFoundation.Build.Client')
$buildserver = $tfs.GetService([Microsoft.TeamFoundation.Build.Client.IBuildServer])
$buildQuerySpec = $buildserver.CreateBuildDetailSpec("YourTFSProjectName","Your-Build-Definition-Name")
$buildQuerySpec.BuildNumber = '1.0.0.12345' # put your build number here.
$buildQuerySpec.QueryDeletedOption = 'IncludeDeleted'
$bld = $buildserver.QueryBuilds($buildQuerySpec)

使用'$bld',您现在可以查询特定构建的所有属性。例如,要查看构建基于哪个变更集、构建状态、谁启动了构建以及该构建是否存在 shelveset:

$bld.Builds[0] | Ft -Property BuildFinished,RequestedFor,ShelvesetName,Status,SourceGetVersion -AutoSize

编辑:更正PowerShell脚本中的拼写错误


d3r3kk,我也在考虑使用BuildId而不是Changeset号码。我同意你的看法,特别是在门禁检入的情况下,这样做可能更好。构建的问题在于它们根据保留策略被删除,因此您会失去可追溯性链接! - Ivan
1
构建工件确实被删除了,但是TFS构建元数据并没有被删除。请注意我的Powershell脚本中的这一行:$buildQuerySpec.QueryDeletedOption='IncludeDeleted'。 - d3r3kk
啊!我明白了!感谢你的解释。 - Ivan
如果您在构建过程中标记源代码,则可以重新创建任何已删除的构建。 - ds19

2
我下载了这个脚本(感谢MrHinsh的答案),将脚本检入源代码控制,并在构建定义的预构建脚本路径中指定它:

BuildDefPreBuild

然后我将构建编号格式配置为"$(BuildDefinitionName)_1.0.0$(Rev:.r)"(有关详细信息,请参见 MrHinsh的答案)。
令我惊讶的是,它起作用了。

0

我们有类似的需求,并使用NANT ASMINFO TASK。在TFS构建期间,我们调用此额外的NANT目标,以创建一个新的AssemblyVersion.cs文件。

<asminfo output="AssemblyInfo.cs" language="CSharp">
<imports>
    <import namespace="System" />
    <import namespace="System.Reflection" />
    <import namespace="System.EnterpriseServices" />
    <import namespace="System.Runtime.InteropServices" />
</imports>
<attributes>
    <attribute type="ComVisibleAttribute" value="false" />
    <attribute type="CLSCompliantAttribute" value="true" />
    <attribute type="AssemblyVersionAttribute" value="${version.number}" />
    <attribute type="AssemblyTitleAttribute" value="My fun assembly" />
    <attribute type="AssemblyDescriptionAttribute" value="More fun than a barrel of monkeys" />
    <attribute type="AssemblyCopyrightAttribute" value="Copyright (c) 2002, Monkeyboy, Inc." />
    <attribute type="ApplicationNameAttribute" value="FunAssembly" />
</attributes>
<references>
    <include name="System.EnterpriseServices.dll" />
</references>

请注意属性 ${version.number},它实际上是根据您的要求设置的。然后我们循环遍历现有的 AssemblyVersion.cs 文件并将其设置为只读,然后用我们创建的新文件替换它。
<attrib readonly="false" file="${project}\AssemblyVersion.cs"/>

正如你所知,这个目标在编译之前被执行。


Isaiah4110,你如何在TFS构建中调用<asminfo>任务? - Ivan
在我们的情况下,我们使用NANT脚本进行实际编译,而不是msbuild/tfs。TFS只是我们启动构建的媒介。构建模板内部会转换并调用NANT脚本。基本上是一个invokeprocess活动,它调用nant.exe以及参数(.build文件、目标名称以及其他选项)。 - Isaiah4110
我看到了一个定制的构建过程 - 这正是我想要避免的)) - Ivan
这意味着您将继续使用旧的定制流程,而不是来自新TFS版本的新流程,其中可能包含许多新功能和错误修复。例如,编辑和调试MsBuild脚本更加简单。我之所以这样说,是因为我两者都做过。请查看12个敏捷原则,特别是这一条:“简单性——最大化未完成工作量的艺术——是必要的。” - Ivan
活动不会改变,但是流程模板可能会改变。 - Ivan
显示剩余2条评论

0

我使用 [Major].[Minor].[BuildNumber].[revision]

然后我可以追溯到一个构建,这将给出一个更改集,这将给出一个工作项等。

您可以使用社区构建任务或者我自己编写。

我对于MSI和DacPac也是同样的做法

基本上,归因于assemblyinfo文件,然后使用正则表达式更新数字,在每日构建中保留net版本相同的值并仅更新文件版本,以便您可以维护兼容性

对于MSI和Dacapac的方法相同,只是位置不同。在MSI中,我有一个Buildparams.wxi,其具有以下结构

<?xml version="1.0" encoding="utf-8"?>
<Include>
    <?define ProductVersion="1.2.3.4"?>  
</Include>

Productversion 然后在 wix 脚本中被用作 var.Productversion。在构建之前,我会使用我想要使用的构建号更新 1.2.3.4。


它是如何实现的?你使用哪些构建工具? - Ivan
纯粹的TFS自定义活动任务,用C#编写。 - Just TFS
是的,我过去也做过这个。下一次 TFS 升级时,您需要将您的流程模板与升级后的模板合并或继续使用您的自定义模板。此外,流程定制是一个繁琐的过程,特别是当您需要重新编译 C# 自定义活动、部署它们等时。想象一下,如果您需要更改版本控制逻辑中的某些内容,使用 MsBuild 或类似工具会更简单。 - Ivan

0
 <Target Name="GetTFSVersion" >  
    <Exec Command="$(TF) history /server:[tfs]\DefaultCollection&quot;$/FP4WebServices/Staging&quot; /stopafter:1 /recursive /login:domain\user,password /noprompt | find /V &quot;Changeset&quot; | find /V > $(LatestChangeSetTxtFile).tmp"/>
    <Exec Command="FOR /F &quot;eol=; tokens=1 delims=, &quot; $(PERCENT_SIGN)$(PERCENT_SIGN)i in ($(LatestChangeSetTxtFile).tmp) do $(AT_SIGN)echo $(PERCENT_SIGN)$(PERCENT_SIGN)i > $(LatestChangeSetTxtFile)"/>
    <ReadLinesFromFile File="$(LatestChangeSetTxtFile)">
      <Output TaskParameter="lines" PropertyName="ChangeSet"/>
    </ReadLinesFromFile>
    <Message Text="TFS ChangeSet: $(ChangeSet)"/>        
  </Target>

  <Target Name="SetVersionInfo" DependsOnTargets="GetTFSVersion">
    <Attrib Files="@(AssemblyInfoFiles)" Normal="true"/>
     <FileUpdate Files="@(AssemblyInfoFiles)" Regex="AssemblyFileVersion\(&quot;.*&quot;\)\]" ReplacementText="AssemblyFileVersion(&quot;$(Major).$(Minor).$(Build).$(ChangeSet)&quot;)]" />
   </Target>

1
这种方法的问题在于你会得到最新的变更集编号,而不一定是你正在构建的那个。此外,你还必须安全地处理登录\密码。 - Ivan

0
我正在开发一个项目,其需求类似但不完全相同。主版本和次版本都存储在AssemblyInfo中,就像任何标准的.NET项目一样。在我们的构建服务器上,我们有一个包装的MsBuild脚本,它调用.sln构建,但它还执行一些设置任务,包括生成其他构建信息。该构建文件仅在我们的构建服务器上执行。通过Visual Studio构建的开发人员只会构建.sln,而不会得到额外的行为。

是的,这是一种完全可以接受的做事方式。而且我过去也曾经这样做过。在我目前的情况下,我使用TFS构建来进行门控提交,我非常喜欢这种方式,尤其是在有很多开发人员和TDD的情况下。 - Ivan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接