Functions/New-WhiskeySemanticVersion.ps1


function New-WhiskeySemanticVersion
{
    <#
    .SYNOPSIS
    Creates a version number that identifies the current build.
 
    .DESCRIPTION
    The `New-WhiskeySemanticVersion` function gets a semantic version that represents the current build. If called multiple times during a build, you'll get the same verson number back.
 
    If passed a `Version`, it will return that version with build metadata attached. Alternatively, you may pass a valid file path to the `Path` parameter and the function will pull the version number from that file. Any build metadata on the passed-in version is replaced. On a build server, build metadata is the build number, source control branch, and commit ID, e.g. `80.master.deadbee`. When run by developers, the build metadata is the current username and computer name, e.g. `whiskey.desktop001`.
 
    The `Path` parameter currently supports the following files:
 
    * Node.js `package.json` files (JSON key: `version`).
      
        { "version": "1.0.0" }
 
    * PowerShell module manifest files with the file extension `.psd1` (Hash table key: `ModuleVersion`).
         
        @{ ModuleVersion = '1.0.0'; }
 
    * .NET csproj files with the file extension `.csproj` (XML element: `/Project/PropertyGroup/Version`).
         
        <Project>
            <PropertyGroup>
                <Version>1.0.0</Version>
            </PropertyGroup>
        </Project>
 
    If not passed a `Path`, `Version`, or the version passed is null or empty, a date-based version number is generated for you. The major number is the year and the minor number is the month and day, e.g. `2017.0327`. If run by a developer, the patch number is set to `0`. If run on a build server, the build number is used.
 
    Pass any prerelease metadata to the `Prerelease` parameter.
    #>

    [CmdletBinding()]
    [OutputType([SemVersion.SemanticVersion])]
    param(
        [Parameter(Mandatory=$true,ParameterSetName='ByVersion')]
        [AllowNull()]
        [object]
        $Version,

        [Parameter(Mandatory=$true,ParameterSetName='ByPath')]
        [object]
        $Path,

        [Parameter(Mandatory=$true)]
        [AllowEmptyString()]
        [string]
        $Prerelease,

        [Parameter(Mandatory=$true)]
        [object]
        $BuildMetadata
    )

    Set-StrictMode -Version 'Latest'
    Use-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState

    if( $Version )
    {
        $semVersion = $Version | ConvertTo-WhiskeySemanticVersion
    }
    elseif( $Path )
    {
        $resolvedPath = Resolve-Path -Path $Path -ErrorAction Ignore | Select-Object -ExpandProperty ProviderPath
        if( -not $resolvedPath )
        {
            Write-Error ('Path to given version file ''{0}'' does not exist.' -f $Path)
            return
        }
        $Path = $resolvedPath

        $fileInfo = Get-Item $Path
        if( $fileInfo.Name -eq 'package.json' )
        {
            try
            {
                $packageJson = Get-Content -Raw -Path $Path | ConvertFrom-Json
            }
            catch
            {
                Write-Error -ErrorRecord $_
                Write-Error -Message ('Unable to get version to build, package.json file ''{0}'' contains bad JSON.' -f $Path)
                return
            }

            $semVersion = $packageJson | Select-Object -ExpandProperty 'version' -ErrorAction Ignore | ConvertTo-WhiskeySemanticVersion

            if( -not $semVersion )
            {
                Write-Error -Message ('Unable to get the version to build from the Node.js package.json file ''{0}''. Please make sure the file is valid JSON, that it contains a ''version'' property, and that the version''s value is a valid semantic version, e.g.
 
                {{
                    "version": "1.2.3"
                }}
                '
 -f $fileInfo.FullName)
                return
            }
        }
        elseif( $fileInfo.Extension -eq '.psd1' )
        {
            $semVersion = Test-ModuleManifest -Path $Path -ErrorAction Ignore |
                            Select-Object -ExpandProperty 'Version' |
                            ConvertTo-WhiskeySemanticVersion

            if( -not $semVersion )
            {
                Write-Error -Message ('Unable to get the version to build from the PowerShell module manifest file ''{0}''. Please make sure the file is a valid Powershell module manifest and that it contains a ''ModuleVersion'' property, e.g.
 
                @{{
                    ModuleVersion = ''1.2.3'';
                }}
                '
 -f $fileInfo.FullName)
                return
            }
        }
        elseif( $fileInfo.Extension -eq '.csproj' )
        {
            try
            {
                [xml]$csprojXml = Get-Content -Path $Path -Raw
            }
            catch
            {
                Write-Error -ErrorRecord $_
                Write-Error -Message ('Unable to get version to build, csproj file ''{0}'' contains bad XML.' -f $Path)
                return
            }

            $xmlns = New-Object System.Xml.XmlNamespaceManager($csprojXml.NameTable)
            $xmlns.AddNamespace('ns', $csprojXml.DocumentElement.NamespaceURI)
            $csprojVersionNode = $csprojXml.SelectSingleNode('//ns:Version', $xmlns)
            $csprojVersion = $null
            if( $csprojVersionNode )
            {
                $csprojVersion = $csprojVersionNode.InnerXml
            }

            if( -not $csprojVersion )
            {
                Write-Error -Message ('Unable to get the version to build from the csproj file ''{0}'' as it either didn''t contain a ''Version'' element or the element text was empty. Please make sure there is a valid ''Version'' element located under ''/Project/PropertyGroup'', e.g.
 
                <Project Sdk="Microsoft.NET.Sdk">
                    <PropertyGroup>
                        <Version>1.2.3</Version>
                    </PropertyGroup>
                </Project>
                '
 -f $Path)
                return
            }

            $semVersion = $csprojVersion | ConvertTo-WhiskeySemanticVersion
        }
        else
        {
            Write-Error -Message ('Version file ''{0}'' is an unsupported file type.' -f $Path)
            return
        }
    }
    else
    {
        $patch = '0'
        if( $BuildMetadata.IsBuildServer )
        {
            $patch = $BuildMetadata.BuildNumber
        }
        $today = Get-Date
        $semVersion = New-Object 'SemVersion.SemanticVersion' $today.Year,$today.ToString('MMdd'),$patch,$Prerelease
    }

    $buildInfo = '{0}.{1}' -f $env:USERNAME,$env:COMPUTERNAME
    if( $BuildMetadata.IsBuildServer )
    {
        $branch = $BuildMetadata.ScmBranch
        $branch = $branch -replace '[^A-Za-z0-9-]','-'
        $commitID = $BuildMetadata.ScmCommitID.Substring(0,7)
        $buildInfo = '{0}.{1}.{2}' -f $BuildMetadata.BuildNumber,$branch,$commitID
    }

    if( -not $Prerelease )
    {
        $Prerelease = $semVersion.Prerelease
    }

    return New-Object 'SemVersion.SemanticVersion' $semVersion.Major,$semVersion.Minor,$semVersion.Patch,$Prerelease,$buildInfo
}