Build Versioning Strategies
Proper version management ensures traceability, reproducibility, and clear release history. This guide shows you how to implement versioning strategies in psake using semantic versioning, git tags, CI build numbers, and automated assembly updates.
Quick Start
Here's a basic versioning setup using git tags and build numbers:
Properties {
# Base version from git tag
$BaseVersion = Get-GitVersion
$BuildNumber = if ($env:BUILD_NUMBER) { $env:BUILD_NUMBER } else { '0' }
# Construct full version
$Version = "$BaseVersion.$BuildNumber"
}
function Get-GitVersion {
try {
$tag = git describe --tags --abbrev=0 2>$null
if ($tag -match '^v?(\d+\.\d+\.\d+)') {
return $matches[1]
}
}
catch { }
return '1.0.0'
}
Task Build {
Write-Host "Building version: $Version" -ForegroundColor Cyan
exec {
dotnet build -c Release /p:Version=$Version
}
}
Semantic Versioning (SemVer)
Semantic versioning (MAJOR.MINOR.PATCH) is the industry standard:
- MAJOR - Breaking changes (incompatible API changes)
- MINOR - New features (backward-compatible functionality)
- PATCH - Bug fixes (backward-compatible bug fixes)
Manual Semantic Versioning
Properties {
# Manually maintained version
$MajorVersion = 1
$MinorVersion = 5
$PatchVersion = 2
# CI build number
$BuildNumber = if ($env:BUILD_NUMBER) { $env:BUILD_NUMBER } else { '0' }
# Construct versions
$SemanticVersion = "$MajorVersion.$MinorVersion.$PatchVersion"
$AssemblyVersion = "$MajorVersion.$MinorVersion.0.0"
$FileVersion = "$MajorVersion.$MinorVersion.$PatchVersion.$BuildNumber"
$InformationalVersion = "$SemanticVersion+build.$BuildNumber"
}
Task ShowVersion {
Write-Host "Version Information:" -ForegroundColor Cyan
Write-Host " Semantic Version: $SemanticVersion" -ForegroundColor Gray
Write-Host " Assembly Version: $AssemblyVersion" -ForegroundColor Gray
Write-Host " File Version: $FileVersion" -ForegroundColor Gray
Write-Host " Informational Version: $InformationalVersion" -ForegroundColor Gray
}
Task Build {
exec {
dotnet build `
/p:Version=$SemanticVersion `
/p:AssemblyVersion=$AssemblyVersion `
/p:FileVersion=$FileVersion `
/p:InformationalVersion=$InformationalVersion
}
}
Pre-release Versions
Properties {
$MajorVersion = 2
$MinorVersion = 0
$PatchVersion = 0
$BuildNumber = if ($env:BUILD_NUMBER) { $env:BUILD_NUMBER } else { '0' }
# Determine pre-release label
$Branch = if ($env:BRANCH_NAME) { $env:BRANCH_NAME } else { 'develop' }
$PreReleaseLabel = switch -Regex ($Branch) {
'^main$|^master$' { '' } # Production release
'^release/.*' { 'rc' } # Release candidate
'^develop$' { 'beta' } # Beta release
'^feature/.*' { 'alpha' } # Alpha release
default { 'dev' } # Development build
}
# Construct version
if ([string]::IsNullOrEmpty($PreReleaseLabel)) {
$Version = "$MajorVersion.$MinorVersion.$PatchVersion"
} else {
$Version = "$MajorVersion.$MinorVersion.$PatchVersion-$PreReleaseLabel.$BuildNumber"
}
}
Task Build {
Write-Host "Building version: $Version" -ForegroundColor Cyan
Write-Host " Branch: $Branch" -ForegroundColor Gray
Write-Host " Pre-release: $PreReleaseLabel" -ForegroundColor Gray
exec {
dotnet build `
-c Release `
/p:Version=$Version `
/p:VersionPrefix="$MajorVersion.$MinorVersion.$PatchVersion" `
/p:VersionSuffix=$PreReleaseLabel
}
}
Git-Based Versioning
Derive versions from git tags and commit history:
Using Git Tags
function Get-GitVersion {
<#
.SYNOPSIS
Gets version from git tags
#>
try {
# Get the latest tag
$latestTag = git describe --tags --abbrev=0 2>$null
if ([string]::IsNullOrEmpty($latestTag)) {
Write-Warning "No git tags found, using default version"
return '0.1.0'
}
# Parse version from tag (handles v1.0.0 or 1.0.0)
if ($latestTag -match '^v?(\d+)\.(\d+)\.(\d+)') {
$major = [int]$matches[1]
$minor = [int]$matches[2]
$patch = [int]$matches[3]
# Get commits since tag
$commitsSinceTag = git rev-list "$latestTag..HEAD" --count 2>$null
if ($commitsSinceTag -gt 0) {
# Bump patch for commits since last tag
$patch++
return "$major.$minor.$patch-dev.$commitsSinceTag"
}
return "$major.$minor.$patch"
}
Write-Warning "Tag format not recognized: $latestTag"
return '0.1.0'
}
catch {
Write-Warning "Error getting git version: $_"
return '0.1.0'
}
}
Properties {
$GitVersion = Get-GitVersion
$BuildNumber = if ($env:BUILD_NUMBER) { $env:BUILD_NUMBER } else { '0' }
$Version = $GitVersion
}
Task Build {
Write-Host "Git-based version: $Version" -ForegroundColor Cyan
exec {
dotnet build -c Release /p:Version=$Version
}
}
Task CreateTag {
param(
[string]$TagVersion = '1.0.0',
[string]$Message = 'Release'
)
Write-Host "Creating git tag: v$TagVersion" -ForegroundColor Green
# Validate version format
if ($TagVersion -notmatch '^\d+\.\d+\.\d+$') {
throw "Invalid version format: $TagVersion (expected: MAJOR.MINOR.PATCH)"
}
# Create annotated tag
exec { git tag -a "v$TagVersion" -m $Message }
Write-Host "Tag created successfully. Push with: git push origin v$TagVersion" -ForegroundColor Yellow
}
Using GitVersion Tool
Properties {
$GitVersionExe = 'gitversion'
}
Task InstallGitVersion {
Write-Host "Installing GitVersion..." -ForegroundColor Green
exec { dotnet tool install --global GitVersion.Tool }
}
Task GetGitVersion {
Write-Host "Calculating version with GitVersion..." -ForegroundColor Green
# Run GitVersion and parse output
$versionJson = & $GitVersionExe | ConvertFrom-Json
# Extract version components
$script:Version = $versionJson.SemVer
$script:MajorMinorPatch = $versionJson.MajorMinorPatch
$script:InformationalVersion = $versionJson.InformationalVersion
$script:AssemblyVersion = $versionJson.AssemblySemVer
$script:FileVersion = $versionJson.AssemblySemFileVer
$script:NuGetVersion = $versionJson.NuGetVersionV2
Write-Host " SemVer: $Version" -ForegroundColor Cyan
Write-Host " NuGet: $NuGetVersion" -ForegroundColor Gray
Write-Host " Assembly: $AssemblyVersion" -ForegroundColor Gray
}
Task Build -depends GetGitVersion {
exec {
dotnet build `
/p:Version=$NuGetVersion `
/p:AssemblyVersion=$AssemblyVersion `
/p:FileVersion=$FileVersion `
/p:InformationalVersion=$InformationalVersion
}
}
GitVersion.yml:
mode: Mainline
branches:
main:
tag: ''
develop:
tag: 'beta'
feature:
tag: 'alpha.{BranchName}'
release:
tag: 'rc'
hotfix:
tag: 'hotfix'
ignore:
sha: []
CI Build Number Versioning
Leverage CI/CD build numbers:
GitHub Actions
Properties {
$BaseVersion = '1.0.0'
$BuildNumber = if ($env:GITHUB_RUN_NUMBER) { $env:GITHUB_RUN_NUMBER } else { '0' }
$GitSha = if ($env:GITHUB_SHA) { $env:GITHUB_SHA.Substring(0, 7) } else { 'local' }
# Construct version
$Version = "$BaseVersion.$BuildNumber"
$InformationalVersion = "$Version+$GitSha"
}
Task Build {
Write-Host "Building version $Version" -ForegroundColor Cyan
Write-Host " Build: $BuildNumber" -ForegroundColor Gray
Write-Host " Commit: $GitSha" -ForegroundColor Gray
exec {
dotnet build `
/p:Version=$Version `
/p:InformationalVersion=$InformationalVersion
}
}
.github/workflows/build.yml:
name: Build
on: [push]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Get full history for versioning
- name: Install psake
shell: pwsh
run: Install-Module -Name psake -Force
- name: Build
shell: pwsh
run: |
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build
env:
GITHUB_RUN_NUMBER: ${{ github.run_number }}
GITHUB_SHA: ${{ github.sha }}
Azure Pipelines
Properties {
$BaseVersion = '1.0.0'
$BuildNumber = if ($env:BUILD_BUILDNUMBER) { $env:BUILD_BUILDNUMBER } else { '0' }
$Version = "$BaseVersion.$BuildNumber"
}
Task Build {
Write-Host "Building version $Version" -ForegroundColor Cyan
# Update Azure Pipelines build number
if ($env:BUILD_BUILDNUMBER) {
Write-Host "##vso[build.updatebuildnumber]$Version"
}
exec {
dotnet build /p:Version=$Version
}
}
azure-pipelines.yml:
trigger:
- main
pool:
vmImage: 'windows-latest'
name: '1.0.$(Rev:r)'
steps:
- task: PowerShell@2
displayName: 'Build'
inputs:
targetType: 'inline'
script: |
Install-Module -Name psake -Force
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build
Assembly Version Updates
Automatically update project file versions:
Updating .NET Project Files
function Update-ProjectVersion {
param(
[string]$ProjectFile,
[string]$Version
)
if (-not (Test-Path $ProjectFile)) {
throw "Project file not found: $ProjectFile"
}
Write-Host "Updating version in $ProjectFile to $Version" -ForegroundColor Green
# Load project file
[xml]$project = Get-Content $ProjectFile
# Find or create PropertyGroup
$propertyGroup = $project.Project.PropertyGroup | Select-Object -First 1
if ($null -eq $propertyGroup) {
$propertyGroup = $project.CreateElement('PropertyGroup')
$project.Project.AppendChild($propertyGroup) | Out-Null
}
# Update or create version properties
$versionProperties = @(
'Version',
'AssemblyVersion',
'FileVersion'
)
foreach ($propName in $versionProperties) {
if ($null -eq $propertyGroup.$propName) {
$propNode = $project.CreateElement($propName)
$propertyGroup.AppendChild($propNode) | Out-Null
}
$propertyGroup.$propName = $Version
}
# Save updated project file
$project.Save($ProjectFile)
Write-Host " Version updated to: $Version" -ForegroundColor Gray
}
Task UpdateProjectVersions {
Write-Host "Updating project versions..." -ForegroundColor Green
$projects = Get-ChildItem "$SrcDir/**/*.csproj" -Recurse
foreach ($project in $projects) {
Update-ProjectVersion -ProjectFile $project.FullName -Version $Version
}
Write-Host "Updated $($projects.Count) project files" -ForegroundColor Green
}
Task Build -depends UpdateProjectVersions {
exec { dotnet build -c Release }
}
Updating AssemblyInfo.cs (Legacy)
function Update-AssemblyInfo {
param(
[string]$AssemblyInfoPath,
[string]$Version
)
if (-not (Test-Path $AssemblyInfoPath)) {
throw "AssemblyInfo.cs not found: $AssemblyInfoPath"
}
Write-Host "Updating AssemblyInfo: $AssemblyInfoPath" -ForegroundColor Green
$content = Get-Content $AssemblyInfoPath
# Update version attributes
$content = $content -replace '\[assembly: AssemblyVersion\(".*?"\)\]', "[assembly: AssemblyVersion(""$Version"")]"
$content = $content -replace '\[assembly: AssemblyFileVersion\(".*?"\)\]', "[assembly: AssemblyFileVersion(""$Version"")]"
$content = $content -replace '\[assembly: AssemblyInformationalVersion\(".*?"\)\]', "[assembly: AssemblyInformationalVersion(""$Version"")]"
Set-Content -Path $AssemblyInfoPath -Value $content
Write-Host " Updated to version: $Version" -ForegroundColor Gray
}
Task UpdateAssemblyInfoFiles {
$assemblyInfoFiles = Get-ChildItem "$SrcDir/**/AssemblyInfo.cs" -Recurse
foreach ($file in $assemblyInfoFiles) {
Update-AssemblyInfo -AssemblyInfoPath $file.FullName -Version $Version
}
}
Updating package.json (Node.js)
function Update-PackageVersion {
param(
[string]$PackageJsonPath,
[string]$Version
)
if (-not (Test-Path $PackageJsonPath)) {
throw "package.json not found: $PackageJsonPath"
}
Write-Host "Updating package.json version to $Version" -ForegroundColor Green
$package = Get-Content $PackageJsonPath | ConvertFrom-Json
$package.version = $Version
$package | ConvertTo-Json -Depth 100 | Set-Content $PackageJsonPath
Write-Host " Updated package.json" -ForegroundColor Gray
}
Task UpdateNodeVersion {
$packageJson = Join-Path $ProjectRoot 'package.json'
Update-PackageVersion -PackageJsonPath $packageJson -Version $Version
}
Complete Versioning Example
psakefile.ps1:
Properties {
$ProjectRoot = $PSScriptRoot
$SrcDir = Join-Path $ProjectRoot 'src'
$BuildDir = Join-Path $ProjectRoot 'build/output'
# Version configuration
$MajorVersion = 1
$MinorVersion = 0
$PatchVersion = 0
# CI/CD integration
$BuildNumber = if ($env:BUILD_NUMBER) { $env:BUILD_NUMBER } else { '0' }
$GitSha = Get-GitCommitSha
$Branch = Get-GitBranch
# Determine version based on branch
$Version = Get-BuildVersion
}
function Get-GitCommitSha {
try {
$sha = git rev-parse --short HEAD 2>$null
return if ($sha) { $sha } else { 'unknown' }
}
catch {
return 'unknown'
}
}
function Get-GitBranch {
try {
if ($env:BRANCH_NAME) {
return $env:BRANCH_NAME
}
$branch = git rev-parse --abbrev-ref HEAD 2>$null
return if ($branch) { $branch } else { 'unknown' }
}
catch {
return 'unknown'
}
}
function Get-BuildVersion {
$baseVersion = "$MajorVersion.$MinorVersion.$PatchVersion"
# Determine pre-release label
$preRelease = switch -Regex ($Branch) {
'^main$|^master$' {
# Production release
return "$baseVersion.$BuildNumber"
}
'^release/.*' {
# Release candidate
return "$baseVersion-rc.$BuildNumber"
}
'^develop$' {
# Beta release
return "$baseVersion-beta.$BuildNumber"
}
'^hotfix/.*' {
# Hotfix release
return "$baseVersion-hotfix.$BuildNumber"
}
default {
# Development/feature build
$safeBranch = $Branch -replace '[^a-zA-Z0-9]', '-'
return "$baseVersion-dev.$safeBranch.$BuildNumber"
}
}
return $preRelease
}
FormatTaskName {
param($taskName)
Write-Host ""
Write-Host "Executing: $taskName" -ForegroundColor Cyan
Write-Host ("=" * 80) -ForegroundColor Gray
}
Task Default -depends Build
Task ShowVersion {
Write-Host ""
Write-Host "Version Information" -ForegroundColor Cyan
Write-Host ("=" * 80) -ForegroundColor Gray
Write-Host " Version: $Version" -ForegroundColor White
Write-Host " Base: $MajorVersion.$MinorVersion.$PatchVersion" -ForegroundColor Gray
Write-Host " Build Number: $BuildNumber" -ForegroundColor Gray
Write-Host " Git SHA: $GitSha" -ForegroundColor Gray
Write-Host " Branch: $Branch" -ForegroundColor Gray
Write-Host ("=" * 80) -ForegroundColor Gray
Write-Host ""
}
Task UpdateVersions -depends ShowVersion {
Write-Host "Updating project versions..." -ForegroundColor Green
# Update .NET projects
$projects = Get-ChildItem "$SrcDir/**/*.csproj" -Recurse
foreach ($project in $projects) {
[xml]$proj = Get-Content $project.FullName
$propertyGroup = $proj.Project.PropertyGroup | Select-Object -First 1
if ($null -eq $propertyGroup.Version) {
$versionNode = $proj.CreateElement('Version')
$propertyGroup.AppendChild($versionNode) | Out-Null
}
$propertyGroup.Version = $Version
$proj.Save($project.FullName)
Write-Host " Updated: $($project.Name)" -ForegroundColor Gray
}
Write-Host "Version updates complete" -ForegroundColor Green
}
Task Clean {
Write-Host "Cleaning build artifacts..." -ForegroundColor Green
if (Test-Path $BuildDir) {
Remove-Item $BuildDir -Recurse -Force
}
New-Item -ItemType Directory -Path $BuildDir | Out-Null
}
Task Build -depends UpdateVersions, Clean {
Write-Host "Building version $Version..." -ForegroundColor Green
exec {
dotnet build $SrcDir `
-c Release `
-o $BuildDir `
/p:Version=$Version `
/p:InformationalVersion="$Version+$GitSha"
}
Write-Host "Build complete: $BuildDir" -ForegroundColor Green
}
Task Pack -depends Build {
Write-Host "Creating NuGet packages..." -ForegroundColor Green
exec {
dotnet pack $SrcDir `
-c Release `
-o $BuildDir `
--no-build `
/p:PackageVersion=$Version
}
$packages = Get-ChildItem "$BuildDir/*.nupkg"
Write-Host "Created $($packages.Count) package(s)" -ForegroundColor Green
}
Task Tag {
param([string]$TagVersion)
if ([string]::IsNullOrEmpty($TagVersion)) {
$TagVersion = "$MajorVersion.$MinorVersion.$PatchVersion"
}
Write-Host "Creating git tag: v$TagVersion" -ForegroundColor Green
# Ensure we're on main/master
if ($Branch -notmatch '^(main|master)$') {
throw "Tags should only be created from main/master branch (current: $Branch)"
}
# Check if tag already exists
$existingTag = git tag -l "v$TagVersion"
if ($existingTag) {
throw "Tag v$TagVersion already exists"
}
# Create annotated tag
exec { git tag -a "v$TagVersion" -m "Release $TagVersion" }
Write-Host "Tag created: v$TagVersion" -ForegroundColor Green
Write-Host "Push tag with: git push origin v$TagVersion" -ForegroundColor Yellow
}
Best Practices
- Use semantic versioning - Follow MAJOR.MINOR.PATCH conventions
- Tag releases in git - Create git tags for all releases
- Include build metadata - Add commit SHA and build number to informational version
- Automate version bumps - Don't manually edit version numbers
- Use pre-release labels - Distinguish beta, alpha, and RC versions
- Keep version in one place - Single source of truth for version number
- Version all artifacts - DLLs, packages, containers should all have same version
- Document version strategy - Team should understand versioning scheme
- Test version updates - Ensure version updates don't break builds
- Archive version history - Maintain changelog with version history
See Also
- GitHub Actions - CI/CD with versioning
- Azure Pipelines - Azure DevOps versioning
- Node.js Builds - Versioning Node.js packages
- .NET Solution Builds - .NET versioning
- Organizing Large Scripts - Version utilities organization