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; } |