microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/lib/Modules/CIHelpers.psm1
609lines · modecode
| 1 | # Copyright (c) Microsoft Corporation. |
| 2 | # SPDX-License-Identifier: MIT |
| 3 | |
| 4 | # CIHelpers.psm1 |
| 5 | # |
| 6 | # Purpose: Shared CI platform detection and output utilities for hve-core scripts. |
| 7 | # Author: HVE Core Team |
| 8 | |
| 9 | #Requires -Version 7.0 |
| 10 | |
| 11 | function ConvertTo-GitHubActionsEscaped { |
| 12 | <# |
| 13 | .SYNOPSIS |
| 14 | Escapes a string for safe use in GitHub Actions workflow commands. |
| 15 | |
| 16 | .DESCRIPTION |
| 17 | Percent-encodes characters that have special meaning in GitHub Actions |
| 18 | logging commands to prevent workflow command injection attacks. |
| 19 | |
| 20 | .PARAMETER Value |
| 21 | The string to escape. |
| 22 | |
| 23 | .PARAMETER ForProperty |
| 24 | If set, also escapes colon and comma characters used in property values. |
| 25 | #> |
| 26 | [CmdletBinding()] |
| 27 | [OutputType([string])] |
| 28 | param( |
| 29 | [Parameter(Mandatory = $true)] |
| 30 | [AllowEmptyString()] |
| 31 | [string]$Value, |
| 32 | |
| 33 | [Parameter(Mandatory = $false)] |
| 34 | [switch]$ForProperty |
| 35 | ) |
| 36 | |
| 37 | if ([string]::IsNullOrEmpty($Value)) { |
| 38 | return $Value |
| 39 | } |
| 40 | |
| 41 | # Order matters: escape % first to avoid double-encoding |
| 42 | $escaped = $Value -replace '%', '%25' |
| 43 | $escaped = $escaped -replace "`r", '%0D' |
| 44 | $escaped = $escaped -replace "`n", '%0A' |
| 45 | # Escape :: patterns to neutralize command sequences (defense in depth) |
| 46 | # This prevents ::command:: patterns. When ForProperty is false, single colons like C:\ are preserved. |
| 47 | $escaped = $escaped -replace '::', '%3A%3A' |
| 48 | |
| 49 | if ($ForProperty) { |
| 50 | $escaped = $escaped -replace ':', '%3A' |
| 51 | $escaped = $escaped -replace ',', '%2C' |
| 52 | } |
| 53 | |
| 54 | return $escaped |
| 55 | } |
| 56 | |
| 57 | function ConvertTo-AzureDevOpsEscaped { |
| 58 | <# |
| 59 | .SYNOPSIS |
| 60 | Escapes a string for safe use in Azure DevOps logging commands. |
| 61 | |
| 62 | .DESCRIPTION |
| 63 | Percent-encodes characters that have special meaning in Azure DevOps |
| 64 | logging commands to prevent workflow command injection attacks. |
| 65 | |
| 66 | .PARAMETER Value |
| 67 | The string to escape. |
| 68 | |
| 69 | .PARAMETER ForProperty |
| 70 | If set, also escapes semicolon and bracket characters used in property values. |
| 71 | #> |
| 72 | [CmdletBinding()] |
| 73 | [OutputType([string])] |
| 74 | param( |
| 75 | [Parameter(Mandatory = $true)] |
| 76 | [AllowEmptyString()] |
| 77 | [string]$Value, |
| 78 | |
| 79 | [Parameter(Mandatory = $false)] |
| 80 | [switch]$ForProperty |
| 81 | ) |
| 82 | |
| 83 | if ([string]::IsNullOrEmpty($Value)) { |
| 84 | return $Value |
| 85 | } |
| 86 | |
| 87 | # Order matters: escape % first to avoid double-encoding |
| 88 | $escaped = $Value -replace '%', '%AZP25' |
| 89 | $escaped = $escaped -replace "`r", '%AZP0D' |
| 90 | $escaped = $escaped -replace "`n", '%AZP0A' |
| 91 | # Escape brackets to prevent ##vso[ command patterns (defense in depth) |
| 92 | $escaped = $escaped -replace '\[', '%AZP5B' |
| 93 | $escaped = $escaped -replace '\]', '%AZP5D' |
| 94 | |
| 95 | if ($ForProperty) { |
| 96 | $escaped = $escaped -replace ';', '%AZP3B' |
| 97 | } |
| 98 | |
| 99 | return $escaped |
| 100 | } |
| 101 | |
| 102 | function Get-CIPlatform { |
| 103 | <# |
| 104 | .SYNOPSIS |
| 105 | Detects the current CI platform. |
| 106 | |
| 107 | .DESCRIPTION |
| 108 | Returns the CI platform identifier based on environment variables. |
| 109 | Supports GitHub Actions, Azure DevOps, and local development. |
| 110 | |
| 111 | .OUTPUTS |
| 112 | System.String - 'github', 'azdo', or 'local' |
| 113 | #> |
| 114 | [CmdletBinding()] |
| 115 | [OutputType([string])] |
| 116 | param() |
| 117 | |
| 118 | if ($env:GITHUB_ACTIONS -eq 'true') { |
| 119 | return 'github' |
| 120 | } |
| 121 | if ($env:TF_BUILD -eq 'True' -or $env:AZURE_PIPELINES -eq 'True') { |
| 122 | return 'azdo' |
| 123 | } |
| 124 | return 'local' |
| 125 | } |
| 126 | |
| 127 | function Test-CIEnvironment { |
| 128 | <# |
| 129 | .SYNOPSIS |
| 130 | Tests whether running in a CI environment. |
| 131 | |
| 132 | .DESCRIPTION |
| 133 | Returns true if running in GitHub Actions or Azure DevOps. |
| 134 | |
| 135 | .OUTPUTS |
| 136 | System.Boolean - $true if in CI, $false otherwise |
| 137 | #> |
| 138 | [CmdletBinding()] |
| 139 | [OutputType([bool])] |
| 140 | param() |
| 141 | |
| 142 | return (Get-CIPlatform) -ne 'local' |
| 143 | } |
| 144 | |
| 145 | function Set-CIOutput { |
| 146 | <# |
| 147 | .SYNOPSIS |
| 148 | Sets a CI output variable. |
| 149 | |
| 150 | .DESCRIPTION |
| 151 | Sets an output variable that can be consumed by subsequent workflow steps. |
| 152 | Uses GITHUB_OUTPUT for GitHub Actions and task.setvariable for Azure DevOps. |
| 153 | |
| 154 | .PARAMETER Name |
| 155 | The variable name. |
| 156 | |
| 157 | .PARAMETER Value |
| 158 | The variable value. |
| 159 | |
| 160 | .PARAMETER IsOutput |
| 161 | For Azure DevOps, marks the variable as an output variable. |
| 162 | #> |
| 163 | [CmdletBinding()] |
| 164 | param( |
| 165 | [Parameter(Mandatory = $true)] |
| 166 | [string]$Name, |
| 167 | |
| 168 | [Parameter(Mandatory = $true)] |
| 169 | [string]$Value, |
| 170 | |
| 171 | [Parameter(Mandatory = $false)] |
| 172 | [switch]$IsOutput |
| 173 | ) |
| 174 | |
| 175 | $platform = Get-CIPlatform |
| 176 | |
| 177 | switch ($platform) { |
| 178 | 'github' { |
| 179 | if ($env:GITHUB_OUTPUT) { |
| 180 | # GITHUB_OUTPUT uses file-based output, less vulnerable but still escape newlines |
| 181 | $escapedName = ConvertTo-GitHubActionsEscaped -Value $Name |
| 182 | $escapedValue = ConvertTo-GitHubActionsEscaped -Value $Value |
| 183 | "$escapedName=$escapedValue" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 |
| 184 | } |
| 185 | else { |
| 186 | Write-Verbose "GITHUB_OUTPUT not set, would set: $Name=$Value" |
| 187 | } |
| 188 | } |
| 189 | 'azdo' { |
| 190 | $outputFlag = if ($IsOutput) { ';isOutput=true' } else { '' } |
| 191 | $escapedName = ConvertTo-AzureDevOpsEscaped -Value $Name -ForProperty |
| 192 | $escapedValue = ConvertTo-AzureDevOpsEscaped -Value $Value |
| 193 | Write-Output "##vso[task.setvariable variable=$escapedName$outputFlag]$escapedValue" |
| 194 | } |
| 195 | 'local' { |
| 196 | Write-Verbose "CI Output: $Name=$Value" |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | function Set-CIEnv { |
| 202 | <# |
| 203 | .SYNOPSIS |
| 204 | Sets a CI environment variable. |
| 205 | |
| 206 | .DESCRIPTION |
| 207 | Writes environment variables for GitHub Actions or Azure DevOps. |
| 208 | |
| 209 | .PARAMETER Name |
| 210 | The environment variable name. |
| 211 | |
| 212 | .PARAMETER Value |
| 213 | The environment variable value. |
| 214 | #> |
| 215 | [CmdletBinding()] |
| 216 | param( |
| 217 | [Parameter(Mandatory = $true)] |
| 218 | [string]$Name, |
| 219 | |
| 220 | [Parameter(Mandatory = $true)] |
| 221 | [string]$Value |
| 222 | ) |
| 223 | |
| 224 | $platform = Get-CIPlatform |
| 225 | |
| 226 | switch ($platform) { |
| 227 | 'github' { |
| 228 | if ($env:GITHUB_ENV) { |
| 229 | if ($Name -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { |
| 230 | throw "Invalid GitHub Actions environment variable name: '$Name'. Names must match '^[A-Za-z_][A-Za-z0-9_]*\$'." |
| 231 | } |
| 232 | |
| 233 | $delimiter = "EOF_$([guid]::NewGuid().ToString('N'))" |
| 234 | @( |
| 235 | "$Name<<$delimiter" |
| 236 | $Value |
| 237 | $delimiter |
| 238 | ) | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 |
| 239 | } |
| 240 | else { |
| 241 | Write-Verbose "GITHUB_ENV not set, would set: $Name=$Value" |
| 242 | } |
| 243 | } |
| 244 | 'azdo' { |
| 245 | $escapedName = ConvertTo-AzureDevOpsEscaped -Value $Name -ForProperty |
| 246 | $escapedValue = ConvertTo-AzureDevOpsEscaped -Value $Value |
| 247 | Write-Output "##vso[task.setvariable variable=$escapedName]$escapedValue" |
| 248 | } |
| 249 | 'local' { |
| 250 | Write-Verbose "CI Env: $Name=$Value" |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | function Write-CIStepSummary { |
| 256 | <# |
| 257 | .SYNOPSIS |
| 258 | Writes content to the CI step summary. |
| 259 | |
| 260 | .DESCRIPTION |
| 261 | Appends markdown content to the step summary for GitHub Actions. |
| 262 | For Azure DevOps, outputs as a section header and content. |
| 263 | |
| 264 | .PARAMETER Content |
| 265 | The markdown content to append. |
| 266 | |
| 267 | .PARAMETER Path |
| 268 | Path to a file containing markdown content. |
| 269 | #> |
| 270 | [CmdletBinding()] |
| 271 | param( |
| 272 | [Parameter(Mandatory = $true, ParameterSetName = 'Content')] |
| 273 | [string]$Content, |
| 274 | |
| 275 | [Parameter(Mandatory = $true, ParameterSetName = 'Path')] |
| 276 | [string]$Path |
| 277 | ) |
| 278 | |
| 279 | $platform = Get-CIPlatform |
| 280 | $markdown = if ($PSCmdlet.ParameterSetName -eq 'Path') { |
| 281 | Get-Content -Path $Path -Raw |
| 282 | } |
| 283 | else { |
| 284 | $Content |
| 285 | } |
| 286 | |
| 287 | switch ($platform) { |
| 288 | 'github' { |
| 289 | if ($env:GITHUB_STEP_SUMMARY) { |
| 290 | $markdown | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 |
| 291 | } |
| 292 | else { |
| 293 | Write-Verbose "GITHUB_STEP_SUMMARY not set" |
| 294 | Write-Verbose $markdown |
| 295 | } |
| 296 | } |
| 297 | 'azdo' { |
| 298 | Write-Output "##[section]Step Summary" |
| 299 | Write-Output $markdown |
| 300 | } |
| 301 | 'local' { |
| 302 | Write-Verbose "Step Summary:" |
| 303 | Write-Verbose $markdown |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | function Write-CIAnnotation { |
| 309 | <# |
| 310 | .SYNOPSIS |
| 311 | Writes a CI annotation (warning, error, notice). |
| 312 | |
| 313 | .DESCRIPTION |
| 314 | Creates a workflow annotation that appears in the GitHub Actions or Azure DevOps UI. |
| 315 | |
| 316 | .PARAMETER Message |
| 317 | The annotation message. |
| 318 | |
| 319 | .PARAMETER Level |
| 320 | The severity level: Warning, Error, or Notice. |
| 321 | |
| 322 | .PARAMETER File |
| 323 | Optional file path for file-level annotations. |
| 324 | |
| 325 | .PARAMETER Line |
| 326 | Optional line number for the annotation. |
| 327 | |
| 328 | .PARAMETER Column |
| 329 | Optional column number for the annotation. |
| 330 | #> |
| 331 | [CmdletBinding()] |
| 332 | param( |
| 333 | [Parameter(Mandatory = $true)] |
| 334 | [AllowEmptyString()] |
| 335 | [string]$Message, |
| 336 | |
| 337 | [Parameter(Mandatory = $false)] |
| 338 | [ValidateSet('Warning', 'Error', 'Notice')] |
| 339 | [string]$Level = 'Warning', |
| 340 | |
| 341 | [Parameter(Mandatory = $false)] |
| 342 | [string]$File, |
| 343 | |
| 344 | [Parameter(Mandatory = $false)] |
| 345 | [int]$Line, |
| 346 | |
| 347 | [Parameter(Mandatory = $false)] |
| 348 | [int]$Column |
| 349 | ) |
| 350 | |
| 351 | $platform = Get-CIPlatform |
| 352 | |
| 353 | switch ($platform) { |
| 354 | 'github' { |
| 355 | $levelLower = $Level.ToLower() |
| 356 | $annotation = "::$levelLower" |
| 357 | $params = @() |
| 358 | if ($File) { |
| 359 | $normalizedFile = $File -replace '\\', '/' |
| 360 | $escapedFile = ConvertTo-GitHubActionsEscaped -Value $normalizedFile -ForProperty |
| 361 | $params += "file=$escapedFile" |
| 362 | } |
| 363 | if ($Line -gt 0) { $params += "line=$Line" } |
| 364 | if ($Column -gt 0) { $params += "col=$Column" } |
| 365 | if ($params.Count -gt 0) { |
| 366 | $annotation += " $($params -join ',')" |
| 367 | } |
| 368 | $escapedMessage = ConvertTo-GitHubActionsEscaped -Value $Message |
| 369 | Write-Output "$annotation::$escapedMessage" |
| 370 | } |
| 371 | 'azdo' { |
| 372 | $typeMap = @{ |
| 373 | 'Warning' = 'warning' |
| 374 | 'Error' = 'error' |
| 375 | 'Notice' = 'info' |
| 376 | } |
| 377 | $adoType = $typeMap[$Level] |
| 378 | $annotation = "##vso[task.logissue type=$adoType" |
| 379 | if ($File) { |
| 380 | $escapedFile = ConvertTo-AzureDevOpsEscaped -Value $File -ForProperty |
| 381 | $annotation += ";sourcepath=$escapedFile" |
| 382 | } |
| 383 | if ($Line -gt 0) { $annotation += ";linenumber=$Line" } |
| 384 | if ($Column -gt 0) { $annotation += ";columnnumber=$Column" } |
| 385 | $escapedMessage = ConvertTo-AzureDevOpsEscaped -Value $Message |
| 386 | Write-Output "$annotation]$escapedMessage" |
| 387 | } |
| 388 | 'local' { |
| 389 | $prefix = switch ($Level) { |
| 390 | 'Warning' { 'WARNING' } |
| 391 | 'Error' { 'ERROR' } |
| 392 | 'Notice' { 'NOTICE' } |
| 393 | } |
| 394 | $location = if ($File) { " [$File" + $(if ($Line) { ":$Line" } else { '' }) + ']' } else { '' } |
| 395 | Write-Warning "$prefix$location $Message" |
| 396 | } |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | function Write-CIAnnotations { |
| 401 | <# |
| 402 | .SYNOPSIS |
| 403 | Writes CI annotations for summary results. |
| 404 | |
| 405 | .DESCRIPTION |
| 406 | Emits annotations for each issue in a summary object, mapping errors and warnings |
| 407 | to the platform-specific annotation formats. |
| 408 | |
| 409 | .PARAMETER Summary |
| 410 | Summary object containing Results with Issues and file metadata. |
| 411 | #> |
| 412 | [CmdletBinding()] |
| 413 | param( |
| 414 | [Parameter(Mandatory = $true)] |
| 415 | $Summary |
| 416 | ) |
| 417 | |
| 418 | if (-not $Summary -or -not $Summary.Results) { |
| 419 | return |
| 420 | } |
| 421 | |
| 422 | foreach ($result in $Summary.Results) { |
| 423 | if (-not $result -or -not $result.Issues) { |
| 424 | continue |
| 425 | } |
| 426 | |
| 427 | foreach ($issue in $result.Issues) { |
| 428 | if (-not $issue) { |
| 429 | continue |
| 430 | } |
| 431 | |
| 432 | # Skip issues with null or empty messages |
| 433 | if ([string]::IsNullOrWhiteSpace($issue.Message)) { |
| 434 | continue |
| 435 | } |
| 436 | |
| 437 | $level = if ($issue.Type -eq 'Error') { 'Error' } else { 'Warning' } |
| 438 | $line = if ($issue.Line -gt 0) { $issue.Line } else { 1 } |
| 439 | $filePath = if ($result.RelativePath) { $result.RelativePath } elseif ($issue.FilePath) { $issue.FilePath } else { $null } |
| 440 | |
| 441 | $annotationParams = @{ |
| 442 | Message = [string]$issue.Message |
| 443 | Level = $level |
| 444 | } |
| 445 | |
| 446 | if ($filePath) { |
| 447 | $annotationParams['File'] = [string]$filePath |
| 448 | $annotationParams['Line'] = $line |
| 449 | } |
| 450 | |
| 451 | if ($issue.Column -gt 0) { |
| 452 | $annotationParams['Column'] = $issue.Column |
| 453 | } |
| 454 | |
| 455 | Write-CIAnnotation @annotationParams |
| 456 | } |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | function Set-CITaskResult { |
| 461 | <# |
| 462 | .SYNOPSIS |
| 463 | Sets the CI task/step result status. |
| 464 | |
| 465 | .DESCRIPTION |
| 466 | Sets the overall result of the current task or step. |
| 467 | |
| 468 | .PARAMETER Result |
| 469 | The result status: Succeeded, SucceededWithIssues, or Failed. |
| 470 | #> |
| 471 | [CmdletBinding()] |
| 472 | param( |
| 473 | [Parameter(Mandatory = $true)] |
| 474 | [ValidateSet('Succeeded', 'SucceededWithIssues', 'Failed')] |
| 475 | [string]$Result |
| 476 | ) |
| 477 | |
| 478 | $platform = Get-CIPlatform |
| 479 | |
| 480 | switch ($platform) { |
| 481 | 'github' { |
| 482 | Write-Verbose "GitHub Actions task result: $Result" |
| 483 | if ($Result -eq 'Failed') { |
| 484 | Write-Output "::error::Task failed" |
| 485 | } |
| 486 | } |
| 487 | 'azdo' { |
| 488 | Write-Output "##vso[task.complete result=$Result]" |
| 489 | } |
| 490 | 'local' { |
| 491 | Write-Verbose "Task result: $Result" |
| 492 | } |
| 493 | } |
| 494 | } |
| 495 | |
| 496 | function Publish-CIArtifact { |
| 497 | <# |
| 498 | .SYNOPSIS |
| 499 | Publishes a CI artifact. |
| 500 | |
| 501 | .DESCRIPTION |
| 502 | Publishes a file or folder as a CI artifact. |
| 503 | For GitHub Actions, outputs the path for use with actions/upload-artifact. |
| 504 | For Azure DevOps, uses the artifact.upload command. |
| 505 | |
| 506 | .PARAMETER Path |
| 507 | The path to the file or folder to publish. |
| 508 | |
| 509 | .PARAMETER Name |
| 510 | The artifact name. |
| 511 | |
| 512 | .PARAMETER ContainerFolder |
| 513 | For Azure DevOps, the container folder path within the artifact. |
| 514 | #> |
| 515 | [CmdletBinding()] |
| 516 | param( |
| 517 | [Parameter(Mandatory = $true)] |
| 518 | [string]$Path, |
| 519 | |
| 520 | [Parameter(Mandatory = $true)] |
| 521 | [string]$Name, |
| 522 | |
| 523 | [Parameter(Mandatory = $false)] |
| 524 | [string]$ContainerFolder |
| 525 | ) |
| 526 | |
| 527 | $platform = Get-CIPlatform |
| 528 | |
| 529 | if (-not (Test-Path $Path)) { |
| 530 | Write-Warning "Artifact path not found: $Path" |
| 531 | return |
| 532 | } |
| 533 | |
| 534 | switch ($platform) { |
| 535 | 'github' { |
| 536 | Set-CIOutput -Name "artifact-path-$Name" -Value $Path |
| 537 | Set-CIOutput -Name "artifact-name-$Name" -Value $Name |
| 538 | Write-Verbose "GitHub artifact ready: $Name at $Path" |
| 539 | } |
| 540 | 'azdo' { |
| 541 | $container = if ($ContainerFolder) { $ContainerFolder } else { $Name } |
| 542 | $escapedContainer = ConvertTo-AzureDevOpsEscaped -Value $container -ForProperty |
| 543 | $escapedName = ConvertTo-AzureDevOpsEscaped -Value $Name -ForProperty |
| 544 | $escapedPath = ConvertTo-AzureDevOpsEscaped -Value $Path |
| 545 | Write-Output "##vso[artifact.upload containerfolder=$escapedContainer;artifactname=$escapedName]$escapedPath" |
| 546 | } |
| 547 | 'local' { |
| 548 | Write-Verbose "Artifact: $Name at $Path" |
| 549 | } |
| 550 | } |
| 551 | } |
| 552 | |
| 553 | function Get-StandardTimestamp { |
| 554 | <# |
| 555 | .SYNOPSIS |
| 556 | Returns the current UTC time as an ISO 8601 string. |
| 557 | |
| 558 | .DESCRIPTION |
| 559 | Returns the current UTC time formatted with the round-trip specifier ("o"), |
| 560 | producing a string such as "2025-01-15T18:30:00.0000000Z". Use this |
| 561 | function wherever a timestamp is needed to ensure consistent, timezone- |
| 562 | unambiguous log output across all scripts. |
| 563 | |
| 564 | .OUTPUTS |
| 565 | System.String - UTC timestamp in ISO 8601 round-trip format ending in Z. |
| 566 | #> |
| 567 | [CmdletBinding()] |
| 568 | [OutputType([string])] |
| 569 | param() |
| 570 | |
| 571 | return (Get-Date).ToUniversalTime().ToString('o') |
| 572 | } |
| 573 | |
| 574 | function Get-StandardTimestampPattern { |
| 575 | <# |
| 576 | .SYNOPSIS |
| 577 | Returns the regex pattern that matches Get-StandardTimestamp output. |
| 578 | |
| 579 | .DESCRIPTION |
| 580 | Returns a single-source regex anchored to the ISO 8601 round-trip format |
| 581 | produced by Get-StandardTimestamp (e.g. "2025-01-15T18:30:00.0000000Z"). |
| 582 | Use this function in tests instead of hard-coding the pattern so that all |
| 583 | assertions stay in sync when the timestamp format changes. |
| 584 | |
| 585 | .OUTPUTS |
| 586 | System.String - Anchored regex pattern for ISO 8601 UTC timestamps. |
| 587 | #> |
| 588 | [CmdletBinding()] |
| 589 | [OutputType([string])] |
| 590 | param() |
| 591 | |
| 592 | return '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$' |
| 593 | } |
| 594 | |
| 595 | Export-ModuleMember -Function @( |
| 596 | 'Get-StandardTimestamp', |
| 597 | 'Get-StandardTimestampPattern', |
| 598 | 'ConvertTo-GitHubActionsEscaped', |
| 599 | 'ConvertTo-AzureDevOpsEscaped', |
| 600 | 'Get-CIPlatform', |
| 601 | 'Test-CIEnvironment', |
| 602 | 'Set-CIOutput', |
| 603 | 'Set-CIEnv', |
| 604 | 'Write-CIStepSummary', |
| 605 | 'Write-CIAnnotation', |
| 606 | 'Write-CIAnnotations', |
| 607 | 'Set-CITaskResult', |
| 608 | 'Publish-CIArtifact' |
| 609 | ) |
| 610 | |