Artifacts/windows-execute-powershell-script/Common.psm1 (737 lines of code) (raw):

function Add-DirectoryToAzureArtifactsDrop { param ( # The URL of the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [uri] $DropServiceURL, # The access token for authenticating with the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [securestring] $AccessToken, # The directory to add to the drop. [Parameter(Mandatory)] [System.IO.DirectoryInfo] $Directory, # The name of the drop to finalize. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $DropName, # Lower case the file paths for compatibility across different operating systems. [Parameter()] [switch] $LowerCasePaths, # Timeout of this operation. [Parameter()] [ValidateNotNull()] [timespan] $Timeout = '0:0:5:0', # 5 minutes # Minimum trace detail 'info', 'warn', 'error', 'verbose'. [Parameter()] [ValidateSet('Info', 'Warn', 'Error', 'Verbose')] [string] $TraceLevel = 'Verbose', # Trace destination file path or 'console' if null. [Parameter()] [System.IO.FileInfo] $TraceTo = $null ) $arguments = "--name '$DropName' --directory '$Directory'" if ($LowerCasePaths) { $arguments += ' --LowercasePaths' } Invoke-DropExe -DropServiceURL $DropServiceURL -AccessToken $AccessToken -Command Publish -Arguments $arguments -Timeout $Timeout -TraceLevel $TraceLevel -TraceTo $TraceTo } function Complete-ArtifactLogsDrop { [CmdletBinding()] param ( [Parameter(Mandatory)] [uri] $LogsDropServiceURL, [Parameter(Mandatory)] [string] $LogsDropName, [Parameter(Mandatory)] [securestring] $AccessToken ) # DTL only captures the last few thousand characters from stdout in its artifact logs. Let's not pollute them with logging from the logic that publishes logs to a drop. $VerbosePreference = 'SilentlyContinue' $DebugPreference = 'SilentlyContinue' $InformationPreference = 'SilentlyContinue' $WarningPreference = 'Continue' $ErrorActionPreference = 'Stop' $LogsDropName = [System.Environment]::ExpandEnvironmentVariables($LogsDropName) $dropExeLogPath = Join-Path $env:TEMP "drop.exe.$(Get-Date -Format FileDateTimeUniversal).log" try { Complete-AzureArtifactsDrop -DropServiceURL $LogsDropServiceURL -DropName $LogsDropName -AccessToken $AccessToken -TraceTo $dropExeLogPath } catch { Write-Warning "An exeception occurred while attempting to finalize drop '$LogsDropName'. For more information, see the logs at '$dropExeLogPath'. Exception: $($_.Exception.ToString())" } } function Complete-AzureArtifactsDrop { param ( # The URL of the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [uri] $DropServiceURL, # The access token for authenticating with the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [securestring] $AccessToken, # The name of the drop to finalize. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $DropName, # Timeout of this operation. [Parameter()] [ValidateNotNull()] [timespan] $Timeout = '0:0:5:0', # 5 minutes # Minimum trace detail 'info', 'warn', 'error', 'verbose'. [Parameter()] [ValidateSet('Info', 'Warn', 'Error', 'Verbose')] [string] $TraceLevel = 'Verbose', # Trace destination file path or 'console' if null. [Parameter()] [System.IO.FileInfo] $TraceTo = $null ) $arguments = "--name '$DropName'" Invoke-DropExe -DropServiceURL $DropServiceURL -AccessToken $AccessToken -Command Finalize -Arguments $arguments -Timeout $Timeout -TraceLevel $TraceLevel -TraceTo $TraceTo } function ConvertTo-UnsecureString { param ( [Parameter(Mandatory, Position = 0)] [securestring] $SecureString ) try { $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString) return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) } finally { if ($null -ne $BSTR) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) } } } function Format-AzureArtifactsLogsDropBrowserURL { param ( # Azure DevOps organization URL [Parameter(Mandatory)] [uri] $LogsDropServiceURL, # Drop name. [Parameter(Mandatory)] [string] $LogsDropName ) $OrganizationURL = $LogsDropServiceURL -replace 'https:\/\/artifacts\.', 'https://' $LogsDropName = [System.Environment]::ExpandEnvironmentVariables($LogsDropName) $azureArtifactsDropBrowserURL = "$OrganizationURL/_apps/hub/ms-vscs-artifact.build-tasks.drop-hub-group-explorer-hub?name=$LogsDropName" return $azureArtifactsDropBrowserURL } # Get access token using an Azure Managed Identity logged in to az accounts. function Get-AccessTokenUsingManagedIdentity { [CmdletBinding()] [OutputType([securestring])] param ( # The ClientID of an Azure Managed Identity [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ClientID ) try { Invoke-CommandWithRetry { # This is the Azure DevOps resource ID $endpointURL = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=499b84ac-1321-427f-aa17-267ca6975798&client_id=$ClientID" $response = Invoke-WebRequest -Uri $endpointURL -Headers @{ Metadata = "true" } -UseBasicParsing $content = $response.Content | ConvertFrom-Json $accessToken = ConvertTo-SecureString -String $content.access_token -AsPlainText -Force return $accessToken } } catch { throw "Failed to get access token using managed identity. Exception: $($_.Exception.ToString())" } } # Computes the path to the latest version of drop.exe and downloads it if not present on the local machine. function Get-DropExePath { param ( # The URL of the drop service. [Parameter(Mandatory)] [uri] $DropServiceURL ) # Look up the latest version of the drop.exe client $localdir = "$env:SystemDrive\DevTestLabs\Drop.App" Invoke-CommandWithRetry { $response = Invoke-WebRequest -Method Head -Uri "$DropServiceURL/_apis/drop/client/" -UseDefaultCredentials -UseBasicParsing $version = $response.Headers["drop-client-version"] $localdir = Join-Path $localdir $version } $dropExePath = Join-Path $localdir 'lib\net45\drop.exe' Write-Verbose "Computed drop.exe path as '$dropExePath'." if (-not (Test-Path $dropExePath)) { Write-Verbose "The drop.exe client was not found at '$dropExePath'. Attempting to install..." $zip = Join-Path $env:TEMP "$(New-Guid).zip" try { Write-Verbose "Downloading '$zip'..." $oldProgressPreference = $ProgressPreference $ProgressPreference = "SilentlyContinue" Invoke-CommandWithRetry { Invoke-WebRequest -Uri "$DropServiceURL/_apis/drop/client/exe" -UseDefaultCredentials -OutFile $zip } $ProgressPreference = $oldProgressPreference Write-Information "Successfully downloaded '$zip'." if (Test-Path $localdir) { Remove-Item -Path $localdir -Recurse -Force } New-Item -Path $localdir -ItemType Directory | Out-Null Expand-Archive -Path $zip -DestinationPath $localdir } finally { if (Test-Path $zip) { try { Remove-Item -Path $zip -Force } catch { Write-Warning "Unable to clean up temporary file '$zip': $_" } } } Write-Information "Successfully installed drop.exe client to '$dropExePath'." } return $dropExePath } function Initialize-Artifact { $env:DevTestLabsArtifactsPath = "$env:SystemDrive\DevTestLabs\Artifacts" } function Install-AzureArtifactsDrop { param ( # The URL of the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [uri] $DropServiceURL, # The access token for authenticating with the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [securestring] $AccessToken, # The name of the drop to finalize. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $DropName, # Directory to store the downloaded drop. [Parameter(Mandatory)] [System.IO.DirectoryInfo] $DestinationDirectory, # Timeout of this operation. [Parameter()] [ValidateNotNull()] [timespan] $Timeout = '0:0:5:0', # 5 minutes # Minimum trace detail 'info', 'warn', 'error', 'verbose'. [Parameter()] [ValidateSet('Info', 'Warn', 'Error', 'Verbose')] [string] $TraceLevel = 'Verbose', # Trace destination file path or 'console' if null. [Parameter()] [System.IO.FileInfo] $TraceTo = $null ) $arguments = "--name '$DropName' --dest '$DestinationDirectory'" Invoke-DropExe -DropServiceURL $DropServiceURL -AccessToken $AccessToken -Command Get -Arguments $arguments -Timeout $Timeout -TraceLevel $TraceLevel -TraceTo $TraceTo } function Invoke-CommandWithRetry { [CmdletBinding()] param ( # Command(s) to run. [Parameter(Mandatory)] [scriptblock] $ScriptBlock, # Number of attempts. [Parameter()] [int] $Attempts = 3, # Base delay between retries. [Parameter()] [timespan] $DelayInterval = '0:0:0:5', # 5 seconds # Whether or not to employ exponential backoff. Use this calculator to model the retries with your inputs: http://backoffcalculator.com/ [Parameter()] [switch] $ExponentialBackoff ) begin { $attempt = 1 } process { do { try { Invoke-Command -ScriptBlock $ScriptBlock -NoNewScope return } catch { if ($attempt -ge $Attempts) { throw $_ } else { $delay = $DelayInterval if ($ExponentialBackoff) { $delay = [timespan]::FromMilliseconds(([Math]::Pow(2, $attempt) - 1) * $DelayInterval.TotalMilliseconds) } Write-Verbose "Attempt #$attempt failed. Retrying in $($delay.TotalSeconds) second(s)..." $attempt++ Start-Sleep -Milliseconds $delay.TotalMilliseconds } } } while ($true) } } function Invoke-DropExe { param ( # The URL of the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [uri] $DropServiceURL, # The access token for authenticating with the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [securestring] $AccessToken, # The drop.exe command to execute. [Parameter(Mandatory)] [ValidateSet('Create', 'Delete', 'Dir', 'Domain', 'Finalize', 'Get', 'List', 'Publish', 'Update')] [string] $Command, # The arguments to pass to drop.exe. [Parameter()] [string] $Arguments = '', # Timeout of this operation. [Parameter()] [ValidateNotNull()] [timespan] $Timeout = '0:0:5:0', # 5 minutes # Minimum trace detail 'info', 'warn', 'error', 'verbose'. [Parameter()] [ValidateSet('Info', 'Warn', 'Error', 'Verbose')] [string] $TraceLevel = 'Verbose', # Trace destination file path or 'console' if null. [Parameter()] [System.IO.FileInfo] $TraceTo = $null ) if ($DropServiceURL.Host -eq 'dev.azure.com') { $DropServiceURL = [uri]"$($DropServiceURL.Scheme)://artifacts.$($DropServiceURL.Host)$($DropServiceURL.PathAndQuery)" } elseif ($DropServiceURL.Host -like '*.visualstudio.com') { $parts = $DropServiceURL.Host -split '\.' if ($parts.Length -eq 3) { $DropServiceURL = [uri]"$($DropServiceURL.Scheme)://$($parts[0]).artifacts.$($parts[1]).$($parts[2])$($DropServiceURL.PathAndQuery)" } } $patAuthEnvVarName = (New-Guid).ToString() New-Item "env:$patAuthEnvVarName" -Value (ConvertTo-UnsecureString -SecureString $AccessToken) | Out-Null $dropExePath = Get-DropExePath -DropServiceURL $DropServiceURL Invoke-Expression "& '$dropExePath' $Command --dropservice '$DropServiceURL' --patAuthEnvVar '$patAuthEnvVarName' --timeout '$($Timeout.TotalMinutes)' --tracelevel '$TraceLevel' --traceto '$(if ($TraceTo) { $TraceTo } else { 'console' })' $Arguments" switch ($LASTEXITCODE) { 0 { Write-Information "The call to drop.exe succeeded." } default { throw "The call to drop.exe failed with exit code $($LASTEXITCODE)." } } } function New-AzureArtifactsDrop { param ( # The URL of the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [uri] $DropServiceURL, # The access token for authenticating with the drop service. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [securestring] $AccessToken, # The name of the drop to create. [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $DropName, # An expiration date for the drop. [Parameter()] [datetime] $Expiration = ([datetime]::MaxValue), # Timeout of this operation. [Parameter()] [ValidateNotNull()] [timespan] $Timeout = '0:0:5:0', # 5 minutes # Minimum trace detail 'info', 'warn', 'error', 'verbose'. [Parameter()] [ValidateSet('Info', 'Warn', 'Error', 'Verbose')] [string] $TraceLevel = 'Verbose', # Trace destination file path or 'console' if null. [Parameter()] [System.IO.FileInfo] $TraceTo = $null ) $arguments = "--name '$DropName'" if ($Expiration -ne [datetime]::MaxValue) { $arguments += " --expirationDate '$Expiration'" } Invoke-DropExe -DropServiceURL $DropServiceURL -AccessToken $AccessToken -Command Create -Arguments $arguments -Timeout $Timeout -TraceLevel $TraceLevel -TraceTo $TraceTo } function New-FilesSnapshot { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $IncludeGlobPatterns, [string] $ExcludeGlobPatterns, [Parameter(Mandatory)] [System.IO.DirectoryInfo] $SearchDirectory, # Must be absolute path [Parameter(Mandatory)] [System.IO.DirectoryInfo] $SnapshotDestinationDirectory, [Parameter(Mandatory)] [string] $SnapshotName ) # DTL only captures the last few thousand characters from stdout in its artifact logs. Let's not pollute them with logging from the logic that publishes logs to a drop. $VerbosePreference = 'SilentlyContinue' $DebugPreference = 'SilentlyContinue' $InformationPreference = 'SilentlyContinue' $WarningPreference = 'Continue' $ErrorActionPreference = 'Stop' try { if ($SnapshotDestinationDirectory.Root.Name -ne $SearchDirectory.Root.Name) { Throw "Snapshot destination drive '$($SnapshotDestinationDirectory.Root.Name)' is not equal to search drive '$($SearchDirectory.Root.Name)' The snapshot destination must be on the same drive as the directory being searched" } $snapshotDirectory = New-Item -Type Directory -Path (Join-Path $SnapshotDestinationDirectory $SnapshotName) -Force if ($IncludeGlobPatterns) { $includeGlobArray = $IncludeGlobPatterns.split(';') } else { throw "Must supply IncludeGlobPatterns" } if ($ExcludeGlobPatterns) { $excludeGlobArray = $ExcludeGlobPatterns.split(';') } $filePathsToCopy = Search-File -IncludeGlobPatterns $includeGlobArray -ExcludeGlobPatterns $excludeGlobArray -SearchDirectory $SearchDirectory foreach ($path in $filePathsToCopy) { try { try { $relativePath = Split-Path $path.FullName -NoQualifier $destinationPath = New-Item -Path (Join-Path $snapshotDirectory $relativePath) -Force Copy-Item -Destination $destinationPath -Path $path.FullName -Force } catch [System.ComponentModel.Win32Exception] { Write-Warning "An exception occured while copying $(Join-Path $SearchDirectory $path) to $destinationPath, attempting to hash the path to make it shorter. Exception: $($_.Exception.ToString())" $pathName = Split-Path -Path $path $pathHash = '{0:x}' -f $pathName.GetHashCode() $fileName = "$pathHash-" + (Split-Path -Path $path -Leaf) $overflowDirectory = Join-Path $snapshotDirectory "MaxFilePathReached" $destinationPath = New-Item -Path (Join-Path $overflowDirectory $fileName) -Force Copy-Item -Destination $destinationPath -Path (Join-Path $SearchDirectory $path) -Force } } catch { Write-Warning "An exception occured while copying $(Join-Path $SearchDirectory $path) to $destinationPath. Exception: $($_.Exception.ToString())" } } } catch { Write-Warning "An exeception occurred while attempting to snapshot (Include:'$IncludeGlobPatterns', Exclude:'$ExcludeGlobPatterns') into '$SnapshotDestinationDirectory' with name '$SnapshotName'. Exception: $($_.Exception.ToString())" } } function Publish-ArtifactLogs { [CmdletBinding()] param ( [Parameter(Mandatory)] [uri] $LogsDropServiceURL, [Parameter(Mandatory)] [string] $LogsDropName, [Parameter(Mandatory)] [System.IO.DirectoryInfo] $LogsDirectory, [Parameter(Mandatory)] [securestring] $AccessToken ) # DTL only captures the last few thousand characters from stdout in its artifact logs. Let's not pollute them with logging from the logic that publishes logs to a drop. $VerbosePreference = 'SilentlyContinue' $DebugPreference = 'SilentlyContinue' $InformationPreference = 'SilentlyContinue' $WarningPreference = 'Continue' $ErrorActionPreference = 'Stop' $LogsDropName = [System.Environment]::ExpandEnvironmentVariables($LogsDropName) $dropExeLogPath = Join-Path $env:TEMP "drop.exe.$(Get-Date -Format FileDateTimeUniversal).log" try { Add-DirectoryToAzureArtifactsDrop -DropServiceURL $LogsDropServiceURL -DropName $LogsDropName -Directory $LogsDirectory -AccessToken $AccessToken -TraceTo $dropExeLogPath -ErrorAction 'Continue' } catch { try { # Publish for the first script will fail because drop does not exist yet, so create it $expiration = (Get-Date).AddMonths(1) New-AzureArtifactsDrop -DropServiceURL $LogsDropServiceURL -DropName $LogsDropName -Expiration $expiration -AccessToken $AccessToken -TraceTo $dropExeLogPath # Try again, now that the drop is created Add-DirectoryToAzureArtifactsDrop -DropServiceURL $LogsDropServiceURL -DropName $LogsDropName -Directory $LogsDirectory -AccessToken $AccessToken -TraceTo $dropExeLogPath -ErrorAction 'Continue' } catch { Write-Warning "An exeception occurred while attempting to upload directory '$LogsDirectory' to drop '$LogsDropName'. For more information, see the logs at '$dropExeLogPath'. Exception: $($_.Exception.ToString())" } } } function Search-File { param ( # The directory to search. [Parameter(Mandatory)] [System.IO.DirectoryInfo] $SearchDirectory, # Glob patterns to include. [Parameter(Mandatory)] [string[]] $IncludeGlobPatterns, # Glob patterns to exclude. [Parameter()] [string[]] $ExcludeGlobPatterns = $null ) $includeFileSystemPaths = @() foreach ($glob in $IncludeGlobPatterns) { $includeFileSystemPaths += Search-FileSystemRecurse -Directory $SearchDirectory -GlobParts (Split-GlobStar -Glob $glob) } $includeFileSystemPaths = $includeFileSystemPaths | Select-Object -Unique $excludeFileSystemPaths = @() foreach ($glob in $ExcludeGlobPatterns) { $excludeFileSystemPaths += Search-FileSystemRecurse -Directory $SearchDirectory -GlobParts (Split-GlobStar -Glob $glob) } $excludeFileSystemPaths = $excludeFileSystemPaths | Select-Object -Unique # Get the relative complement of excluded files in included files. $fileSystemPaths = $includeFileSystemPaths | Where-Object { $excludeFileSystemPaths -NotContains $_ } $results = @() foreach ($fileSystemPath in $fileSystemPaths) { # We only want files, no directories. if (Test-Path $fileSystemPath -PathType Leaf) { $results += [System.IO.FileInfo]$fileSystemPath } } return $results } # Returns the resolved paths of file system entries under the given directory that match the given glob parts. # Results may include directories and files and contain duplicates. function Search-FileSystemRecurse { param ( # The path [Parameter(Mandatory)] [System.IO.DirectoryInfo] $Directory, # The glob [Parameter(Mandatory)] [string[]] $GlobParts ) $glob, $globRemainder = $GlobParts $result = @() if ($glob -eq '**') { foreach ($subdirectory in $Directory.EnumerateDirectories()) { $result += Search-FileSystemRecurse -Directory $subdirectory -GlobParts $GlobParts } $result += Search-FileSystemRecurse -Directory $Directory -GlobParts $(if ($globRemainder) { $globRemainder } else { '*' }) } else { $matchingPaths = @(Resolve-Path -Path (Join-Path $Directory.FullName $glob) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path) if ($globRemainder) { foreach ($path in $matchingPaths) { Search-FileSystemRecurse -Directory $path -GlobParts $globRemainder } } else { $result += $matchingPaths } } return $result } # What is a globstar? According to the GNU organization, it is the ** pattern used to "match all files and zero or more directories and subdirectories". # https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html function Split-GlobStar { param ( # Parameter help description [Parameter(Mandatory)] [string] $Glob ) $normalizedGlob = $Glob.Replace('/', '\') # A glob ending in '\' should be treated as a globstar, according to https://learn.microsoft.com/en-us/dotnet/core/extensions/file-globbing#pattern-formats if ($normalizedGlob.EndsWith('\')) { $normalizedGlob += '**' } $parts = $normalizedGlob.Split('\', [StringSplitOptions]::RemoveEmptyEntries) $result = @() $currentPart = $null foreach ($part in $parts) { if ($part -eq '**') { if ($currentPart) { $result += $currentPart $currentPart = $null } # Following logic removes multiple consecutive globstar (**) parts from the string because they are functionally equivalent and computationally intensive. # For example, '**/**' is functionally equivalent to '**'. if ((Select-Object $result -Last 1) -ne '**') { $result += $part } } else { if ($currentPart) { $currentPart = Join-Path $currentPart $part } else { $currentPart = $part } } } if ($currentPart) { $result += $currentPart } return $result } function Start-ArtifactLogging { [CmdletBinding()] param ( [Parameter(Mandatory)] [System.IO.FileInfo] $LogFile ) # Disable all confirmation dialogs because they can block automation $global:ConfirmPreference = 'None' $global:DebugPreference = 'Continue' $global:VerbosePreference = 'Continue' $global:InformationPreference = 'SilentlyContinue' # Due a bug in Windows PowerShell, Write-Information will only appear in transcripts if InformationPreference is set to 'SilentlyContinue' $global:WarningPreference = 'Continue' $global:ErrorActionPreference = 'Stop' # Set up transcript if (-not (Test-Path -Path $LogFile.Directory -PathType Container)) { New-Item -Path $LogFile.Directory -ItemType Directory -Force | Out-Null } Start-Transcript -Path $LogFile } function Stop-ArtifactLogging { [CmdletBinding()] param ( [Parameter(Mandatory)] [System.IO.FileInfo] $LogFile ) Stop-Transcript # Adjust log levels to not pollute StOut with messages unrelated to the actual artifact execution $global:DebugPreference = 'SilentlyContinue' $global:VerbosePreference = 'SilentlyContinue' $global:InformationPreference = 'SilentlyContinue' $global:WarningPreference = 'Continue' $global:ErrorActionPreference = 'Continue' # Need to remove the invocation header from the log file because it may contain secrets. $tempLogFile = New-TemporaryFile Move-Item -Path $LogFile -Destination $tempLogFile -Force Get-Content -Path $tempLogFile | Where-Object { $_ -NotMatch "^Host Application:" } | Set-Content -Path $logFile Remove-Item -Path $tempLogFile -Force } function Test-RebootPending { return (Test-Path (Join-Path $env:DevTestLabsArtifactsPath 'RebootPending.sem')) } function Write-ToArtifactOrchestrator { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $message, [Parameter(Mandatory)] [ValidateSet('Debug', 'Information', 'Warning', 'Error')] [string] $logLevel ) Write-Host "##DevTestLabs(LogLevel=$logLevel;Message=$message)" } # Log must appear in the last 50 lines of StOut to be read by the artifact orchestrator function Write-ErrorToArtifactOrchestrator { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $message ) Write-ToArtifactOrchestrator -Message $message -LogLevel "Error" } # Log must appear in the last 50 lines of StOut to be read by the artifact orchestrator function Write-DebugToArtifactOrchestrator { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $message ) Write-ToArtifactOrchestrator -Message $message -LogLevel "Debug" } # Log must appear in the last 50 lines of StOut to be read by the artifact orchestrator function Write-InformationToArtifactOrchestrator { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $message ) Write-ToArtifactOrchestrator -Message $message -LogLevel "Information" } # Log must appear in the last 50 lines of StOut to be read by the artifact orchestrator function Write-WarningToArtifactOrchestrator { [CmdletBinding()] param ( [Parameter(Mandatory)] [string] $message ) Write-ToArtifactOrchestrator -Message $message -LogLevel "Warning" } Export-ModuleMember -Function Add-DirectoryToAzureArtifactsDrop Export-ModuleMember -Function Complete-ArtifactLogsDrop Export-ModuleMember -Function Complete-AzureArtifactsDrop Export-ModuleMember -Function Format-AzureArtifactsLogsDropBrowserURL Export-ModuleMember -Function Get-AccessTokenUsingManagedIdentity Export-ModuleMember -Function Initialize-Artifact Export-ModuleMember -Function Install-AzureArtifactsDrop Export-ModuleMember -Function New-AzureArtifactsDrop Export-ModuleMember -Function New-FilesSnapshot Export-ModuleMember -Function Publish-ArtifactLogs Export-ModuleMember -Function Search-File Export-ModuleMember -Function Start-ArtifactLogging Export-ModuleMember -Function Stop-ArtifactLogging Export-ModuleMember -Function Test-RebootPending Export-ModuleMember -Function Write-ErrorToArtifactOrchestrator Export-ModuleMember -Function Write-DebugToArtifactOrchestrator Export-ModuleMember -Function Write-InformationToArtifactOrchestrator Export-ModuleMember -Function Write-WarningToArtifactOrchestrator