Private/_Helpers.ps1

function Set-ColourOption(
    [string]$value
) {
    if ($value -eq "False") { 
        return "White" 
    } 
    elseif ($value -eq "Error") {
        return "Red"
    }
    elseif ($value -eq "Optional") {
        return "Magenta"
    }
    else { return "Green" }
}

function Test-InsideDockerContainer {
    $DockerSvc = Get-Service -Name cexecsvc -ErrorAction SilentlyContinue
    if ($null -eq $DockerSvc ) {
        return $false
    }
    else {
        return $true
    }
}

function Out-FileUtf8NoBom {

    <#
    .SYNOPSIS
      Outputs to a UTF-8-encoded file *without a BOM* (byte-order mark).
   
    .DESCRIPTION
   
      Mimics the most important aspects of Out-File:
        * Input objects are sent to Out-String first.
        * -Append allows you to append to an existing file, -NoClobber prevents
          overwriting of an existing file.
        * -Width allows you to specify the line width for the text representations
          of input objects that aren't strings.
      However, it is not a complete implementation of all Out-File parameters:
        * Only a literal output path is supported, and only as a parameter.
        * -Force is not supported.
        * Conversely, an extra -UseLF switch is supported for using LF-only newlines.
   
    .NOTES
      The raison d'être for this advanced function is that Windows PowerShell
      lacks the ability to write UTF-8 files without a BOM: using -Encoding UTF8
      invariably prepends a BOM.
   
      Copyright (c) 2017, 2022 Michael Klement <mklement0@gmail.com> (http://same2u.net),
      released under the [MIT license](https://spdx.org/licenses/MIT#licenseText).
   
    #>

  
    [CmdletBinding(PositionalBinding = $false)]
    param(
        [Parameter(Mandatory, Position = 0)] [string] $LiteralPath,
        [switch] $Append,
        [switch] $NoClobber,
        [AllowNull()] [int] $Width,
        [switch] $UseLF,
        [Parameter(ValueFromPipeline)] $InputObject
    )
  
    begin {
  
        # Convert the input path to a full one, since .NET's working dir. usually
        # differs from PowerShell's.
        $dir = Split-Path -LiteralPath $LiteralPath
        if ($dir) { $dir = Convert-Path -ErrorAction Stop -LiteralPath $dir } else { $dir = $pwd.ProviderPath }
        $LiteralPath = [IO.Path]::Combine($dir, [IO.Path]::GetFileName($LiteralPath))
      
        # If -NoClobber was specified, throw an exception if the target file already
        # exists.
        if ($NoClobber -and (Test-Path $LiteralPath)) {
            Throw [IO.IOException] "The file '$LiteralPath' already exists."
        }
      
        # Create a StreamWriter object.
        # Note that we take advantage of the fact that the StreamWriter class by default:
        # - uses UTF-8 encoding
        # - without a BOM.
        $sw = New-Object System.IO.StreamWriter $LiteralPath, $Append
      
        $htOutStringArgs = @{}
        if ($Width) { $htOutStringArgs += @{ Width = $Width } }
  
        try { 
            # Create the script block with the command to use in the steppable pipeline.
            $scriptCmd = { 
                & Microsoft.PowerShell.Utility\Out-String -Stream @htOutStringArgs | 
                . { process { if ($UseLF) { $sw.Write(($_ + "`n")) } else { $sw.WriteLine($_) } } }
            }  
        
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        }
        catch { throw }
  
    }
  
    process {
        $steppablePipeline.Process($_)
    }
  
    end {
        $steppablePipeline.End()
        $sw.Dispose()
    }
  
}

function Update-Manifest {
    #.Synopsis
    # Update a PowerShell module manifest
    #.Description
    # By default Update-Manifest increments the ModuleVersion, but it can set any key in the Module Manifest, its PrivateData, or the PSData in PrivateData.
    #
    # NOTE: This cannot currently create new keys, or uncomment keys.
    #.Example
    # Update-Manifest .\Configuration.psd1
    #
    # Increments the Build part of the ModuleVersion in the Configuration.psd1 file
    #.Example
    # Update-Manifest .\Configuration.psd1 -Increment Major
    #
    # Increments the Major version part of the ModuleVersion in the Configuration.psd1 file
    #.Example
    # Update-Manifest .\Configuration.psd1 -Value '0.4'
    #
    # Sets the ModuleVersion in the Configuration.psd1 file to 0.4
    #.Example
    # Update-Manifest .\Configuration.psd1 -Property ReleaseNotes -Value 'Add the awesome Update-Manifest function!'
    #
    # Sets the PrivateData.PSData.ReleaseNotes value in the Configuration.psd1 file!
    [CmdletBinding()]
    param(
        # The path to the module manifest file
        [Parameter(ValueFromPipelineByPropertyName = "True", Position = 0)]
        [Alias("PSPath")]
        [string]$Manifest,

        # The property to be set in the manifest. It must already exist in the file (and not be commented out)
        # This searches the Manifest root properties, then the properties PrivateData, then the PSData
        [Parameter(ParameterSetName = "Overwrite")]
        [string]$PropertyName = 'ModuleVersion',

        # A new value for the property
        [Parameter(ParameterSetName = "Overwrite", Mandatory)]
        $Value,

        # By default Update-Manifest increments ModuleVersion; this controls which part of the version number is incremented
        [Parameter(ParameterSetName = "Increment")]
        [ValidateSet("Major", "Minor", "Build", "Revision")]
        [string]$Increment = "Build",

        # When set, and incrementing the ModuleVersion, output the new version number.
        [Parameter(ParameterSetName = "Increment")]
        [switch]$Passthru
    )

    $KeyValue = Get-ManifestValue $Manifest -PropertyName $PropertyName -Passthru

    if ($PSCmdlet.ParameterSetName -eq "Increment") {
        $Version = [Version]$KeyValue.SafeGetValue()

        $Version = switch ($Increment) {
            "Major" {
                [Version]::new($Version.Major + 1, 0)
            }
            "Minor" {
                $Minor = if ($Version.Minor -le 0) { 1 } else { $Version.Minor + 1 }
                [Version]::new($Version.Major, $Minor)
            }
            "Build" {
                $Build = if ($Version.Build -le 0) { 1 } else { $Version.Build + 1 }
                [Version]::new($Version.Major, $Version.Minor, $Build)
            }
            "Revision" {
                $Revision = if ($Version.Revision -le 0) { 1 } else { $Version.Revision + 1 }
                [Version]::new($Version.Major, $Version.Minor, $Version.Build, $Revision)
            }
        }

        $Value = $Version

        if ($Passthru) { $Value }
    }

    $Value = ConvertTo-Metadata $Value

    $Extent = $KeyValue.Extent
    while ($KeyValue.parent) { $KeyValue = $KeyValue.parent }

    $ManifestContent = $KeyValue.Extent.Text.Remove(
        $Extent.StartOffset, 
        ($Extent.EndOffset - $Extent.StartOffset)
    ).Insert($Extent.StartOffset, $Value)

    if (Test-Path $Manifest) {
        Out-FileUtf8NoBom $Manifest $ManifestContent
    }
    else {
        $ManifestContent
    }
}

function Get-ManifestValue {
    #.Synopsis
    # Reads a specific value from a module manifest
    #.Description
    # By default Get-ManifestValue gets the ModuleVersion, but it can read any key in the Module Manifest, including the PrivateData, or the PSData inside the PrivateData.
    #.Example
    # Get-ManifestValue .\Configuration.psd1
    #
    # Returns the module version number (as a string)
    #.Example
    # Get-ManifestValue .\Configuration.psd1 ReleaseNotes
    #
    # Returns the release notes!
    [CmdletBinding()]
    param(
        # The path to the module manifest file
        [Parameter(ValueFromPipelineByPropertyName = "True", Position = 0)]
        [Alias("PSPath")]
        [string]$Manifest,

        # The property to be read from the manifest. Get-ManifestValue searches the Manifest root properties, then the properties PrivateData, then the PSData
        [Parameter(ParameterSetName = "Overwrite", Position = 1)]
        [string]$PropertyName = 'ModuleVersion',

        [switch]$Passthru
    )
    $ErrorActionPreference = "Stop"

    if (Test-Path $Manifest) {
        $ManifestContent = Get-Content $Manifest -Raw
    }
    else { 
        $ManifestContent = $Manifest
    }

    $Tokens = $Null; $ParseErrors = $Null
    $AST = [System.Management.Automation.Language.Parser]::ParseInput( $ManifestContent, $Manifest, [ref]$Tokens, [ref]$ParseErrors )
    $ManifestHash = $AST.Find( { $args[0] -is [System.Management.Automation.Language.HashtableAst] }, $true )
    $KeyValue = $ManifestHash.KeyValuePairs.Where{ $_.Item1.Value -eq $PropertyName }.Item2

    # Recursively search for PropertyName in the PrivateData and PrivateData.PSData
    if (!$KeyValue) {
        $global:devops_PrivateData = $ManifestHash.KeyValuePairs.Where{ $_.Item1.Value -eq 'PrivateData' }.Item2.PipelineElements.Expression
        $KeyValue = $PrivateData.KeyValuePairs.Where{ $_.Item1.Value -eq $PropertyName }.Item2
        if (!$KeyValue) {
            $global:devops_PSData = $PrivateData.KeyValuePairs.Where{ $_.Item1.Value -eq 'PSData' }.Item2.PipelineElements.Expression
            $KeyValue = $PSData.KeyValuePairs.Where{ $(Write-Verbose "'$($_.Item1.Value)' -eq '$PropertyName'"); $_.Item1.Value -eq $PropertyName }.Item2
            if (!$KeyValue) {
                Write-Error "Couldn't find '$PropertyName' to update in '$(Convert-Path $ManifestPath)'"
                return
            }
        }
    }

    if ($Passthru) { $KeyValue } else { $KeyValue.SafeGetValue() }
}

function Update-ProjectFile() {
    $projectFileMaster = Get-Content (Join-Path (Get-Module -Name Capgemini.PowerPlatform.DevOps).Path -ChildPath ..\Private\emptyProject.json) | ConvertFrom-Json
    if ($projectFileMaster) {
        $properties = Get-Member -InputObject $global:devops_projectFile -MemberType Properties
        $properties | ForEach-Object { 
            $hasProperty = (Get-Member -InputObject $projectFileMaster.$($_.Name) -MemberType Properties -ErrorAction SilentlyContinue)
            if ($hasProperty) {
                $projectFileMaster.$($_.Name) = $global:devops_projectFile.$($_.Name)     
            }
            
        }
        $global:devops_projectFile = $projectFileMaster
        $global:devops_projectFile | ConvertTo-Json | Out-FileUtf8NoBom ("$global:devops_projectLocation\$global:devops_gitRepo.json")              
    }
}

function Update-ArtifactIgnore() {
    $artifactFileMaster = Get-Content (Join-Path (Get-Module -Name Capgemini.PowerPlatform.DevOps).Path -ChildPath ..\FrameworkTemplate\file.artifactignore)
    if ($artifactFileMaster) {
        $projectArtifact = Get-Content ("$global:devops_projectLocation\.artifactignore") 
        $artifactFileMaster + $projectArtifact | Sort-Object | Get-Unique | Out-FileUtf8NoBom ("$global:devops_projectLocation\.artifactignore") 
    }
}

function Format-Json {
    <#
  .SYNOPSIS
      Prettifies JSON output.
  .DESCRIPTION
      Reformats a JSON string so the output looks better than what ConvertTo-Json outputs.
  .PARAMETER Json
      Required: [string] The JSON text to prettify.
  .PARAMETER Minify
      Optional: Returns the json string compressed.
  .PARAMETER Indentation
      Optional: The number of spaces (1..1024) to use for indentation. Defaults to 4.
  .PARAMETER AsArray
      Optional: If set, the output will be in the form of a string array, otherwise a single string is output.
  .EXAMPLE
      $json | ConvertTo-Json | Format-Json -Indentation 2
  #>

    [CmdletBinding(DefaultParameterSetName = 'Prettify')]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$Json,

        [Parameter(ParameterSetName = 'Minify')]
        [switch]$Minify,

        [Parameter(ParameterSetName = 'Prettify')]
        [ValidateRange(1, 1024)]
        [int]$Indentation = 4,

        [Parameter(ParameterSetName = 'Prettify')]
        [switch]$AsArray
    )

    if ($PSCmdlet.ParameterSetName -eq 'Minify') {
        return ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100 -Compress
    }

    # If the input JSON text has been created with ConvertTo-Json -Compress
    # then we first need to reconvert it without compression
    if ($Json -notmatch '\r?\n') {
        $Json = ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100
    }

    $indent = 0
    $regexUnlessQuoted = '(?=([^"]*"[^"]*")*[^"]*$)'

    $result = $Json -split '\r?\n' |
    ForEach-Object {
        # If the line contains a ] or } character,
        # we need to decrement the indentation level unless it is inside quotes.
        if ($_ -match "[}\]]$regexUnlessQuoted") {
            $indent = [Math]::Max($indent - $Indentation, 0)
        }

        # Replace all colon-space combinations by ": " unless it is inside quotes.
        $line = (' ' * $indent) + ($_.TrimStart() -replace ":\s+$regexUnlessQuoted", ': ')

        # If the line contains a [ or { character,
        # we need to increment the indentation level unless it is inside quotes.
        if ($_ -match "[\{\[]$regexUnlessQuoted") {
            $indent += $Indentation
        }

        $line
    }

    if ($AsArray) { return $result }
    return $result -Join [Environment]::NewLine
}

function Invoke-OpenSolution {
    . "$global:devops_projectLocation\$global:devops_gitRepo.sln"
}

function Invoke-OpenSolutionInVSCode {
    Start-Process code -ArgumentList  "`"$($global:devops_projectLocation)`""
}

function Get-ConfigJSON($StartPath) {
    $global:devops_BaseConfig = Join-Path $StartPath -ChildPath "$SelectedSolution\Scripts\config.json"

    # Load and parse the JSON configuration file
    try {
        $global:devops_Config = Get-Content -Path $global:devops_BaseConfig -Raw -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue | ConvertFrom-Json -ErrorAction:SilentlyContinue -WarningAction:SilentlyContinue
    }
    catch {
        Write-Error "The Base configuration file is missing!" -Stop
    }

    # Check the configuration
    if (!($global:devops_Config)) {
        Write-Error "The Base configuration file is missing!" -Stop
    }

    $global:devops_ServerUrl = ($global:devops_Config.target.ServerUrl)
    $global:devops_SolutionName = ($global:devops_Config.target.SolutionName)

    Write-Host $global:devops_ServerUrl
    Write-Host $global:devops_SolutionName
}

function Get-DeployEnvironments {
    try {
        Get-AccessToken
        $Environments = Get-Content -Path $global:devops_projectLocation\Environments.json | ConvertFrom-Json
        [array]$options = "[Go Back]"
        $options += $Environments | ForEach-Object { "$($_.EnvironmentName)" }

        do {
            $sel = Invoke-Menu -MenuTitle "---- Select Environment to Deploy To ------" -MenuOptions $options          
        } until ($sel -ge 0)
        if ($sel -eq 0) {
            return
        }
        else {

            #Check if Environment has Approver
            Write-Host "Checking if Environment $($Environments[$sel -1].EnvironmentName) requires approval"
            $AzureDevOpsAuthenticationHeader = @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($env:AZURE_DEVOPS_EXT_PAT)")) }
            $UriOrganization = "https://dev.azure.com/$($global:devops_projectFile.OrgName)/"
            $uriEnvironments = $UriOrganization + "$($global:devops_projectFile.Project)/_apis/distributedtask/environments?api-version=6.1-preview.1"
            try {
                $EnvironmentsResult = Invoke-RestMethod -Uri $uriEnvironments -Method get -Headers $AzureDevOpsAuthenticationHeader
                $theEnvironment = $EnvironmentsResult.value | Where-Object name -eq $($Environments[$sel - 1].EnvironmentName)
                $body = @(
                    @{
                        type = "queue"
                        id   = "1"
                        name = "Default"
                    },
                    @{
                        type = "environment"
                        id   = "$($theEnvironment.id)"
                        name = "$($theEnvironment.name)"
                    }
                ) | ConvertTo-Json
                $uriEnvironmentChecks = $UriOrganization + "$($global:devops_projectFile.Project)/_apis/pipelines/checks/queryconfigurations?`$expand=settings&api-version=6.1-preview.1"
                $EnvironmentChecksResult = Invoke-RestMethod -Uri $uriEnvironmentChecks -Method Post -Body $body -Headers $AzureDevOpsAuthenticationHeader -ContentType application/json
                    
            }
            catch {
                #Do Nothing
            }

            if (!$EnvironmentChecksResult.count -gt 0) {
                if ($global:devops_DataverseCredType -eq "servicePrincipal") {
                    Write-Host "Using Service Principal"
                    Start-DeploySolution -DeployServerUrl $Environments[$sel - 1].EnvironmentURL -UserName $global:devops_ClientID -Password "$global:clientSecret" -PipelinePath $global:devops_projectLocation -UseClientSecret $true -EnvironmentName $Environments[$sel - 1].EnvironmentName -RunLocally $true   
                }
                else {
                    Write-Host "Using User Credentials"
                    Start-DeploySolution -DeployServerUrl $Environments[$sel - 1].EnvironmentURL -UserName $global:devops_DataverseEmail -Password "" -PipelinePath $global:devops_projectLocation -UseClientSecret $false -EnvironmentName $Environments[$sel - 1].EnvironmentName -RunLocally $true
                }
            }
            else {
                Write-Host "Environment $($Environments[$sel -1].EnvironmentName) requires approval, please run Deployment via the Pipeline"
            }
            pause
        }   
    }
    catch {
        Write-Error $_
        pause
    }
}

