Modules/businessdev.ALbuild.Core/Public/Invoke-BcBuildVersionStamp.ps1
|
function Invoke-BcBuildVersionStamp { <# .SYNOPSIS Stamps a build version into app.json and atomically claims a build branch for it. .DESCRIPTION The pipeline-level versioning step. It combines version stamping (Set-BcAppVersion) and build branch creation (New-BcBuildBranch) into one operation that is safe when several builds run in parallel - the classic race where two simultaneous jobs compute the same version. Concurrency is solved with a branch-push compare-and-swap: the build branch name encodes the version, so pushing it (without --force) is an atomic claim on the remote. If the push is rejected because the branch already exists (another build claimed that version), the version is incremented and the claim is retried, up to -MaxAttempts. Any other push failure throws. With -NoBuildBranch the version is written but nothing is committed or pushed (used by the local pipeline runner so a developer's run never claims a version or touches the remote). An explicit -Version is stamped as-is and claimed once (a conflict throws rather than retrying). .PARAMETER Path Repository root to search for app.json files, an app folder, or a single app.json. Default: the current location. .PARAMETER RepositoryRoot The git working tree. Defaults to -Path when it is a folder, otherwise its parent. .PARAMETER Schema Token schema for computing the version (see Set-BcAppVersion). Default 'major.minor.increment.0'. .PARAMETER Version An explicit version (overrides -Schema); stamped as-is and claimed once. .PARAMETER BuildId Value for the 'build-id' schema token. Defaults to BUILD_BUILDID, or '0'. .PARAMETER OnlyUpdateOnChangedSource Only stamp apps whose folder changed in the last commit; if nothing changed, no branch is claimed. .PARAMETER BranchPrefix Build branch name prefix. Default 'build/'. .PARAMETER Remote Remote to claim the branch on. Default 'origin'. .PARAMETER MaxAttempts Maximum claim attempts before giving up. Default 10. .PARAMETER NoBuildBranch Stamp only; do not commit, branch or push (dry-run for local runs). .PARAMETER UserName git identity name for the commit. Default 'ALbuild CI'. .PARAMETER UserEmail git identity email for the commit. Default 'albuild@365businessdev.com'. .EXAMPLE Invoke-BcBuildVersionStamp -Path . -Schema 'major.minor.increment.0' .EXAMPLE Invoke-BcBuildVersionStamp -Path . -NoBuildBranch # local dry-run .OUTPUTS PSCustomObject: Version, Branch, Attempts, Claimed, Committed, Apps. #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Schema')] [OutputType([PSCustomObject])] param( [Parameter(Position = 0)] [string] $Path = (Get-Location).Path, [string] $RepositoryRoot, [Parameter(ParameterSetName = 'Schema')] [ValidateNotNullOrEmpty()] [string] $Schema = 'major.minor.increment.0', [Parameter(Mandatory, ParameterSetName = 'Explicit')] [ValidateNotNullOrEmpty()] [string] $Version, [string] $BuildId, [switch] $OnlyUpdateOnChangedSource, [string] $BranchPrefix = 'build/', [string] $Remote = 'origin', [ValidateRange(1, [int]::MaxValue)] [int] $MaxAttempts = 10, [switch] $NoBuildBranch, [string] $UserName = 'ALbuild CI', [string] $UserEmail = 'albuild@365businessdev.com' ) if (-not (Test-Path -LiteralPath $Path)) { throw "Path '$Path' does not exist." } if (-not $RepositoryRoot) { $item = Get-Item -LiteralPath $Path $RepositoryRoot = if ($item.PSIsContainer) { $item.FullName } else { $item.Directory.FullName } } if (-not $PSBoundParameters.ContainsKey('BuildId') -or [string]::IsNullOrEmpty($BuildId)) { $BuildId = if ($env:BUILD_BUILDID) { $env:BUILD_BUILDID } else { '0' } } $isExplicit = $PSCmdlet.ParameterSetName -eq 'Explicit' # Helper: pick the highest version across the stamped apps as the build version. function Get-HighestVersion([object[]] $Stamped) { ($Stamped | ForEach-Object { ConvertTo-BcVersion $_.NewVersion } | Sort-Object -Descending | Select-Object -First 1) } # --- Initial stamp --------------------------------------------------------------------------- $stampArgs = @{ Path = $Path; BuildId = $BuildId } if ($OnlyUpdateOnChangedSource) { $stampArgs['OnlyUpdateOnChangedSource'] = $true } if ($isExplicit) { $stampArgs['Version'] = $Version } else { $stampArgs['Schema'] = $Schema } $apps = @(Set-BcAppVersion @stampArgs) $current = Get-HighestVersion $apps if (-not $current) { throw 'No version could be determined from the stamped apps.' } if ($OnlyUpdateOnChangedSource -and -not ($apps | Where-Object { $_.Changed })) { Write-ALbuildLog -Level Information 'No app source changed; skipping the build branch claim.' return [PSCustomObject]@{ Version = $current.ToString(); Branch = $null; Attempts = 0; Claimed = $false; Committed = $false; Apps = $apps } } if ($NoBuildBranch) { Write-ALbuildLog -Level Information "Stamped version $current (no build branch; -NoBuildBranch)." return [PSCustomObject]@{ Version = $current.ToString(); Branch = $null; Attempts = 0; Claimed = $false; Committed = $false; Apps = $apps } } if (-not $PSCmdlet.ShouldProcess("$Remote/$BranchPrefix$current", 'Claim build branch')) { return [PSCustomObject]@{ Version = $current.ToString(); Branch = "$BranchPrefix$current"; Attempts = 0; Claimed = $false; Committed = $false; Apps = $apps } } # --- Claim loop (compare-and-swap on the build branch) --------------------------------------- $branchArgs = @{ RepositoryRoot = $RepositoryRoot; BranchPrefix = $BranchPrefix; Remote = $Remote; UserName = $UserName; UserEmail = $UserEmail } for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) { $branch = New-BcBuildBranch -Version $current.ToString() @branchArgs if ($branch.Pushed) { Write-ALbuildLog -Level Success "Claimed build version $current on attempt $attempt." return [PSCustomObject]@{ Version = $current.ToString(); Branch = $branch.Branch; Attempts = $attempt; Claimed = $true; Committed = $branch.Committed; Apps = $apps } } # Only a conflict (ref already claimed) reaches here; hard failures threw inside New-BcBuildBranch. if ($isExplicit) { throw "Build version $Version is already claimed on '$Remote'. Choose a different version." } $next = [version]("{0}.{1}.{2}.{3}" -f $current.Major, $current.Minor, ([Math]::Max($current.Build, 0) + 1), [Math]::Max($current.Revision, 0)) Write-ALbuildLog -Level Warning "Version $current was already claimed; retrying as $next." $current = $next $apps = @(Set-BcAppVersion -Path $Path -Version $current.ToString() -BuildId $BuildId) } throw "Could not claim a build version after $MaxAttempts attempt(s) on '$Remote'." } |