Modules/businessdev.ALbuild.Core/Public/New-BcBuildBranch.ps1

function New-BcBuildBranch {
    <#
    .SYNOPSIS
        Commits the working tree to a build/<version> branch and (optionally) pushes it.
 
    .DESCRIPTION
        Creates (or resets) a branch named "<BranchPrefix><Version>" at the current HEAD, stages all
        changes, commits them when there is something to commit, and pushes the branch to the remote.
        This is the carrier of the versioned sources produced by a build; the parallel-build lock is
        handled separately by Invoke-BcBuildVersionStamp.
 
        Pushing requires that the checkout step persisted credentials (e.g. Azure DevOps
        'persistCredentials: true'). A push that is rejected because the branch already exists with
        divergent history is reported as a non-terminating conflict (the returned object's Pushed is
        $false and a warning is logged) rather than thrown, so a caller doing its own concurrency
        control can react; any other push failure (auth, missing remote) throws.
 
    .PARAMETER Version
        The version that names the branch.
 
    .PARAMETER RepositoryRoot
        The git working tree. Default: the current location.
 
    .PARAMETER BranchPrefix
        Prefix for the branch name. Default 'build/'.
 
    .PARAMETER CommitMessage
        Commit message. Default "Build <Version>".
 
    .PARAMETER UserName
        git author/committer name set locally for the commit. Default 'ALbuild CI'.
 
    .PARAMETER UserEmail
        git author/committer email set locally for the commit. Default 'albuild@365businessdev.com'.
 
    .PARAMETER Remote
        The remote to push to. Default 'origin'.
 
    .PARAMETER Force
        Force-push the branch (overwrites an existing remote branch). Use with care.
 
    .PARAMETER NoPush
        Create and commit the branch locally without pushing (used by the local pipeline runner).
 
    .EXAMPLE
        New-BcBuildBranch -Version '2.3.0.0'
 
    .OUTPUTS
        PSCustomObject: Branch, Committed, Pushed, Conflict.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string] $Version,
        [string] $RepositoryRoot = (Get-Location).Path,
        [string] $BranchPrefix = 'build/',
        [string] $CommitMessage,
        [string] $UserName = 'ALbuild CI',
        [string] $UserEmail = 'albuild@365businessdev.com',
        [string] $Remote = 'origin',
        [switch] $Force,
        [switch] $NoPush
    )

    if (-not (Test-Path -LiteralPath $RepositoryRoot)) { throw "Repository root '$RepositoryRoot' does not exist." }
    $branch = "$BranchPrefix$Version"
    if (-not $CommitMessage) { $CommitMessage = "Build $Version" }

    if (-not $PSCmdlet.ShouldProcess($branch, 'Create build branch')) {
        return [PSCustomObject]@{ Branch = $branch; Committed = $false; Pushed = $false; Conflict = $false }
    }

    Invoke-BcGit -RepositoryRoot $RepositoryRoot -Arguments @('config', 'user.name', $UserName) | Out-Null
    Invoke-BcGit -RepositoryRoot $RepositoryRoot -Arguments @('config', 'user.email', $UserEmail) | Out-Null
    Invoke-BcGit -RepositoryRoot $RepositoryRoot -Arguments @('checkout', '-B', $branch) | Out-Null
    Invoke-BcGit -RepositoryRoot $RepositoryRoot -Arguments @('add', '-A') | Out-Null

    # 'diff --cached --quiet' exits 0 when nothing is staged, 1 when there are staged changes.
    $staged = Invoke-BcGit -RepositoryRoot $RepositoryRoot -Arguments @('diff', '--cached', '--quiet') -AllowFailure
    $committed = $false
    if ($staged.ExitCode -ne 0) {
        Invoke-BcGit -RepositoryRoot $RepositoryRoot -Arguments @('commit', '-m', $CommitMessage) | Out-Null
        $committed = $true
        Write-ALbuildLog -Level Information "Committed build sources to '$branch'."
    }
    else {
        Write-ALbuildLog -Level Information "No changes to commit on '$branch'."
    }

    if ($NoPush) {
        Write-ALbuildLog -Level Information "Skipping push of '$branch' (-NoPush)."
        return [PSCustomObject]@{ Branch = $branch; Committed = $committed; Pushed = $false; Conflict = $false }
    }

    $pushArgs = @('push')
    if ($Force) { $pushArgs += '--force' }
    $pushArgs += @($Remote, $branch)

    $push = Invoke-BcGit -RepositoryRoot $RepositoryRoot -Arguments $pushArgs -AllowFailure
    if ($push.Success) {
        Write-ALbuildLog -Level Success "Pushed build branch '$branch' to '$Remote'."
        return [PSCustomObject]@{ Branch = $branch; Committed = $committed; Pushed = $true; Conflict = $false }
    }

    if (Test-BcGitPushConflict -StdErr $push.StdErr) {
        Write-ALbuildLog -Level Warning "Build branch '$branch' already exists on '$Remote' with divergent history; not pushed."
        return [PSCustomObject]@{ Branch = $branch; Committed = $committed; Pushed = $false; Conflict = $true }
    }

    $detail = if ([string]::IsNullOrWhiteSpace($push.StdErr)) { $push.StdOut } else { $push.StdErr }
    throw "Failed to push build branch '$branch' to '$Remote' (exit $($push.ExitCode)). Ensure the checkout persisted credentials.$([Environment]::NewLine)$($detail.Trim())"
}