只是想知道.NET构建版本化的最佳方法是什么?
我用:
我想设置版本:
理想版本号允许将已安装的软件追溯到确切的源代码 类似的东西:
<Major>.<Minor>.<TFS_changeset_number>
我希望在解决方案附近的版本控制中存储一些简单文本\ XML文件的版本的前两部分,因为我相信它们应该共存。开发人员将手动更新此文件(例如,遵循语义版本控制方法)。每个构建版本都将读取此版本文件,从调用CI工具中获取3d版本的部分版本,并使用该版本更新所有必需的文件。
实现此目标的最佳方法是什么?
我过去曾使用过一些方法:
1)执行此版本的NAnt \ MsBuild包装器,然后调用MsBuild获取解决方案。它可以从CI工具(Jenkins \ TeamCity \ etc)调用。
问题 - 因为我构建了两次解决方案,所以与TFS门控签入集成很难看。
2)自定义TFS构建过程模板
问题 - 它并不那么简单,并导致一些合并工作进行TFS升级。此外,更改集编号在门控签到中尚不存在,因此我们只能使用之前的变更集ID。
3)解决方案中的一个单独的MsBuild项目,它只执行此版本控制任务,并配置为在VS解决方案的项目构建顺序中首先运行。
问题 - 需要在所有其他项目(包括所有未来的项目)中引用这个感觉难看的元项目
我知道可以简化更新的不同MsBuild和TFS扩展包。这个主题不是关于哪一个是最好的。问题在于方法而不是技术。
我还认为,如果微软在其标准TFS构建模板中包含版本控制内容,那将是理想的选择。其他CI工具已经具有此功能(AssemblyInfo修补程序)。
更新2014年9月9日
我决定明确表达符合敏捷\持续交付最佳做法的版本规则:
1)能够重现任何历史建筑
2)由于1)并且根据CD原则,所有内容(源代码,测试,应用程序配置,环境配置,构建\包\部署脚本等)都存储在版本控制下,因此分配了版本它
3)版本号与它适用的源代码紧密存储在一起
4)人们可以根据他们的业务\营销逻辑更新版本
5)该版本只有一个主副本,用于自动构建\包装过程的所有部分
6)您可以轻松地说出目标系统上当前安装了哪个版本的软件
7)已安装软件的版本必须明确标识用于构建它的源代码
8)比较版本比较低,哪个更高,以控制允许哪些升级\降级方案及其实现细节
非常简单
更新15/09/2014
查看我自己的回答below 我很幸运能找到满足我所有要求的解决方案!
答案 0 :(得分:5)
好悲伤,所有那些复杂的答案。
在TFS 2013中,这很简单。 Community TFS Build Extensions site提供了一个简单的PowerShell,用于从TFS分配的构建名称中提取构建号。
您将构建号格式配置为“$(BuildDefinitionName)_6.0.0 $(Rev:.r)”,这将产生类似“6.0.0.1”的内容,其中“1”为每个构建增加。 / p>
然后将PowerShell版本控制脚本添加到构建中,它会自动删除上面的版本号并将其应用到构建文件夹中的所有AssemblyInfo。*文件。您可以通过更新脚本来添加其他文件类型。
答案 1 :(得分:3)
我提出了一个满足我所有要求的解决方案,而且非常简单!
<强> IDEA 强>
将所有自定义版本控制工作放入自定义 Version.proj MsBuild脚本中,并在 .sln 之前在TFS构建定义中调用它。该脚本将Version注入源代码(SharedAssemblyInfo.cs,Wix代码,readme.txt),然后解决方案构建构建该源代码。
版本由存储在TFS中的 Version.xml 文件中的主要和次要号码以及源代码组成;和来自变更集编号的提供 TF_BUILD_SOURCEGETVERSION env var by parent TFS Build process
感谢Microsoft:
因此无需使用任何MsBuild或TFS社区\扩展包\ addons \ whatever。并且无需修改标准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>(?<=\[assembly:\s*Assembly?Version\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
<Replacement>$(FullVersion)</Replacement>
</FilesToUpdate>
<FilesToUpdate Include="$(MainProjectDir)\**\AssemblyInfo.cs">
<Regex>(?<=\[assembly:\s*AssemblyFileVersion\(["'])(\d+\.){2,3}\d+(?=["']\)\])</Regex>
<Replacement>$(FullVersion)</Replacement>
</FilesToUpdate>
<FilesToUpdate Include="CommonProperties.wxi">
<Regex>(?<=<\?define\s+ProductVersion\s*=\s*['"])(\d+\.){2,3}\d+(?=["']\s*\?>)</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>
答案 2 :(得分:2)
注入变更集编号很棒,但它确实没有做我需要的所有内容。例如,如果我知道构建系统生成的构建的变更集号,我真的知道该可执行文件中的内容吗?不,我不这样做,因为构建可能是私有构建,甚至构建失败。
我们相信Build编号(真的是BuildID),这样我们就可以通过查询TFS系统获得尽可能多的关于该版本的数据。通过这种方式,我们可以确定构建是否是私有构建,是否有传递给它的特殊命令行参数的构建,以及其他相关细节。
我们使用以下设置:
将构建定义中的构建格式设置为:1.0.0。$(BuildId)
在构建过程模板中,在MSBuild任务中,将以下内容注入MSBuildArguments项目
String.format("/p:BuildNumber={0}", BuildDetail.BuildNumber)
...确保保留已存在的内容。
在您的项目中(或者理想情况下,所有项目中包含的常见道具文件)定义了一个名为内部版本号的属性,默认为0.0.0.1。
<PropertyGroup><BuildNumber Condition="'$(BuildNumber)'==''">0.0.0.1</BuildNumber></PropertyGroup>
请注意可以进一步打破这个,但你喜欢使用属性函数。我们使用它来获取Major编号,例如:<MajorVersionNumber>$(BuildNumber.Split('.')[0])</MajorVersionNumber>
,是的,这确实依赖于我们构建中的构建数字格式!
在项目中,您可以使用内部版本号属性在构建期间注入各种文件。您可以使用自定义构建任务(我使用&#39; sed&#39;并且此宏将版本号注入到.h文件中 - 对于任何基于文本的文件类型都可以这样做。)
如果您有更复杂的版本控制要求,则可以使用将构建号注入其他文件类型的自定义MSBuild目标。我已经完成了NuGet包的版本控制,我们的版本会自动为我们的公共库CS项目创建。
要按构建编号查询构建,可以在PowerShell中执行以下操作(安装Visual Studio Team Foundation Server电动工具):
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)
使用&#39; $ bld&#39;您现在可以查询该特定构建的所有属性。例如,要查看构建所基于的变更集,构建的状态,发起构建的情况,以及是否存在构建的搁置集:
$bld.Builds[0] | Ft -Property BuildFinished,RequestedFor,ShelvesetName,Status,SourceGetVersion -AutoSize
编辑:更正Powershell脚本中的拼写错误
答案 3 :(得分:2)
我下载了this script(感谢MrHinsh's answer),在源代码管理中签入了脚本,并在构建定义预构建脚本路径中指定了它:
然后我将内部版本号格式配置为&#34; $(BuildDefinitionName)_1.0.0 $(Rev:.r)&#34; (有关详细信息,请参阅MrHinsh's answer)。
令我惊讶的是。
答案 4 :(得分:0)
我正在处理一个类似但不完全相同的项目。主要版本和次要版本保存在AssemblyInfo中,因为任何标准的.net项目都会有它们。在我们的构建服务器上,我们有一个调用.sln构建的包装器MsBuild脚本,但它也执行一些设置任务,包括生成其他构建信息。此构建文件仅在我们的构建服务器上执行。通过Visual Studio构建的开发人员只会构建.sln,而不会获得其他行为。
答案 5 :(得分: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"/>
如您所知,此目标在编译之前执行。
答案 6 :(得分:0)
我使用[Major]。[Minor]。[BuildNumber]。[revision]
然后我可以追溯到一个构建,它将提供一个变更集,它将提供一个工作项等。
您可以使用社区构建任务,也可以使用自己的构建任务。
我也为MSI和DacPac的
做同样的事情基本上归属于assemblyinfo文件,然后使用正则表达式更新数字,在每日构建中将网络版本保留为相同的值并只更新文件版本,这样就可以保持兼容性
与MSI和Dacapac的不同位置相同的方法。在MSI中,我有一个Buildparams.wxi,它具有以下结构
<?xml version="1.0" encoding="utf-8"?>
<Include>
<?define ProductVersion="1.2.3.4"?>
</Include>
然后在wix脚本中将Productversion用作var.Productversion。 pre build我用我想要使用的内部版本号更新1.2.3.4
答案 7 :(得分:0)
<Target Name="GetTFSVersion" >
<Exec Command="$(TF) history /server:[tfs]\DefaultCollection"$/FP4WebServices/Staging" /stopafter:1 /recursive /login:domain\user,password /noprompt | find /V "Changeset" | find /V > $(LatestChangeSetTxtFile).tmp"/>
<Exec Command="FOR /F "eol=; tokens=1 delims=, " $(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\(".*"\)\]" ReplacementText="AssemblyFileVersion("$(Major).$(Minor).$(Build).$(ChangeSet)")]" />
</Target>