Start-ModuleBuild.ps1

# This is taken directory from Invoke-Build .build.ps1 at https://github.com/nightroman/Invoke-Build/blob/master/.build.ps1
# will use this as a starting point
<#
.Synopsis
    Build script (https://github.com/pauby/PsTodoTxt)
 
.Description
    TASKS AND REQUIREMENTS
    Run tests
    Clean the project directory
#>


[CmdletBinding()]
Param (
    [String]
    $ReleaseType = 'Unknown',

    [string]
    $GitHubUsername = $env:GITHUB_USERNAME,

    [string]
    $GitHubApiKey = $env:GITHUB_API_KEY,

    [string]
    $PSGalleryApiKey = $env:PSGALLERY_API_KEY
)

# Find the build folder based on build system
$Timestamp = Get-Date -UFormat "%Y%m%d-%H%M%S"
$PSVersion = $PSVersionTable.PSVersion.Major
$TestFile = "TestResults_PS$PSVersion`_$TimeStamp.xml"
$lines = '----------------------------------------------------------------------'
$CodeCoverageThreshold = 0.8 # 80%

$script:BuildDefault = @{
    BuildConfigurationFilename = 'build.configuration.psd1'
    CodeCoverageThreshold      = 0.8 # 80%
}

if($ENV:BHCommitMessage -match "!verbose") {
    #$global:VerbosePreference = 'Continue'
    $global:VerbosePreference = [System.Management.Automation.ActionPreference]::Continue
}

task Build Clean,
TestSyntax,
TestAttributeSyntax,
CopyModuleFilesToBuild,
MergeFunctionsToModuleScript,
CopyLicense,
UpdateMetadata

task MakeDocs UpdateModuleHelp,
MakeHTMLDocs

task Test CleanImportedModule, 
PSScriptAnalyzer,
Pester,
ValidateTestResults,
CreateCodeHealthReport

task PublishToPSGalleryOnly CleanImportedModule,
PublishPSGallery, 
PushManifestBackToGitHub

task PublishGitReleaseOnly PushGitRelease,
PushManifestBackToGitHub

task PublishAll CleanImportedModule,
PushManifestBackToGitHub,
?PushGitRelease,
?PublishPSGallery

Enter-Build {
    # Github links require >= tls 1.2
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    # Read the configuration file if it exists
    $buildConfigPath = Get-ChildItem -Path $script:BuildDefault.BuildConfigurationFilename -Recurse | Select-Object -First 1
    if ($buildConfigPath) {
        Write-Verbose "Found build configuration file '$buildConfigPath'."
        $script:BuildConfig = Import-PowerShellDataFile -Path $buildConfigPath

        # code coverage
        $codeCoverageThreshold = $script:BuildDefault.CodeCoverageThreshold
        if ($script:BuildConfig.Testing.Keys -contains 'CodeCoverageThreshold') {
            $codeCoverageThreshold = $script:BuildConfig.Testing.CodeCoverageThreshold
            Write-Verbose "CodeCoverageThreshold of '$codeCoverageThreshold' found in configuration file."            
        }
    }

    $script:BuildInfo = Get-BuildEnvironment -ReleaseType $ReleaseType `
        -GitHubUsername $GitHubUsername -GitHubApiKey $GitHubApiKey `
        -PSGalleryApiKey $PSGalleryApiKey -CodeCoverageThreshold $codeCoverageThreshold
    Set-Location $BuildInfo.ProjectRootPath

    if ($VerbosePreference -ne 'SilentlyContinue') {
        $lines
        Write-Host "Build Started: $(Get-Date)"
        Write-Host 'Build System Environment Variables: ============================='
        Get-BuildSystemEnvironment | Hide-SensitiveData

        Write-Host 'Operating System: ==============================================='
        Get-BuildOperatingSystemDetail | Format-List

        Write-Host 'PowerShell Version: ============================================='
        Get-BuildPowerShellDetail | Format-List

        Write-Host 'Build Environment: =============================================='
        $script:BuildInfo | Hide-SensitiveData
        $lines
    }
    "`n"
}

Exit-Build {
    $lines
    Write-Host "Build Ended: $(Get-Date)"
}

# Synopsis: Remove build folder
task Clean {
    try {
        $BuildInfo.BuildPath, $BuildInfo.OutputPath | ForEach-Object { 
            Write-Verbose "Removing folder $_" 
            Remove-Item -Path $_ -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
            Write-Verbose "Creating folder $_" 
            New-Item $_ -ItemType Directory -Force | Out-Null
        }
    }
    catch {
        throw $_
    }
}

task CleanImportedModule {
    Write-Verbose "Unloading all versions of module '$($buildInfo.ModuleName)'." 
    Remove-Module $buildInfo.ModuleName -ErrorAction SilentlyContinue
    if ($null -ne (Get-Module -Name $buildInfo.ModuleName)) {
        throw "Removed module '$($BuildInfo.ModuleName)' but it's still loaded in the current session."
    }
}

task BleachClean {
    try {
        $BuildInfo.BuildRootPath, $BuildInfo.OutputPath | ForEach-Object { 
            Write-Verbose "Removing folder $_" 
            Remove-Item -Path $_ -Recurse -Force -ErrorAction SilentlyContinue | Out-Null
            Write-Verbose "Creating folder $_" 
            New-Item $_ -ItemType Directory -Force | Out-Null
        }
    }
    catch {
        throw $_
    }
}

task InitDependencies {
    # init dependencies
    if ($script:BuildConfig.Keys -contains 'Dependency') {
        $script:BuildConfig.Dependency | Initialize-BuildDependency 
    }
}

# https://github.com/indented-automation/Indented.Build
task TestSyntax {
    $hasSyntaxErrors = $false

    Get-BuildItem -Path $BuildInfo.SourcePath -Type ShouldMerge -ExcludeClass | ForEach-Object {
        $tokens = $null
        [System.Management.Automation.Language.ParseError[]]$parseErrors = @()
        $null = [System.Management.Automation.Language.Parser]::ParseInput(
            (Get-Content $_.FullName -Raw),
            $_.FullName,
            [Ref]$tokens,
            [Ref]$parseErrors
        )
        
        if ($parseErrors.Count -gt 0) {
            $parseErrors | Write-Error

            $hasSyntaxErrors = $true
        }
    }

    if ($hasSyntaxErrors) {
        throw 'TestSyntax failed'
    }
}

# https://github.com/indented-automation/Indented.Build
task TestAttributeSyntax {
    $hasSyntaxErrors = $false
    Get-BuildItem -Path $BuildInfo.SourcePath -Type ShouldMerge -ExcludeClass | ForEach-Object {
        $tokens = $null
        [System.Management.Automation.Language.ParseError[]]$parseErrors = @()
        $ast = [System.Management.Automation.Language.Parser]::ParseInput(
            (Get-Content $_.FullName -Raw),
            $_.FullName,
            [Ref]$tokens,
            [Ref]$parseErrors
        )

        # Test attribute syntax
        $attributes = $ast.FindAll( {
                param( $ast )
                
                $ast -is [System.Management.Automation.Language.AttributeAst]
            },
            $true
        )
        foreach ($attribute in $attributes) {
            if (($type = $attribute.TypeName.FullName -as [Type]) -or ($type = ('{0}Attribute' -f $attribute.TypeName.FullName) -as [Type])) {
                $propertyNames = $type.GetProperties().Name

                if ($attribute.NamedArguments.Count -gt 0) {
                    foreach ($argument in $attribute.NamedArguments) {
                        if ($argument.ArgumentName -notin $propertyNames) {
                            'Invalid property name in attribute declaration: {0}: {1} at line {2}, character {3}' -f
                            $_.Name,
                            $argument.ArgumentName,
                            $argument.Extent.StartLineNumber,
                            $argument.Extent.StartColumnNumber

                            $hasSyntaxErrors = $true
                        }
                    }
                }
            }
            else {
                'Invalid attribute declaration: {0}: {1} at line {2}, character {3}' -f
                $_.Name,
                $attribute.TypeName.FullName,
                $attribute.Extent.StartLineNumber,
                $attribute.Extent.StartColumnNumber

                $hasSyntaxErrors = $true
            }
        }
    }

    if ($hasSyntaxErrors) {
        throw 'TestAttributeSyntax failed'
    }
}

task CopyModuleFilesToBuild {
    try {
        Get-BuildItem -Path $BuildInfo.SourcePath -Type Static | `
            Copy-Item -Destination $BuildInfo.BuildPath -Recurse -Force
    }
    catch {
        throw
    }
}

task MergeFunctionsToModuleScript {
    $fileStream = [System.IO.File]::Create($BuildInfo.BuildModulePath)
    $writer = New-Object System.IO.StreamWriter($fileStream)

    $usingStatements = New-Object System.Collections.Generic.List[String]

    Get-BuildItem -Path $BuildInfo.SourcePath -Type ShouldMerge | ForEach-Object {
        $functionDefinition = Get-Content $_.FullName | ForEach-Object {
            if ($_ -match '^using (namespace|assembly)') {
                $usingStatements.Add($_)
            }
            else {
                $_.TrimEnd()
            }
        } | Out-String
        $writer.WriteLine($functionDefinition.Trim())
        $writer.WriteLine()
    }

    $writer.Close()

    $rootModule = (Get-Content $BuildInfo.BuildModulePath -Raw).Trim()
    if ($usingStatements.Count -gt 0) {
        # Add "using" statements to be start of the psm1
        $rootModule = $rootModule.Insert(0, "`r`n`r`n").Insert(
            0,
            (($usingStatements.ToArray() | Sort-Object | Get-Unique) -join "`r`n")
        )
    }
    Set-Content -Path $BuildInfo.BuildModulePath -Value $rootModule -NoNewline
}

task UpdateMetadata {
    # read the source manifest
    $manifestData = Import-PowerShellDataFile -Path $BuildInfo.SourceManifestPath

    # Manifest Version
    $manifestData.ModuleVersion = $BuildInfo.ReleaseVersion

    # RootModule
    $manifestData.RootModule = "$($BuildInfo.ModuleName).psm1"

    # FunctionsToExport
    $functionsToExport = (Get-ChildItem (Join-Path -Path $BuildInfo.SourcePath -ChildPath 'pub*') -Filter '*.ps1' -Recurse)
    if ($functionsToExport) {
        Write-Verbose "FunctionsToExport:`nFound $($functionsToExport.count) public functions to add to manifest 'FunctionsToExport' key."
        $manifestData.FunctionsToExport = $functionsToExport.BaseName
    }

    # RequiredAssemblies
    if (Test-Path (Join-Path -Path $BuildInfo.SourcePath -ChildPath 'lib\*.dll')) {
        $manifestData.RequiredAssemblies = (
            (Get-Item (Join-Path -Path $BuildInfo.SourcePath -ChildPath 'lib\*.dll')).Name | ForEach-Object {
                Join-Path -Path 'lib' -ChildPath $_
            }
        )
    }

    #ScriptsToProcess
    $scriptsToProcess = (Get-ChildItem (Join-Path -Path $BuildInfo.SourcePath -ChildPath 'script*') -Filter '*.ps1' -Recurse)
    if ($scriptsToProcess) {
        Write-Verbose "ScriptsToProcess:`nFound $($scriptsToProcess.Count) scripts to add to manifest ScriptsToProcess key."
        $manifestData.ScriptsToProcess = ($scriptsToProcess | `
                ForEach-Object { 
                if ($_.FullName -match '(?<name>script.*\\.*\.ps1)') {
                    Write-Verbose "Adding '$($matches.name)' to ScriptsToProcess"
                    $matches.name
                } #end if
            } #end Foreach
        )
    } #end if

    # FormatsToProcess
    if (Test-Path (Join-Path -Path $BuildInfo.SourcePath -ChildPath '*.Format.ps1xml')) {
        $manifestData = (Get-Item (Join-Path -Path $BuildInfo.SourcePath -ChildPath '*.Format.ps1xml')).Name
    }

    # Attempt to parse the project URI from the list of upstream repositories
    $gitOriginUri = ''
    [String]$pushOrigin = (git remote -v) -like 'origin*(push)'
    if ($pushOrigin -match 'origin\s+(?<origin>https?://\S+).*') {
        $gitOriginUri = $matches.Origin

        # if we have no license in the source manifest but we have a LICENSE
        # file then use that
        if (($manifestData.PrivateData.PSData.Keys -notcontains 'LicenseUri') -and `
            (Test-Path -Path (Join-Path -Path $BuildInfo.ProjectRootPath -ChildPath 'LICENSE'))) {
            $manifestData.PrivateData.PSData.LicenseUri = "$gitOriginUri/blob/master/LICENSE"
        }

        # if we have no project uri then add the git remote origin
        if ($manifestData.PrivateData.PSData.Keys -notcontains 'ProjectUri') {
            $manifestData.PrivateData.PSData.ProjectUri = $gitOriginUri
        }
        
        if (($manifestData.PrivateData.PSData.Keys -notcontains 'ReleaseNotes') -and `
            (Test-Path -Path (Join-Path -Path $BuildInfo.ProjectRootPath -ChildPath 'CHANGELOG.md'))) {
            $manifestData.PrivateData.PSData.ReleaseNotes = "$gitOriginUri/blob/master/CHANGELOG.md"
        }
    }

    # clone the privatedata key and rthen remove it so we can use it and manifestData for splatting
    $privateData = ($manifestData.PrivateData.PSData).Clone()
    $manifestData.Remove('PrivateData')

    New-ModuleManifest -Path $BuildInfo.BuildManifestPath @manifestData @privateData -verbose
}

task UpdateModuleHelp -If (Get-Module platyPS -ListAvailable) CleanImportedModule, {
    try {
        $moduleInfo = Import-Module $BuildInfo.BuildModulePath -ErrorAction Stop -PassThru
        if ($moduleInfo.ExportedCommands.Count -gt 0) {
            $moduleInfo.ExportedCommands.Keys | ForEach-Object { 
                New-MarkdownHelp -Command $_ `
                    -OutputFolder (Join-Path -Path $BuildInfo.ProjectRootPath -ChildPath 'help') -Force | Out-Null
            }

            New-ExternalHelp -Path (Join-Path -Path $BuildInfo.ProjectRootPath -ChildPath 'help') `
                -OutputPath (Join-Path -Path $BuildInfo.BuildPath -ChildPath 'en-GB') -Force | Out-Null
        }
    }
    catch {
        throw
    }
}

task MakeHTMLDocs -If { [bool](exec { pandoc.exe --help }) } {
    $names = 'README', 'CHANGELOG'
    ForEach ($name in $names) {
        $sourcePath = Join-Path -Path $BuildInfo.ProjectRootPath -ChildPath "$name.md"
        if (Test-Path $sourcePath) {
            $destPath = Join-Path -Path $BuildInfo.BuildPath -ChildPath "$name.html"
            exec { pandoc.exe --standalone --from=markdown_strict --metadata=title:$name --output=$destPath $sourcePath }
            Write-Verbose "Converted markdown file '$name.md' to '$destPath'"
        } # end if
    } # end foreach
}

task CopyLicense -If {Test-Path (Join-Path -Path $BuildInfo.ProjectRootPath -ChildPath 'LICENSE')}  {
    try {
        Copy-Item -Path (Join-Path -Path $BuildInfo.ProjectRootPath -ChildPath 'LICENSE') -Destination $BuildInfo.BuildPath
    }
    catch {
        throw
    }
}

task PSScriptAnalyzer -If (Get-Module PSScriptAnalyzer -ListAvailable) {
    try {
        Set-Location $BuildInfo.SourcePath
        'priv*', 'pub*' | Where-Object { Test-Path $_ } | ForEach-Object {
            $path = Resolve-Path (Join-Path -Path $BuildInfo.SourcePath -ChildPath $_)
            if (Test-Path $path) {
                $splat = @{
                    Path    = $path
                    Recurse = $true
                    #Verbose = $true
                }

                if (($BuildInfo.PSSASettingsPath -ne '') -and (Test-Path $BuildInfo.PSSASettingsPath -PathType Leaf)) {
                    # the settings parameter for PSScriptAnalyzer MUST be a
                    # string - see
                    # https://github.com/PowerShell/PSScriptAnalyzer/issues/914
                    $splat += @{ Settings = "$($BuildInfo.PSSASettingsPath)" } 
                }
                
                Write-Verbose "Running PSScriptAnalyzer default rules on '$path'."
                Invoke-ScriptAnalyzer @splat | ForEach-Object {
                    $_
                    $_ | Export-Csv (Join-Path -Path $BuildInfo.OutputPath -ChildPath 'psscriptanalyzer.csv') -NoTypeInformation -Append
                }
    
                # TODO:We only need to do this becasue the PSScriptAnalyzer
                # settings file does not allow CustomRulePath and
                # IncludeDefaultRules together. Once this is resolved this could
                # should be revisited.
                # https://github.com/PowerShell/PSScriptAnalyzer/issues/675
                if ($BuildInfo.PSSACustomRulesPath -ne '') {
                    $splat += @{ 
                        CustomRulePath      = "$(Join-Path -Path $BuildInfo.PSSACustomRulesPath -ChildPath '*.psd1')"
                        # TODO: This rule is here as it is throwing an exception on some code
                        #ExcludeRule = 'Measure-ErrorActionPreference'
                    }

                    Write-Verbose "Running PSScriptAnalyzer custom rules on '$path'."
                    Invoke-ScriptAnalyzer @splat | ForEach-Object {
                        $_
                        $_ | Export-Csv (Join-Path $BuildInfo.OutputPath 'psscriptanalyzer.csv') -NoTypeInformation -Append
                    }
                }
            }
        }
    }
    catch {
        throw
    }
}

task Pester -If { Get-ChildItem -Path $BuildInfo.TestPath -Filter '*.tests.ps1' -Recurse -File } {

    Import-Module $BuildInfo.BuildManifestPath -Global -ErrorAction Stop -Force
    $params = @{
        Script       = $BuildInfo.TestPath
        CodeCoverage = $BuildInfo.BuildModulePath
        OutputFile   = Join-Path -Path $BuildInfo.OutputPath -ChildPath "$($BuildInfo.ModuleName)-nunit.xml"
        PassThru     = $true
        Show         = if ($VerbosePreference -eq 'SilentlyContinue') { 'None' } else { 'all' }
        Strict       = $true 
    }

    $pester = Invoke-Pester @params

    $path = Join-Path -Path $BuildInfo.OutputPath -ChildPath 'pester-output.xml'
    $pester | Export-CliXml $path
}

task ValidateTestResults PSScriptAnalyzer, Pester, {
    $testsFailed = $false

    # PSScriptAnalyzer
    $path = Join-Path -Path $BuildInfo.OutputPath -ChildPath 'psscriptanalyzer.csv'
    if ((Test-Path $path) -and ($testResults = Import-Csv -Path $path)) {
        '{0} warnings were raised by PSScriptAnalyzer' -f @($testResults).Count
        $testsFailed = $true
    }
    else {
        Write-Verbose '0 warnings were raised by PSScriptAnalyzer'
    }

    # Pester tests
    $path = Join-Path -Path $BuildInfo.OutputPath -ChildPath 'pester-output.xml'
    $pester = Import-CliXml -Path $path
    if ($pester.FailedCount -gt 0) {
        '{0} of {1} Pester tests are failing' -f $pester.FailedCount, $pester.TotalCount
        $testsFailed = $true
    }
    else {
        Write-Verbose 'All Pester tests passed.'
    }

    # Pester code coverage
    [Double]$codeCoverage = $pester.CodeCoverage.NumberOfCommandsExecuted / $pester.CodeCoverage.NumberOfCommandsAnalyzed
    $pester.CodeCoverage.MissedCommands | `
        Export-Csv -Path (Join-Path -Path $BuildInfo.OutputPath -ChildPath 'CodeCoverage.csv') -NoTypeInformation

    if ($codecoverage -lt $BuildInfo.CodeCoverageThreshold) {
        'Pester code coverage ({0:P}) is below threshold {1:P}.' -f $codeCoverage, $BuildInfo.CodeCoverageThreshold
        $testsFailed = $true
    }

    # Solution tests
    Get-ChildItem $BuildInfo.OutputPath -Filter *.dll.xml | ForEach-Object {
        $report = [Xml](Get-Content $_.FullName -Raw)
        if ([Int]$report.'test-run'.failed -gt 0) {
            '{0} of {1} solution tests in {2} are failing' -f $report.'test-run'.failed,
            $report.'test-run'.total,
            $report.'test-run'.'test-suite'.name
            $testsFailed = $true
        }
    }

    if ($testsFailed) {
        throw 'Test result validation failed'
    }
}

task CreateCodeHealthReport -If (Get-Module PSCodeHealth -ListAvailable) {
    Import-Module -FullyQualifiedName $BuildInfo.BuildManifestPath -Global -ErrorAction Stop
    $params = @{
        Path           = $BuildInfo.BuildModulePath
        Recurse        = $true
        TestsPath      = $BuildInfo.TestPath
        HtmlReportPath = Join-Path -Path $BuildInfo.OutputPath -ChildPath "$($Buildinfo.ModuleName)-code-health.html"
    }
    Invoke-PSCodeHealth @params
}

# Synopsis: Warn about not empty git status if .git exists.
task GitStatus -If (Test-Path .git) {
    $status = exec { git status -s }
    if ($status) {
        Write-Warning "Git status: $($status -join ', ')"
    }
}

# Synopsis: Push with a version tag.
task PushGitRelease CreateBuildArtifact, {

    if ((Test-Path -Path $BuildInfo.BuildArtifactPath) -and ($BuildInfo.GithubUsername -ne '') -and ($BuildInfo.GithubApiKey -ne '')) {
        $params = @{
            Version             = $BuildInfo.ReleaseVersion
            CommitID            = $BuildInfo.RepoLastCommitHash
            ReleaseNotes        = "Release v$($BuildInfo.ReleaseVersion)"
            ArtifactPath        = $BuildInfo.BuildArtifactPath
            GitHubUsername      = $BuildInfo.GitHubUsername
            GitHubRepository    = $BuildInfo.ModuleName
            GitHubApiKey        = $BuildInfo.GithubApiKey
            Draft               = $true
        } 

        New-GitHubRelease @params
    }
    else {
        throw "Cannot push a release to GitHub - '$($BuildInfo.BuildArtifactPath)' is missing or GitHubUsername / GitHubApiKey has not been given."
    }
}

task PublishPSGallery {
    if (-not $BuildInfo.PSGalleryApiKey) {
        Write-Error "Cannot push to the PowerShell Gallery as no Api Key was provided."
    }

    # get the current PS Gallery version and if it's the same as our new version then do not push it
    try {
        $psGalleryVersion = [version](Find-Module -Name $BuildInfo.ModuleName -ErrorAction Stop).Version
    }
    catch {
        Write-Warning "Cannot find a previous version of '$($BuildInfo.ModuleName)' in the PowerShell Gallery. Please push the first verison of the module manually."
    }
    if ([version]$psGalleryVersion -lt [version]$BuildInfo.ReleaseVersion) {
        Write-Verbose "Publishing version '$($BuildInfo.ReleaseVersion)' of '$($BuildInfo.ModuleName)' module to PowerShell Gallery."
        Import-Module $BuildInfo.BuildManifestPath -Global -Force
        Publish-Module -NuGetApiKey $BuildInfo.PSGalleryApiKey -Path $BuildInfo.BuildPath -ReleaseNotes $BuildInfo.ReleaseNotes
    }
    else {
        throw "PowerShell Gallery Version ($psGalleryVersion) is the same or greater than our new version '($($BuildInfo.ReleaseVersion))'. Cannot publish module."
    }
}

task PushManifestBackToGitHub {
    if ($BuildInfo.GitHubUsername -eq '' -or $BuildInfo.GitHubApiKey -eq '') {
        throw 'Cannot push manifest back to Github - Username or API Key are blank.'
    }

    Set-Location $BuildInfo.ProjectRootPath
    # Resolve-Path will return the relative path that we need to match against
    # the git changed files. However the resolved path will start with '.\' so
    # we need to strip this.
    $relativePath = (Resolve-Path -Path $BuildInfo.SourceManifestPath -Relative).Substring(2).Replace('\', '/')
    # we need to use the EXACT case to 'git add <manifest>' otherwise it does
    # not work - so here we are matching with the manifest file and returning
    # the case git needs
    $manifest = Get-GitChange | Where-Object { $_ -eq $relativePath }
    if ($manifest) {
        $pushUrl = exec { git remote get-url origin --push }
        $authUrl = $pushUrl.Replace('github.com', "$($BuildInfo.GitHubUsername):$($BuildInfo.GitHubApiKey)@github.com") 
        Write-Verbose "Pushing '$manifest' back to GitHub with message 'Updated version to $($BuildInfo.ReleaseVersion)'."
        #exec { git pull }
        exec { git add $manifest }
        exec { git commit -m "Updated version to $($BuildInfo.ReleaseVersion) [skip ci]" }
        exec { git push --porcelain }   # --porcelain is required or exec detects the command as failed
    }
    else {
        Write-Warning "The source manifest '$($BuildInfo.SourceManifestPath)' has not been changed. Cannot push it back to GitHub."
    }
}

task CreateBuildArtifact {
    # create the build artifact
    Remove-Item -Path $BuildInfo.BuildArtifactPath -ErrorAction SilentlyContinue

    $sourcePath = Join-Path -Path $BuildInfo.BuildPath -ChildPath '*'
    Write-Verbose "Creating ZIP archive '$($BuildInfo.BuildArtifactPath)' containing '$sourcePath'."
    Compress-Archive -Path $sourcePath -DestinationPath $BuildInfo.BuildArtifactPath
}