About
The Ubiquity.NET.Versioning.Build.Tasks package provides automated support for build versioning using a Constrained Semantic Version (CSemVer).
Warning
As a Breaking change in .NET SDK 8
is now setting the build meta data for the InformationalVersion
property automatically and
without user consent. (A Highly controversial choice that was more easily handled via an
OPT-IN pattern) Unfortunately, this was set ON by default and made into an 'OPT-OUT' scenario.
This library will honor such a setting and does not alter/interfere with it in any way.
(Though the results can, unfortunately, produce surprising behavior as it is not well
documented).
If you wish to disable this behavior you can set an MSBUILD property to OPT-OUT as follows:
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
This choice of ignoring the additional data is considered to have the least impact on those who are aware of the change and those who use this library to set an explicit build meta data string. (Principle of least surprise for what this library can control).
The default behavior added in this breaking change is to use the Repository ID (usually a GIT
commit hash [FULL SHA form!]) as the build metadata. This is appended with a leading +
if
one isn't already in the InformationalVersion
property. If build metadata is already
included (Like from use of this task) the id is appended using a .
instead. So it is ALWAYS
appended unless the project has opted out of this behavior by setting the property as
previously described.
Thus, it is strongly recommended that projects using this package OPT-OUT of the new behavior.
Overview
Officially, SemVer 2.0 doesn't consider or account for publicly available CI builds. SemVer is only concerned with official releases. This makes CI builds producing versioned packages challenging. Fortunately, someone has already defined a solution to using SemVer in a specially constrained way to ensure compatibility, while also allowing for automated CI builds. These new versions are called a Constrained Semantic Version (CSemVer and CSemVer-CI).
A CSemVer is unique for each build and always increments while still supporting official releases. However, in the real world, there are often cases where there are additional builds that are distinct from official releases and formal CI builds. These include local developer builds and builds generated from a Pull Request (a.k.a Automated buddy build). Neither SemVer, nor CSemVer explicitly define any format for these cases. So this library defines a pattern of versioning that is fully compatible with CSemVer[-CI] and allows for the additional build types in a way that retains precedence having the least surprising consequences. In particular, local build versions have a higher precedence than automated or release versions if all other components of the version match1. This ensures that what you are building includes the dependent components you just built instead of the last one released publicly.
The following is a list of the version formats in descending order of precedence:
Build Type | Format |
---|---|
Local build | {BuildMajor}.{BuildMinor}.{BuildPatch}.ci.{CiBuildIndex}.ZZZ+{BuildMeta} |
Pull Request | {BuildMajor}.{BuildMinor}.{BuildPatch}.ci.{CiBuildIndex}.PRQ+{BuildMeta} |
Official CI builds | {BuildMajor}.{BuildMinor}.{BuildPatch}.ci-{CiBuildIndex}.BLD+{BuildMeta} |
Official PreRelease | {BuildMajor}.{BuildMinor}.{BuildPatch}-{PreReleaseName}[.PreReleaseNumber][.PreReleaseFix]+{BuildMeta} |
Official Release | {BuildMajor}.{BuildMinor}.{BuildPatch}+{BuildMeta} |
That is the, CI BuildName (ZZZ
, PRQ
, BLD
) is specifically chosen to ensure the ordering
matches the expected behavior for a build while still making sense for most uses.
This project provides an MSBUILD task to automate the generation of these versions in an easy to use NuGet Package.
The package creates File and Assembly Versions and defines the appropriate MsBuild properties so the build will automatically incorporate them.
NOTE:
The automatic use of MsBuild properties requires using the new SDK attribute support for .NET projects. Where the build auto generates the assembly info. If you are using some other means to auto generate the assembly level versioning attributes. You can use the properties generated by this package to generate the attributes.
File and AssemblyVersions are computed based on the CSemVer "Ordered version", which is a 64 bit value that maps to a standard windows FILE VERSION Quad with each part consuming 16 bits. This ensures a strong relationship between the assembly/file versions and the packages as well as ensures that CI builds can function properly. Furthermore, this guarantees that each build has a different file and assembly version so that strong name signing functions properly to enable loading different versions in the same process.
Important
A file version quad representation of a CSemVer does NOT carry with it the CI information nor any build metadata. It only contains a single bit to indicate a Release vs. a CI build. In fact, the official CSemVer specs are silent on the use of this bit though the "playground" does indicate an ODD numbered revision is reserved as a CI build.
The Major, Minor and Patch versions are only updated in the primary branch at the time of a release. This ensures the concept that SemVer versions define released products. The version numbers used are stored in the repository in the BuildVersion.xml
Properties used to determine the version
CSemVer.Build uses MSBuild properties to determine the final version number.
Name | Default Value | Description |
---|---|---|
BuildMajor | Read from BuildVersion.xml | Major portion of the build number |
BuildMinor | Read from BuildVersion.xml | Minor portion of the build number |
BuildPatch | Read from BuildVersion.xml | Patch portion of the build number |
PreReleaseName | <Undefined> or value read from BuildVersion.xml if present |
PreRelease Name of the CSemVer |
PreReleaseNumber | <Undefined> or value read from BuildVersion.xml if present |
PreRelease Number of the CSemVer |
PreReleaseFix | <Undefined> or value read from BuildVersion.xml if present |
PreRelease Fix of the CSemVer |
BuildMeta | <undefined> |
Build meta for the version |
CiBuildIndex | ISO 8601 formated UTC time-stamp for the build | Provides a unique build to build value guaranteed to increase with each build |
CiBuildName | <see notes> |
CSemVer CI name |
GeneratedVersionInfoHeader | <undefined> |
Full path of the header file to generate [No header generated if not set] [Only applies to a VCXPROJ file ] |
GenerateAssemblyInfo | <undefined> |
If set, creates $(IntermediateOutputPath)AssemblyVersionInfo.g.cs and includes it for compilation. Generally, only used for legacy CSPROJ files as new SDK projects handle this automatically now |
CiBuildIndex
Unless specified or a release build [$(IsReleaseBuild) is true] this indicates the CSemVer CI Build index for a build. For a CI build this will default to the ISO-8601 formatted time stamp of the build. Consumers can specify any value desired as an override but should ensure the value is ALWAYS increasing according to the rules of a CSemVer-CI. Generating duplicates for the same build is an error condition and can lead to consumer confusion. Usually, if set externally, this is set to the time stamp of the head commit of a repository so that any automated builds are consistent with the build number. (For PullRequest buddy builds this is usually left as a build time stamp)
CiBuildName
Unless explicitly provided, the CiBuildName is determined by a set of properties that indicate the nature of the build. The properties used (in evaluation order) are:
Name | Default Value | CiBuildName | Description |
---|---|---|---|
IsPullRequestBuild | <Undefined> |
PRQ if true |
Used to indicate if the build is from a pull request |
IsAutomatedBuild | <Undefined> |
BLD if true |
Used to indicate if the build is an automated build |
IsReleaseBuild | <Undefined> |
ZZZ if !true |
Used to indicate if the build is an official release build |
These three values are determined by the automated build in some form. These are either
explicit variables set for the build definition or determined on the fly based on values set by
the build. Commonly a directory.build.props
for a repository will specify these. The
following is an example for setting them based on an AppVeyor build in the
Directory.Build.props
file:
<PropertyGroup>
<!-- If running in APPVEYOR it is an automated build -->
<IsAutomatedBuild Condition="'$(IsAutomatedBuild)'=='' AND '$(APPVEYOR)'!=''">true</IsAutomatedBuild>
<IsAutomatedBuild Condition="'$(IsAutomatedBuild)'==''">false</IsAutomatedBuild>
<!-- If it has a PR number associated it is a PR build -->
<IsPullRequestBuild Condition="'$(IsPullRequestBuild)'=='' AND '$(APPVEYOR_PULL_REQUEST_NUMBER)'!=''">true</IsPullRequestBuild>
<IsPullRequestBuild Condition="'$(IsPullRequestBuild)'==''">false</IsPullRequestBuild>
<!-- Tags applied without a PR are release builds -->
<IsReleaseBuild Condition="'$(IsReleaseBuild)'=='' AND '$(APPVEYOR_REPO_TAG)'=='true' AND '$(APPVEYOR_PULL_REQUEST_NUMBER)'==''">true</IsReleaseBuild>
<IsReleaseBuild Condition="'$(IsReleaseBuild)'==''">false</IsReleaseBuild>
</PropertyGroup>
Commonly a build is scripted in a build service such as GitHub Actions or AppVeyor. Such scripting should set these values based on conditions from the back end system. An example of this for common systems like GitHub and AppVeyor could look like this:
enum BuildKind
{
LocalBuild
PullRequestBuild
CiBuild
ReleaseBuild
}
function Get-CurrentBuildKind
{
<#
.SYNOPSIS
Determines the kind of build for the current environment
.DESCRIPTION
This function retrieves environment values set by various automated builds
to determine the kind of build the environment is for. The return is one of
the [BuildKind] enumeration values:
| Name | Description |
|------------------|-------------|
| LocalBuild | This is a local developer build (e.g. not an automated build)
| PullRequestBuild | This is a build from a PullRequest with untrusted changes, so build should limit the steps appropriately |
| CiBuild | This build is from a Continuous Integration (CI) process, usually after a PR is accepted and merged to the branch |
| ReleaseBuild | This is an official release build, the output is ready for publication (Automated builds may use this to automatically publish) |
#>
[OutputType([BuildKind])]
param()
$currentBuildKind = [BuildKind]::LocalBuild
# IsAutomatedBuild is the top level gate (e.g. if it is false, all the others must be false)
# This supports identification of APPVEYOR or GitHub explicitly but also supports the common
# `CI` environment variable. Additional build back-ends that don't set the env var therefore,
# would need special handling here.
$isAutomatedBuild = [System.Convert]::ToBoolean($env:CI) `
-or [System.Convert]::ToBoolean($env:APPVEYOR) `
-or [System.Convert]::ToBoolean($env:GITHUB_ACTIONS)
if( $isAutomatedBuild )
{
# PR and release builds have externally detected indicators that are tested
# below, so default to a CiBuild (e.g. not a PR, And not a RELEASE)
$currentBuildKind = [BuildKind]::CiBuild
# Based on back-end type - determine if this is a release or CI build
# The assumption here is that a TAG is pushed to the repo for releases
# and therefore that is what distinguishes a release build. Other conditions
# would need to use other criteria to determine a PR buddy build, CI build
# and release build.
# IsPullRequestBuild indicates an automated buddy build and should not be trusted
$isPullRequestBuild = $env:GITHUB_BASE_REF -or $env:APPVEYOR_PULL_REQUEST_NUMBER
if($isPullRequestBuild)
{
$currentBuildKind = [BuildKind]::PullRequestBuild
}
else
{
if([System.Convert]::ToBoolean($env:APPVEYOR))
{
$isReleaseBuild = $env:APPVEYOR_REPO_TAG
}
elseif([System.Convert]::ToBoolean($env:GITHUB_ACTIONS))
{
$isReleaseBuild = $env:GITHUB_REF -like 'refs/tags/*'
}
if($isReleaseBuild)
{
$currentBuildKind = [BuildKind]::ReleaseBuild
}
}
}
return $currentBuildKind
}
$currentBuildKind = Get-CurrentBuildKind
# set/reset legacy environment vars for non-script tools
$env:IsAutomatedBuild = $currentBuildKind -ne [BuildKind]::LocalBuild
$env:IsPullRequestBuild = $currentBuildKind -eq [BuildKind]::PullRequestBuild
$env:IsReleaseBuild = $currentBuildKind -eq [BuildKind]::ReleaseBuild
BuildVersion.xml
If the MSBuild property BuildMajor
is not set, then the base build version is read from the
repository file specified in the BuildVersion.xml, typically this is located at the root of a
repository so that any child projects share the same versioning information. The location of
the file is specified by an MSBuild property BuildVersionXml
. The contents of the file are
fairly simple and only requires a single BuildVersionData
element with a set of attributes.
The available attributes are:
Name | Description |
---|---|
BuildMajor | Major portion of the build number |
BuildMinor | Minor portion of the build number |
BuildPatch | Patch portion of the build number |
PreReleaseName | PreRelease Name of the CSemVer |
PreReleaseNumber | PreRelease Number of the CSemVer |
PreReleaseFix | PreRelease Fix of the CSemVer |
Only the Major, minor and Patch numbers are required. Example:
<BuildVersionData
BuildMajor = "5"
BuildMinor = "0"
BuildPatch = "0"
PreReleaseName = "alpha"
/>
Generated Properties
Name | Description |
---|---|
BuildTime | Set to the current time (UTC ISO-8601 format) if not already set by build tooling) |
IsAutomatedBuild | Automated build system value to indicate this is an automated build |
IsPullRequestBuild | Automated build system value to indicate this is a build for an untrusted PR |
IsReleaseBuild | Automated build system value to indicate this is an official release build (No CI information) |
CiBuildName | If not set externally, this is set based on the kind of build |
CiBuildIndex | If not set externally, this is set based on the $(BuildTime) property by parsing the ISO-8601 string and computing an index from that |
BuildMajor | Major portion of the build; If not set externally, this is set based on the information in the $(BuildVersionXml) file |
BuildMinor | Minor portion of the build; If not set externally, this is set based on the information in the $(BuildVersionXml) file |
BuildPatch | Patch portion of the build; If not set externally, this is set based on the information in the $(BuildVersionXml) file |
PreReleaseName | PreRelease Name for the build [Optional]; If not set externally, this is set based on the information in the $(BuildVersionXml) file, which may not include a value for this |
PreReleaseNumber | PreRelease Number for the build [Optional]; If not set externally, this is set based on the information in the $(BuildVersionXml) file, which may not include a value for this |
PreReleaseFix | PreRelease Fix for the build [Optional]; If not set externally, this is set based on the information in the $(BuildVersionXml) file, which may not include a value for this |
FullBuildNumber | String form of the full CSemVer value for a build |
ShortBuildNumber | Short form of the CSemVer for use with legacy NuGet clients (Modern clients support the full name) |
FileVersionMajor | Major portion of the FileVersion number (Used for vcxproj files to generate the version header) |
FileVersionMinor | Minor portion of the FileVersion number (Used for vcxproj files to generate the version header) |
FileVersionBuild | Build portion of the FileVersion number (Used for vcxproj files to generate the version header) |
FileVersionRevision | Revision portion of the FileVersion number (Used for vcxproj files to generate the version header) |
BuildTime
Ordinarily this is set for an entire solution by build scripting to ensure that all components using this build task report the same version number. If it is not set the current time at the moment of property evaluation for a project is used. This will result in a distinct CI version for each project in a solution. Whether, that is desired or not is left for the consumer. If it is not desired, then a centralized setting as a build property or environment variable is warranted.
Automated build flags
IsAutomatedBuild
, IsPullRequestBuild
, and IsReleaseBuild
are normally set by an automated build script/action
based on the build environment used and aid in determining the CI build name as previously described in
CiBuildName.
CiBuildName
If not explicitly set this is determined by the automated build flags as described in the CiBuildName section of this document.
Detected Error Conditions
Targets file
Code | Description |
---|---|
CSM001 | BuildMajor is a required property, either set it as a global or in the build version XML |
CSM002 | BuildMinor is a required property, either set it as a global or in the build version XML |
CSM003 | BuildPatch is a required property, either set it as a global or in the build version XML |
CSM004 | FileVersion property not provided AND FileVersionMajor property not found to create it from |
CSM005 | FileVersion property not provided AND FileVersionMinor property not found to create it from |
CSM006 | FileVersion property not provided AND FileVersionBuild property not found to create it from |
CSM007 | FileVersion property not provided AND FileVersionRevision property not found to create it from |
CreateVersionInfo Task
Code | Description |
---|---|
CSM100 | BuildMajor value must be in range [0-99999] |
CSM101 | BuildMinor value must be in range [0-49999] |
CSM102 | BuildPatch value must be in range [0-9999] |
CSM103 | PreReleaseName is unknown |
CSM104 | PreReleaseNumber value must be in range [0-99] |
CSM105 | PreReleaseFix value must be in range [0-99] |
CSM1062 | If CiBuildIndex is set then CiBuildName must also be set; If CiBuildIndex is NOT set then CiBuildName must not be set. |
CSM107 | CiBuildIndex does not match syntax defined by CSemVer |
CSM108 | CiBuildName does not match syntax defined by CSemVer |
ParseBuildVersionXml Task
Code | Description |
---|---|
CSM200 | BuildVersionXml is required and must not be all whitespace |
CSM201 | Specified BuildVersionXml does not exist $(BuildVersionXml) |
CSM202 | BuildVersionData element does not exist in $(BuildVersionXml) |
CSM203 | [Warning] Unexpected attribute on BuildVersionData Element |
CSM204 | XML format of file specified by `$(BuildVersionXml)' is invalid |
1 CSemVer-CI uses a latest build format and therefore all
version numbers for a CI build, including local builds use a Patch+1
pattern as defined by
CSemVer-CI. This ensures that all forms of CI builds have a higher precedence than any release
they are based on. To clarify the understanding of that, a CI build contains everything in a
given release and then some more. How much more, or what release that will eventually become
is intentionally undefined. This allows selection of the final release version based on the
contents of the actual changes. CI builds are understood unstable and subject to complete
changes or even abandoned lines of thought.
2 CSM106 is essentially an internal sanity test. The
props/targets files ensure that $(CiBuildIndex)
and $(CiBuildName)
have a value unless
$(IsReleaseBuild) is set. In that case the targets file will force them to empty. So, there's
no way to test for or hit this condition without completely replacing/bypassing the props/targets
files for the task. Which is obviously, an unsupported scenario 😁.