function Get-PublishedModuleVersion($Name) {
    # access the main module page, and add a random number to trick proxies
    $url = "https://www.powershellgallery.com/packages/$Name/?dummy=$(Get-Random)"
    $request = [System.Net.WebRequest]::Create($url)
    # do not allow to redirect. The result is a "MovedPermanently"
    $request.AllowAutoRedirect = $false
    try {
        # send the request
        $response = $request.GetResponse()
        # get back the URL of the true destination page, and split off the version
        $response.GetResponseHeader("Location").Split("/")[-1] -as [Version]
        # make sure to clean up
        $response.Close()
        $response.Dispose()
    }
    catch {
        Write-Warning $_.Exception.Message
    }
}

function Get-CurrentISODateTime {
    $date = Get-Date
    return $date.ToString("yyyy-MM-dd HH:mm:ss")
}

function Write-PPDOMessage {
    [CmdletBinding()]
    param (
        [Parameter()] 
        [string] $Message,
        [Parameter()]
        [ValidateSet('group', 'warning', 'error', 'section', 'debug', 'command', 'endgroup')]
        [string] $Type,
        [Parameter()] 
        [bool] $LogError = $false,
        [Parameter()] 
        [bool] $LogWarning = $false,
        [Parameter()] 
        [bool] $RunLocally = $false
    )
    switch ($Type) {
        'group' { 
            if ($RunLocally) {
                Write-Host $Message -BackgroundColor Magenta
            }
            else {
                Write-Host "##[group]$Message"
            }
        } 'warning' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor DarkYellow
            }
            else {
                if ($LogWarning) {
                    Write-Host "##vso[task.logissue type=warning]$Message"
                }
                else {
                    Write-Host "##[warning]$Message"
                }                
            }
        } 'error' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Red
            }
            else {
                if ($LogError) {
                    Write-Host "##vso[task.logissue type=error]$Message"
                }
                else {
                    Write-Host "##[error]$Message"
                }                
            }
        } 'section' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Green
            }
            else {
                Write-Host "##[section]$Message"
            }
        } 'debug' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Magenta
            }
            else {
                Write-Host "##[debug]$Message"
            }
        } 'command' {
            if ($RunLocally) {
                Write-Host $Message -ForegroundColor Blue
            }
            else {
                Write-Host "##[command]$Message"
            }
        } 'endgroup' {
            if ($RunLocally) {
                Write-Host ":END:" -BackgroundColor Magenta
            }
            else {
                Write-Host "##[endgroup]"
            }
        }
        Default {
            Write-Host $Message
        }
    }
}

function Add-7zip([String] $aDirectory, [String] $aZipfile) {
    [string]$pathToZipExe = "$($Env:ProgramFiles)\7-Zip\7z.exe";
    [Array]$arguments = "a", "-tzip", "$aZipfile", "$aDirectory", "-r";
    Remove-Item $aZipfile -ErrorAction SilentlyContinue
    & $pathToZipExe $arguments;
}