From 7546918712b487a3f63e94a55e56b14cc4c908aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 14:55:15 +0000 Subject: [PATCH 01/13] Initial plan From a034eb2591ee1ce0a4bba2f44f8b4c34c7a496bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:00:45 +0000 Subject: [PATCH 02/13] Fix DateTime locale-agnostic serialization using ISO 8601 format Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .../WorkflowInitialize/WorkflowInitialize.ps1 | 3 ++- .../WorkflowPostProcess.ps1 | 2 +- Tests/WorkflowPostProcess.Test.ps1 | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Actions/WorkflowInitialize/WorkflowInitialize.ps1 b/Actions/WorkflowInitialize/WorkflowInitialize.ps1 index 5629ea4215..9413e24efc 100644 --- a/Actions/WorkflowInitialize/WorkflowInitialize.ps1 +++ b/Actions/WorkflowInitialize/WorkflowInitialize.ps1 @@ -30,8 +30,9 @@ TestALGoRepository TestRunnerPrerequisites # Create a json object that contains an entry for the workflowstarttime +# Use ISO 8601 format for locale-agnostic serialization $scopeJson = @{ - "workflowStartTime" = [DateTime]::UtcNow + "workflowStartTime" = [DateTime]::UtcNow.ToString("o") } | ConvertTo-Json -Compress Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "telemetryScopeJson=$scopeJson" diff --git a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 index 9928ce9444..128b8765ae 100644 --- a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 +++ b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 @@ -68,7 +68,7 @@ function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { # Calculate the workflow duration using the github api if ($telemetryScope -and ($null -ne $telemetryScope.workflowStartTime)) { Write-Host "Calculating workflow duration..." - $workflowTiming= [DateTime]::UtcNow.Subtract([DateTime]::Parse($telemetryScope.workflowStartTime)).TotalSeconds + $workflowTiming= [DateTime]::UtcNow.Subtract([DateTime]::Parse($telemetryScope.workflowStartTime, [System.Globalization.CultureInfo]::InvariantCulture)).TotalSeconds Add-TelemetryProperty -Hashtable $AdditionalData -Key 'WorkflowDuration' -Value $workflowTiming } diff --git a/Tests/WorkflowPostProcess.Test.ps1 b/Tests/WorkflowPostProcess.Test.ps1 index dbcc2c104a..a23b50ba73 100644 --- a/Tests/WorkflowPostProcess.Test.ps1 +++ b/Tests/WorkflowPostProcess.Test.ps1 @@ -25,4 +25,22 @@ Describe "WorkflowPostProcess Action Tests" { # Call action + It 'Test DateTime serialization is locale-agnostic' { + # Simulate what WorkflowInitialize does - serialize a datetime in ISO 8601 format + $utcNow = [DateTime]::UtcNow + $scopeJson = @{ + "workflowStartTime" = $utcNow.ToString("o") + } | ConvertTo-Json -Compress + + # Simulate what WorkflowPostProcess does - deserialize the datetime + $telemetryScope = $scopeJson | ConvertFrom-Json + + # This should not throw regardless of culture settings + $parsedDateTime = [DateTime]::Parse($telemetryScope.workflowStartTime, [System.Globalization.CultureInfo]::InvariantCulture) + + # Verify the parsed datetime is close to the original (within 1 second to account for execution time) + $timeDiff = [Math]::Abs(($parsedDateTime - $utcNow).TotalSeconds) + $timeDiff | Should -BeLessThan 1 + } + } From dc6fa1f97b10467c7d1e7657edc2bb38acdc3832 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova <43066499+mazhelez@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:13:53 +0100 Subject: [PATCH 03/13] Apply suggestion from @mazhelez --- Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 index 128b8765ae..be32f81b6b 100644 --- a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 +++ b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 @@ -68,7 +68,7 @@ function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { # Calculate the workflow duration using the github api if ($telemetryScope -and ($null -ne $telemetryScope.workflowStartTime)) { Write-Host "Calculating workflow duration..." - $workflowTiming= [DateTime]::UtcNow.Subtract([DateTime]::Parse($telemetryScope.workflowStartTime, [System.Globalization.CultureInfo]::InvariantCulture)).TotalSeconds + $workflowTiming = [DateTime]::UtcNow.Subtract([DateTime]::Parse($telemetryScope.workflowStartTime, [System.Globalization.CultureInfo]::InvariantCulture)).TotalSeconds Add-TelemetryProperty -Hashtable $AdditionalData -Key 'WorkflowDuration' -Value $workflowTiming } From 3e1c750bcc3adb8cdabcf31cdfd825e9bce1cc33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:14:15 +0000 Subject: [PATCH 04/13] Add release notes entry for DateTime locale fix (Issue #2045) Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- RELEASENOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index de44f3d8d7..ad7a67faf9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -46,6 +46,7 @@ Read more at [workflowDefaultInputs](https://aka.ms/algosettings#workflowDefault ### Issues +- Issue 2045 DateTime parsing fails on non-US locale runners in WorkflowPostProcess.ps1 - Issue 2039 Error when deploy to environment: NewTemporaryFolder is not recognized - Issue 1961 KeyVault access in PR pipeline - Discussion 1911 Add support for reportSuppressedDiagnostics From 283309778e43a0061fdb35093487d05b16a2881e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:40:27 +0000 Subject: [PATCH 05/13] Ensure datetime is converted to UTC before calculating duration Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .../WorkflowPostProcess/WorkflowPostProcess.ps1 | 8 +++++++- Tests/WorkflowPostProcess.Test.ps1 | 14 +++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 index be32f81b6b..09ff4d685e 100644 --- a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 +++ b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 @@ -68,7 +68,13 @@ function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { # Calculate the workflow duration using the github api if ($telemetryScope -and ($null -ne $telemetryScope.workflowStartTime)) { Write-Host "Calculating workflow duration..." - $workflowTiming = [DateTime]::UtcNow.Subtract([DateTime]::Parse($telemetryScope.workflowStartTime, [System.Globalization.CultureInfo]::InvariantCulture)).TotalSeconds + $startTime = $telemetryScope.workflowStartTime + if ($startTime -is [DateTime]) { + $startTimeUtc = $startTime.ToUniversalTime() + } else { + $startTimeUtc = [DateTime]::Parse($startTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) + } + $workflowTiming = [DateTime]::UtcNow.Subtract($startTimeUtc).TotalSeconds Add-TelemetryProperty -Hashtable $AdditionalData -Key 'WorkflowDuration' -Value $workflowTiming } diff --git a/Tests/WorkflowPostProcess.Test.ps1 b/Tests/WorkflowPostProcess.Test.ps1 index a23b50ba73..16724d8b5f 100644 --- a/Tests/WorkflowPostProcess.Test.ps1 +++ b/Tests/WorkflowPostProcess.Test.ps1 @@ -35,11 +35,19 @@ Describe "WorkflowPostProcess Action Tests" { # Simulate what WorkflowPostProcess does - deserialize the datetime $telemetryScope = $scopeJson | ConvertFrom-Json - # This should not throw regardless of culture settings - $parsedDateTime = [DateTime]::Parse($telemetryScope.workflowStartTime, [System.Globalization.CultureInfo]::InvariantCulture) + # ConvertFrom-Json automatically parses ISO 8601 dates to DateTime with UTC Kind + $startTime = $telemetryScope.workflowStartTime + if ($startTime -is [DateTime]) { + $startTimeUtc = $startTime.ToUniversalTime() + } else { + $startTimeUtc = [DateTime]::Parse($startTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) + } + + # Verify the parsed datetime is in UTC + $startTimeUtc.Kind | Should -Be 'Utc' # Verify the parsed datetime is close to the original (within 1 second to account for execution time) - $timeDiff = [Math]::Abs(($parsedDateTime - $utcNow).TotalSeconds) + $timeDiff = [Math]::Abs(($startTimeUtc - $utcNow).TotalSeconds) $timeDiff | Should -BeLessThan 1 } From 5702191e85230ec58cbb42cb533f02c97cdccec6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Dec 2025 13:23:40 +0000 Subject: [PATCH 06/13] Move Issue 2045 release notes to unreleased section above v8.1 Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- RELEASENOTES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e61f7438d6..428ea42d39 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,3 +1,7 @@ +### Issues + +- Issue 2045 DateTime parsing fails on non-US locale runners in WorkflowPostProcess.ps1 + ## v8.1 ### Custom AL-Go files @@ -48,7 +52,6 @@ Read more at [workflowDefaultInputs](https://aka.ms/algosettings#workflowDefault ### Issues -- Issue 2045 DateTime parsing fails on non-US locale runners in WorkflowPostProcess.ps1 - Issue 2039 Error when deploy to environment: NewTemporaryFolder is not recognized - Issue 1961 KeyVault access in PR pipeline - Discussion 1911 Add support for reportSuppressedDiagnostics From 5952fd60e0b470773cebc92d4e719d41b5f488a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:18:55 +0000 Subject: [PATCH 07/13] Merge branch 'main' into copilot/fix-datetime-parsing-bug - resolve RELEASENOTES conflict Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Actions/AnalyzeTests/TestResultAnalyzer.ps1 | 69 ++++++++++++++++++--- RELEASENOTES.md | 4 ++ Scenarios/EnablingTelemetry.md | 45 ++++++++++++++ 3 files changed, 110 insertions(+), 8 deletions(-) diff --git a/Actions/AnalyzeTests/TestResultAnalyzer.ps1 b/Actions/AnalyzeTests/TestResultAnalyzer.ps1 index 7baea1b338..1beae6e4fa 100644 --- a/Actions/AnalyzeTests/TestResultAnalyzer.ps1 +++ b/Actions/AnalyzeTests/TestResultAnalyzer.ps1 @@ -9,6 +9,7 @@ $statusSkipped = " :question:" # Returns both a summary part and a failures part $mdHelperPath = Join-Path -Path $PSScriptRoot -ChildPath "..\MarkDownHelper.psm1" Import-Module $mdHelperPath +Import-Module (Join-Path $PSScriptRoot '..\TelemetryHelper.psm1' -Resolve) #Helper function to build a markdown table. #Headers are an array of strings with format "label;alignment" where alignment is 'left', 'right' or 'center' @@ -132,12 +133,14 @@ function GetTestResultSummaryMD { $summarySb = [System.Text.StringBuilder]::new() $failuresSb = [System.Text.StringBuilder]::new() + $totalTests = 0 + $totalTime = 0.0 + $totalFailed = 0 + $totalSkipped = 0 + if (Test-Path -Path $testResultsFile -PathType Leaf) { $testResults = [xml](Get-Content -path $testResultsFile -Encoding UTF8) - $totalTests = 0 - $totalTime = 0.0 - $totalFailed = 0 - $totalSkipped = 0 + if ($testResults.testsuites) { $appNames = @($testResults.testsuites.testsuite | ForEach-Object { $_.Properties.property | Where-Object { $_.Name -eq "appName" } | ForEach-Object { $_.Value } } | Select-Object -Unique) if (-not $appNames) { @@ -219,6 +222,19 @@ function GetTestResultSummaryMD { else { $failuresSummaryMD = '' } + + # Log test metrics to telemetry + $totalPassed = $totalTests - $totalFailed - $totalSkipped + if ($totalTests -gt 0) { + $telemetryData = [System.Collections.Generic.Dictionary[[System.String], [System.String]]]::new() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalTests' -Value $totalTests.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalPassed' -Value $totalPassed.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalFailed' -Value $totalFailed.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalSkipped' -Value $totalSkipped.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalTime' -Value $totalTime.ToString() + Trace-Information -Message "AL-Go Test Results - Tests" -AdditionalData $telemetryData + } + $summarySb.ToString() $failuresSb.ToString() $failuresSummaryMD @@ -320,6 +336,10 @@ function GetBcptSummaryMD { $lastCodeunitName = '' $lastOperationName = '' + $totalPassed = 0 + $totalFailed = 0 + $totalSkipped = 0 + # calculate statistics on measurements, skipping the $skipMeasurements longest measurements foreach($suiteName in $bcpt.Keys) { $suite = $bcpt."$suiteName" @@ -418,6 +438,16 @@ function GetBcptSummaryMD { } } $mdTableRow = @($thisSuiteName, $thisCodeunitID, $thisCodeunitName, $thisOperationName, $statusStr, $durationMinStr, $baseDurationMinStr, $diffDurationMinStr, $diffDurationMinPctStr, $numberOfSQLStmtsStr, $baseNumberOfSQLStmtsStr, $diffNumberOfSQLStmtsStr, $diffNumberOfSQLStmtsPctStr) + + # Update test counts + if ($statusStr) { + switch ($statusStr) { + $statusOK { $totalPassed++ } + $statusWarning { $totalFailed++ } + $statusError { $totalFailed++ } + $statusSkipped { $totalSkipped++ } + } + } } } $mdTableRows.Add($mdTableRow) | Out-Null @@ -438,6 +468,17 @@ function GetBcptSummaryMD { $summarySb.AppendLine("\nNo baseline provided. Copy a set of BCPT results to $([System.IO.Path]::GetFileName($baseLinePath)) in the project folder in order to establish a baseline.") | Out-Null } + # Log BCPT metrics to telemetry + $totalTests = $totalPassed + $totalFailed + $totalSkipped + if ($totalTests -gt 0) { + $telemetryData = [System.Collections.Generic.Dictionary[[System.String], [System.String]]]::new() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalTests' -Value $totalTests.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalPassed' -Value $totalPassed.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalFailed' -Value $totalFailed.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalSkipped' -Value $totalSkipped.ToString() + Trace-Information -Message "AL-Go Test Results - BCPT Tests" -AdditionalData $telemetryData + } + $summarySb.ToString() } @@ -449,13 +490,14 @@ function GetPageScriptingTestResultSummaryMD { $summarySb = [System.Text.StringBuilder]::new() $failuresSb = [System.Text.StringBuilder]::new() + $totalTests = 0 + $totalTime = 0.0 + $totalFailed = 0 + $totalSkipped = 0 + $totalPassed = 0 if (Test-Path -Path $testResultsFile -PathType Leaf) { $testResults = [xml](Get-Content -path $testResultsFile -Encoding UTF8) - $totalTests = 0 - $totalTime = 0.0 - $totalFailed = 0 - $totalSkipped = 0 $rootFailureNode = [FailureNode]::new($false) if ($testResults.testsuites) { @@ -524,6 +566,17 @@ function GetPageScriptingTestResultSummaryMD { $failuresSummaryMD = '' } + # Log test metrics to telemetry + if ($totalTests -gt 0) { + $telemetryData = [System.Collections.Generic.Dictionary[[System.String], [System.String]]]::new() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalTests' -Value $totalTests.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalPassed' -Value $totalPassed.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalFailed' -Value $totalFailed.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalSkipped' -Value $totalSkipped.ToString() + Add-TelemetryProperty -Hashtable $telemetryData -Key 'TotalTime' -Value $totalTime.ToString() + Trace-Information -Message "AL-Go Test Results - Page scripting Tests" -AdditionalData $telemetryData + } + return @{ SummaryMD = $summarySb.ToString() FailuresMD = $failuresSb.ToString() diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 428ea42d39..8e7d631f88 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,3 +1,7 @@ +## AL-Go Telemetry updates + +AL-Go telemetry now includes test results so you can more easily see how many AL tests, Page Scripting tests and BCPT tests ran in your workflows across all your repositories. Documentation for this can be found on [this article](https://github.com/microsoft/AL-Go/blob/main/Scenarios/EnablingTelemetry.md) on enabling telemetry. + ### Issues - Issue 2045 DateTime parsing fails on non-US locale runners in WorkflowPostProcess.ps1 diff --git a/Scenarios/EnablingTelemetry.md b/Scenarios/EnablingTelemetry.md index 0fee818b8e..427aa33838 100644 --- a/Scenarios/EnablingTelemetry.md +++ b/Scenarios/EnablingTelemetry.md @@ -86,6 +86,29 @@ traces | where message contains "AL-Go action" ``` +The following query gets all telemetry emitted for test results + +``` +traces +| where timestamp > ago(7d) +| project timestamp, + message, + severityLevel, + RepositoryOwner = tostring(customDimensions.RepositoryOwner), + RepositoryName = tostring(customDimensions.RepositoryName), + RunId = tostring(customDimensions.RunId), + RunNumber = tostring(customDimensions.RunNumber), + RunAttempt = tostring(customDimensions.RunAttempt), + RefName = tostring(customDimensions.RefName), + TotalTests = todouble(customDimensions.TotalTests), + TotalFailed = todouble(customDimensions.TotalFailed), + TotalSkipped = todouble(customDimensions.TotalSkipped), + TotalPassed = todouble(customDimensions.TotalPassed), + TotalTime = todouble(customDimensions.TotalTime) +| extend HtmlUrl = strcat("https://github.com/", RepositoryName, "/actions/runs/", RunId) +| where message contains "AL-Go Test Results" +``` + ## Telemetry events and data AL-Go logs four different types of telemetry events: AL-Go action ran/failed and AL-Go workflow ran/failed. Each of those telemetry events provide slightly different telemetry but common dimensions for all of them are: @@ -159,6 +182,28 @@ Additional Dimensions: | RunsOn | Value of the RunsOn setting | | ALGoVersion | The AL-Go version used for the workflow run | +### Test results + +Telemetry messages: + +- `AL-Go Test Results - Tests` +- `AL-Go Test Results - Page scripting Tests` +- `AL-Go Test Results - BCPT Tests` + +SeverityLevel: 1 + +Additional Dimensions: + +| Dimension | Description | +|-----------|-------------| +| TotalTests | The total number of tests executed | +| TotalFailed | The number of tests that failed | +| TotalSkipped | The number of tests that were skipped | +| TotalPassed | The number of tests that passed | +| TotalTime | The total time taken to execute all tests | + +**Note:** The `TotalTime` dimension is not tracked for BCPT test results. For BCPT tests, only `TotalTests`, `TotalPassed`, `TotalFailed`, and `TotalSkipped` are available. + ______________________________________________________________________ [back](../README.md) From dbce971e58e5c83dc44a748103f188ac07b804b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:24:04 +0000 Subject: [PATCH 08/13] Merge branch 'main' into copilot/fix-datetime-parsing-bug - resolve conflicts Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .../DownloadProjectDependencies.Action.ps1 | 16 ++++++---- Actions/DownloadProjectDependencies/README.md | 4 +-- .../DownloadProjectDependencies/action.yaml | 4 +-- Actions/RunPipeline/README.md | 4 +-- Actions/RunPipeline/RunPipeline.ps1 | 30 +++++++++++++++---- Actions/RunPipeline/action.yaml | 8 ++--- RELEASENOTES.md | 8 ++--- 7 files changed, 48 insertions(+), 26 deletions(-) diff --git a/Actions/DownloadProjectDependencies/DownloadProjectDependencies.Action.ps1 b/Actions/DownloadProjectDependencies/DownloadProjectDependencies.Action.ps1 index 9cff002fe9..be9f84fb75 100644 --- a/Actions/DownloadProjectDependencies/DownloadProjectDependencies.Action.ps1 +++ b/Actions/DownloadProjectDependencies/DownloadProjectDependencies.Action.ps1 @@ -130,18 +130,22 @@ $downloadedTestApps = @() $downloadedDependencies | ForEach-Object { # naming convention: app, (testapp) if ($_.startswith('(')) { - $DownloadedTestApps += $_ + $downloadedTestApps += $_ } else { - $DownloadedApps += $_ + $downloadedApps += $_ } } OutputMessageAndArray -message "Downloaded dependencies (Apps)" -arrayOfStrings $downloadedApps OutputMessageAndArray -message "Downloaded dependencies (Test Apps)" -arrayOfStrings $downloadedTestApps -$DownloadedAppsJson = ConvertTo-Json $DownloadedApps -Depth 99 -Compress -$DownloadedTestAppsJson = ConvertTo-Json $DownloadedTestApps -Depth 99 -Compress +# Write the downloaded apps and test apps to temporary JSON files and set them as GitHub Action outputs +$tempPath = NewTemporaryFolder +$downloadedAppsJson = Join-Path $tempPath "DownloadedApps.json" +$downloadedTestAppsJson = Join-Path $tempPath "DownloadedTestApps.json" +ConvertTo-Json $downloadedApps -Depth 99 -Compress | Out-File -Encoding UTF8 -FilePath $downloadedAppsJson +ConvertTo-Json $downloadedTestApps -Depth 99 -Compress | Out-File -Encoding UTF8 -FilePath $downloadedTestAppsJson -Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "DownloadedApps=$DownloadedAppsJson" -Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "DownloadedTestApps=$DownloadedTestAppsJson" +Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "DownloadedApps=$downloadedAppsJson" +Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "DownloadedTestApps=$downloadedTestAppsJson" diff --git a/Actions/DownloadProjectDependencies/README.md b/Actions/DownloadProjectDependencies/README.md index 4df0bc14b6..4892918f1f 100644 --- a/Actions/DownloadProjectDependencies/README.md +++ b/Actions/DownloadProjectDependencies/README.md @@ -37,5 +37,5 @@ The action constructs arrays of paths to .app files, that are dependencies of th | Name | Description | | :-- | :-- | -| DownloadedApps | A JSON-formatted list of paths to .app files, that dependencies of the apps | -| DownloadedTestApps | A JSON-formatted list of paths to .app files, that dependencies of the test apps | +| DownloadedApps | A path to a JSON-formatted list of apps to install | +| DownloadedTestApps | A path to a JSON-formatted list of test apps to install | diff --git a/Actions/DownloadProjectDependencies/action.yaml b/Actions/DownloadProjectDependencies/action.yaml index 4c85e1d3a3..befff9c37b 100644 --- a/Actions/DownloadProjectDependencies/action.yaml +++ b/Actions/DownloadProjectDependencies/action.yaml @@ -21,10 +21,10 @@ inputs: default: '0' outputs: DownloadedApps: - description: A JSON-formatted array of paths to .app files of the apps that were downloaded + description: A path to a JSON-formatted list of apps to install value: ${{ steps.DownloadDependencies.outputs.DownloadedApps }} DownloadedTestApps: - description: A JSON-formatted array of paths to .app files of the test apps that were downloaded + description: A path to a JSON-formatted list of test apps to install value: ${{ steps.DownloadDependencies.outputs.DownloadedTestApps }} runs: using: composite diff --git a/Actions/RunPipeline/README.md b/Actions/RunPipeline/README.md index 40bfa84be2..2a790e94df 100644 --- a/Actions/RunPipeline/README.md +++ b/Actions/RunPipeline/README.md @@ -20,8 +20,8 @@ Run pipeline in AL-Go repository | artifact | | ArtifactUrl to use for the build | settings.artifact | | project | | Project name if the repository is setup for multiple projects | . | | buildMode | | Specifies a mode to use for the build steps | Default | -| installAppsJson | | A JSON-formatted list of apps to install | [] | -| installTestAppsJson | | A JSON-formatted list of test apps to install | [] | +| installAppsJson | | A path to a JSON-formatted list of apps to install | '' | +| installTestAppsJson | | A path to a JSON-formatted list of test apps to install | '' | | baselineWorkflowRunId | RunId of the baseline workflow run | | | baselineWorkflowSHA | SHA of the baseline workflow run | | diff --git a/Actions/RunPipeline/RunPipeline.ps1 b/Actions/RunPipeline/RunPipeline.ps1 index 76abf38923..9593f33b31 100644 --- a/Actions/RunPipeline/RunPipeline.ps1 +++ b/Actions/RunPipeline/RunPipeline.ps1 @@ -7,10 +7,10 @@ Param( [string] $project = "", [Parameter(HelpMessage = "Specifies a mode to use for the build steps", Mandatory = $false)] [string] $buildMode = 'Default', - [Parameter(HelpMessage = "A JSON-formatted list of apps to install", Mandatory = $false)] - [string] $installAppsJson = '[]', - [Parameter(HelpMessage = "A JSON-formatted list of test apps to install", Mandatory = $false)] - [string] $installTestAppsJson = '[]', + [Parameter(HelpMessage = "A path to a JSON-formatted list of apps to install", Mandatory = $false)] + [string] $installAppsJson = '', + [Parameter(HelpMessage = "A path to a JSON-formatted list of test apps to install", Mandatory = $false)] + [string] $installTestAppsJson = '', [Parameter(HelpMessage = "RunId of the baseline workflow run", Mandatory = $false)] [string] $baselineWorkflowRunId = '0', [Parameter(HelpMessage = "SHA of the baseline workflow run", Mandatory = $false)] @@ -187,8 +187,26 @@ try { } $install = @{ - "Apps" = $settings.installApps + @($installAppsJson | ConvertFrom-Json) - "TestApps" = $settings.installTestApps + @($installTestAppsJson | ConvertFrom-Json) + "Apps" = $settings.installApps + "TestApps" = $settings.installTestApps + } + + if ($installAppsJson -and (Test-Path $installAppsJson)) { + try { + $install.Apps += @(Get-Content -Path $installAppsJson -Raw | ConvertFrom-Json) + } + catch { + throw "Failed to parse JSON file at path '$installAppsJson'. Error: $($_.Exception.Message)" + } + } + + if ($installTestAppsJson -and (Test-Path $installTestAppsJson)) { + try { + $install.TestApps += @(Get-Content -Path $installTestAppsJson -Raw | ConvertFrom-Json) + } + catch { + throw "Failed to parse JSON file at path '$installTestAppsJson'. Error: $($_.Exception.Message)" + } } # Replace secret names in install.apps and install.testApps diff --git a/Actions/RunPipeline/action.yaml b/Actions/RunPipeline/action.yaml index c4aad6861a..cd5003f7b4 100644 --- a/Actions/RunPipeline/action.yaml +++ b/Actions/RunPipeline/action.yaml @@ -22,13 +22,13 @@ inputs: required: false default: 'Default' installAppsJson: - description: A JSON-formatted list of apps to install + description: A path to a JSON-formatted list of apps to install required: false - default: '[]' + default: '' installTestAppsJson: - description: A JSON-formatted list of test apps to install + description: A path to a JSON-formatted list of test apps to install required: false - default: '[]' + default: '' baselineWorkflowRunId: description: RunId of the baseline workflow run required: false diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8e7d631f88..564a102328 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,11 +1,11 @@ -## AL-Go Telemetry updates - -AL-Go telemetry now includes test results so you can more easily see how many AL tests, Page Scripting tests and BCPT tests ran in your workflows across all your repositories. Documentation for this can be found on [this article](https://github.com/microsoft/AL-Go/blob/main/Scenarios/EnablingTelemetry.md) on enabling telemetry. - ### Issues - Issue 2045 DateTime parsing fails on non-US locale runners in WorkflowPostProcess.ps1 +- AL-Go repositories with large amounts of projects may run into issues with too large environment variables +## AL-Go Telemetry updates + +AL-Go telemetry now includes test results so you can more easily see how many AL tests, Page Scripting tests and BCPT tests ran in your workflows across all your repositories. Documentation for this can be found on [this article](https://github.com/microsoft/AL-Go/blob/main/Scenarios/EnablingTelemetry.md) on enabling telemetry. ## v8.1 ### Custom AL-Go files From 6b92269e01691c926d281dd1a7a771c82431a8aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:33:09 +0000 Subject: [PATCH 09/13] Refactor datetime parsing into testable function and add locale testing Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- .../WorkflowPostProcess.ps1 | 17 +++-- Tests/WorkflowPostProcess.Test.ps1 | 64 +++++++++++++------ 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 index 09ff4d685e..2b68ef26d1 100644 --- a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 +++ b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 @@ -54,6 +54,16 @@ function GetAlGoVersion($ActionsRepo, $ActionRef) { } } +function ConvertToUtcDateTime($DateTimeValue) { + # Convert a datetime value to UTC, handling both DateTime objects and strings + # This function ensures locale-agnostic datetime parsing + if ($DateTimeValue -is [DateTime]) { + return $DateTimeValue.ToUniversalTime() + } else { + return [DateTime]::Parse($DateTimeValue, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) + } +} + function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { [System.Collections.Generic.Dictionary[[System.String], [System.String]]] $AdditionalData = @{} $telemetryScope = $null @@ -68,12 +78,7 @@ function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { # Calculate the workflow duration using the github api if ($telemetryScope -and ($null -ne $telemetryScope.workflowStartTime)) { Write-Host "Calculating workflow duration..." - $startTime = $telemetryScope.workflowStartTime - if ($startTime -is [DateTime]) { - $startTimeUtc = $startTime.ToUniversalTime() - } else { - $startTimeUtc = [DateTime]::Parse($startTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) - } + $startTimeUtc = ConvertToUtcDateTime -DateTimeValue $telemetryScope.workflowStartTime $workflowTiming = [DateTime]::UtcNow.Subtract($startTimeUtc).TotalSeconds Add-TelemetryProperty -Hashtable $AdditionalData -Key 'WorkflowDuration' -Value $workflowTiming } diff --git a/Tests/WorkflowPostProcess.Test.ps1 b/Tests/WorkflowPostProcess.Test.ps1 index 16724d8b5f..79298db686 100644 --- a/Tests/WorkflowPostProcess.Test.ps1 +++ b/Tests/WorkflowPostProcess.Test.ps1 @@ -26,29 +26,53 @@ Describe "WorkflowPostProcess Action Tests" { # Call action It 'Test DateTime serialization is locale-agnostic' { - # Simulate what WorkflowInitialize does - serialize a datetime in ISO 8601 format - $utcNow = [DateTime]::UtcNow - $scopeJson = @{ - "workflowStartTime" = $utcNow.ToString("o") - } | ConvertTo-Json -Compress - - # Simulate what WorkflowPostProcess does - deserialize the datetime - $telemetryScope = $scopeJson | ConvertFrom-Json - - # ConvertFrom-Json automatically parses ISO 8601 dates to DateTime with UTC Kind - $startTime = $telemetryScope.workflowStartTime - if ($startTime -is [DateTime]) { - $startTimeUtc = $startTime.ToUniversalTime() - } else { - $startTimeUtc = [DateTime]::Parse($startTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) + # Define the ConvertToUtcDateTime function from WorkflowPostProcess for testing + # This is the actual function used in WorkflowPostProcess.ps1 + function ConvertToUtcDateTime($DateTimeValue) { + if ($DateTimeValue -is [DateTime]) { + return $DateTimeValue.ToUniversalTime() + } else { + return [DateTime]::Parse($DateTimeValue, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) + } } + + # Save the current culture to restore later + $originalCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture + + try { + # Test with different locales to ensure the fix works regardless of culture + # This simulates the scenario where WorkflowInitialize runs on one machine with one locale + # and WorkflowPostProcess runs on another machine with a different locale + $testCultures = @('en-US', 'en-AU', 'de-DE', 'ja-JP') + + foreach ($cultureName in $testCultures) { + # Set the culture to simulate different locale machines + [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::new($cultureName) + + # Simulate what WorkflowInitialize does - serialize a datetime in ISO 8601 format + $utcNow = [DateTime]::UtcNow + $scopeJson = @{ + "workflowStartTime" = $utcNow.ToString("o") + } | ConvertTo-Json -Compress - # Verify the parsed datetime is in UTC - $startTimeUtc.Kind | Should -Be 'Utc' + # Simulate what WorkflowPostProcess does - deserialize the datetime + $telemetryScope = $scopeJson | ConvertFrom-Json - # Verify the parsed datetime is close to the original (within 1 second to account for execution time) - $timeDiff = [Math]::Abs(($startTimeUtc - $utcNow).TotalSeconds) - $timeDiff | Should -BeLessThan 1 + # Use the ConvertToUtcDateTime function (same logic as in WorkflowPostProcess.ps1) + $startTimeUtc = ConvertToUtcDateTime -DateTimeValue $telemetryScope.workflowStartTime + + # Verify the parsed datetime is in UTC + $startTimeUtc.Kind | Should -Be 'Utc' -Because "DateTime should be in UTC regardless of culture ($cultureName)" + + # Verify the parsed datetime is close to the original (within 1 second to account for execution time) + $timeDiff = [Math]::Abs(($startTimeUtc - $utcNow).TotalSeconds) + $timeDiff | Should -BeLessThan 1 -Because "Parsed datetime should match original for culture $cultureName" + } + } + finally { + # Restore the original culture + [System.Threading.Thread]::CurrentThread.CurrentCulture = $originalCulture + } } } From b90d7215a6d4f09acbde3535e5294a7a3878a556 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:10:49 +0000 Subject: [PATCH 10/13] Use ConvertToUtcDateTime function from WorkflowPostProcess.ps1 instead of redefining Co-authored-by: mazhelez <43066499+mazhelez@users.noreply.github.com> --- Tests/WorkflowPostProcess.Test.ps1 | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Tests/WorkflowPostProcess.Test.ps1 b/Tests/WorkflowPostProcess.Test.ps1 index 79298db686..fd2f527ff5 100644 --- a/Tests/WorkflowPostProcess.Test.ps1 +++ b/Tests/WorkflowPostProcess.Test.ps1 @@ -26,15 +26,8 @@ Describe "WorkflowPostProcess Action Tests" { # Call action It 'Test DateTime serialization is locale-agnostic' { - # Define the ConvertToUtcDateTime function from WorkflowPostProcess for testing - # This is the actual function used in WorkflowPostProcess.ps1 - function ConvertToUtcDateTime($DateTimeValue) { - if ($DateTimeValue -is [DateTime]) { - return $DateTimeValue.ToUniversalTime() - } else { - return [DateTime]::Parse($DateTimeValue, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) - } - } + # Dot-source the WorkflowPostProcess script to load the ConvertToUtcDateTime function + . $scriptPath # Save the current culture to restore later $originalCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture From bee17fb03a53f8128a8439df261058010c084e4f Mon Sep 17 00:00:00 2001 From: Maria Zhelezova <43066499+mazhelez@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:13:16 +0100 Subject: [PATCH 11/13] Clean up whitespace in DateTime serialization test for consistency --- Tests/WorkflowPostProcess.Test.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/WorkflowPostProcess.Test.ps1 b/Tests/WorkflowPostProcess.Test.ps1 index fd2f527ff5..2aea0f62aa 100644 --- a/Tests/WorkflowPostProcess.Test.ps1 +++ b/Tests/WorkflowPostProcess.Test.ps1 @@ -28,20 +28,20 @@ Describe "WorkflowPostProcess Action Tests" { It 'Test DateTime serialization is locale-agnostic' { # Dot-source the WorkflowPostProcess script to load the ConvertToUtcDateTime function . $scriptPath - + # Save the current culture to restore later $originalCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture - + try { # Test with different locales to ensure the fix works regardless of culture # This simulates the scenario where WorkflowInitialize runs on one machine with one locale # and WorkflowPostProcess runs on another machine with a different locale $testCultures = @('en-US', 'en-AU', 'de-DE', 'ja-JP') - + foreach ($cultureName in $testCultures) { # Set the culture to simulate different locale machines [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::new($cultureName) - + # Simulate what WorkflowInitialize does - serialize a datetime in ISO 8601 format $utcNow = [DateTime]::UtcNow $scopeJson = @{ From 7594d997d04554b81f51f45a9c531f9758cc8be2 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova <43066499+mazhelez@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:39:12 +0100 Subject: [PATCH 12/13] Add GetWorkflowDuration function and update tests for locale handling --- .../.Modules/WorkflowPostProcessHelper.psm1 | 47 +++++++++++++++++ .../WorkflowPostProcess.ps1 | 17 ++---- Tests/WorkflowPostProcess.Test.ps1 | 52 ++++++------------- 3 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 Actions/.Modules/WorkflowPostProcessHelper.psm1 diff --git a/Actions/.Modules/WorkflowPostProcessHelper.psm1 b/Actions/.Modules/WorkflowPostProcessHelper.psm1 new file mode 100644 index 0000000000..aff5f0a3a8 --- /dev/null +++ b/Actions/.Modules/WorkflowPostProcessHelper.psm1 @@ -0,0 +1,47 @@ +<# +.SYNOPSIS +Helper functions for WorkflowPostProcess action + +.DESCRIPTION +Contains utility functions used by the WorkflowPostProcess action and its tests +#> + +<# +.SYNOPSIS +Calculate the duration of a workflow from a start time + +.DESCRIPTION +Calculates the duration in seconds from the provided start time to the current UTC time. +Handles both DateTime objects and string representations in ISO 8601 format. + +.PARAMETER StartTime +The workflow start time as either a DateTime object or a string in ISO 8601 format + +.PARAMETER EndTime +(Optional) The end time as a DateTime object. Defaults to the current UTC time. + +.EXAMPLE +$duration = GetWorkflowDuration -StartTime "2025-12-12T10:00:00.0000000Z" -EndTime ([DateTime]::UtcNow) + +.EXAMPLE +$duration = GetWorkflowDuration -StartTime ([DateTime]::UtcNow) +#> +function GetWorkflowDuration { + Param( + [Parameter(Mandatory = $true)] + $StartTime, + [Parameter(Mandatory = $false)] + $EndTime = [DateTime]::UtcNow + ) + + if ($StartTime -is [DateTime]) { + $workflowStartTime = $StartTime.ToUniversalTime() + } else { + $workflowStartTime = [DateTime]::Parse($StartTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) + } + + $workflowDuration = $EndTime.ToUniversalTime().Subtract($workflowStartTime).TotalSeconds + return $workflowDuration +} + +Export-ModuleMember -Function GetWorkflowDuration diff --git a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 index 2b68ef26d1..eb1eb8bb4f 100644 --- a/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 +++ b/Actions/WorkflowPostProcess/WorkflowPostProcess.ps1 @@ -54,16 +54,6 @@ function GetAlGoVersion($ActionsRepo, $ActionRef) { } } -function ConvertToUtcDateTime($DateTimeValue) { - # Convert a datetime value to UTC, handling both DateTime objects and strings - # This function ensures locale-agnostic datetime parsing - if ($DateTimeValue -is [DateTime]) { - return $DateTimeValue.ToUniversalTime() - } else { - return [DateTime]::Parse($DateTimeValue, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal) - } -} - function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { [System.Collections.Generic.Dictionary[[System.String], [System.String]]] $AdditionalData = @{} $telemetryScope = $null @@ -78,9 +68,9 @@ function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { # Calculate the workflow duration using the github api if ($telemetryScope -and ($null -ne $telemetryScope.workflowStartTime)) { Write-Host "Calculating workflow duration..." - $startTimeUtc = ConvertToUtcDateTime -DateTimeValue $telemetryScope.workflowStartTime - $workflowTiming = [DateTime]::UtcNow.Subtract($startTimeUtc).TotalSeconds - Add-TelemetryProperty -Hashtable $AdditionalData -Key 'WorkflowDuration' -Value $workflowTiming + + $workflowDuration = GetWorkflowDuration -StartTime $telemetryScope.workflowStartTime + Add-TelemetryProperty -Hashtable $AdditionalData -Key 'WorkflowDuration' -Value $workflowDuration } # Log additional telemetry from AL-Go settings @@ -113,6 +103,7 @@ function LogWorkflowEnd($TelemetryScopeJson, $JobContext, $AlGoVersion) { } Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "..\TelemetryHelper.psm1" -Resolve) +Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "..\.Modules\WorkflowPostProcessHelper.psm1" -Resolve) try { LogWorkflowEnd -TelemetryScopeJson $telemetryScopeJson -JobContext $currentJobContext -AlGoVersion (GetAlGoVersion -ActionsRepo $actionsRepo -ActionRef $actionsRef) diff --git a/Tests/WorkflowPostProcess.Test.ps1 b/Tests/WorkflowPostProcess.Test.ps1 index 2aea0f62aa..36fea65a29 100644 --- a/Tests/WorkflowPostProcess.Test.ps1 +++ b/Tests/WorkflowPostProcess.Test.ps1 @@ -1,5 +1,6 @@ Get-Module TestActionsHelper | Remove-Module -Force Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') +Import-Module (Join-Path $PSScriptRoot '..\Actions\.Modules\WorkflowPostProcessHelper.psm1') $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 Describe "WorkflowPostProcess Action Tests" { @@ -25,47 +26,24 @@ Describe "WorkflowPostProcess Action Tests" { # Call action - It 'Test DateTime serialization is locale-agnostic' { - # Dot-source the WorkflowPostProcess script to load the ConvertToUtcDateTime function - . $scriptPath + It 'GetWorkflowDuration handles different culture formats correctly' { + $endTimeUTC = [DateTime]::Parse("2025-12-12T10:00:01.0000000Z") - # Save the current culture to restore later - $originalCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture + # Test UTC start time + $workflowDuration = GetWorkflowDuration -StartTime "2025-12-12T10:00:00.0000000Z" -EndTime $endTimeUTC + $workflowDuration | Should -Be 1 - try { - # Test with different locales to ensure the fix works regardless of culture - # This simulates the scenario where WorkflowInitialize runs on one machine with one locale - # and WorkflowPostProcess runs on another machine with a different locale - $testCultures = @('en-US', 'en-AU', 'de-DE', 'ja-JP') + # Test en-US culture format + $workflowDuration = GetWorkflowDuration -StartTime "12/12/2025 10:00:00 AM" -EndTime $endTimeUTC + $workflowDuration | Should -Be 1 - foreach ($cultureName in $testCultures) { - # Set the culture to simulate different locale machines - [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::new($cultureName) + # Test de-DE culture format + $workflowDuration = GetWorkflowDuration -StartTime "12.12.2025 10:00:00" -EndTime $endTimeUTC + $workflowDuration | Should -Be 1 - # Simulate what WorkflowInitialize does - serialize a datetime in ISO 8601 format - $utcNow = [DateTime]::UtcNow - $scopeJson = @{ - "workflowStartTime" = $utcNow.ToString("o") - } | ConvertTo-Json -Compress - - # Simulate what WorkflowPostProcess does - deserialize the datetime - $telemetryScope = $scopeJson | ConvertFrom-Json - - # Use the ConvertToUtcDateTime function (same logic as in WorkflowPostProcess.ps1) - $startTimeUtc = ConvertToUtcDateTime -DateTimeValue $telemetryScope.workflowStartTime - - # Verify the parsed datetime is in UTC - $startTimeUtc.Kind | Should -Be 'Utc' -Because "DateTime should be in UTC regardless of culture ($cultureName)" - - # Verify the parsed datetime is close to the original (within 1 second to account for execution time) - $timeDiff = [Math]::Abs(($startTimeUtc - $utcNow).TotalSeconds) - $timeDiff | Should -BeLessThan 1 -Because "Parsed datetime should match original for culture $cultureName" - } - } - finally { - # Restore the original culture - [System.Threading.Thread]::CurrentThread.CurrentCulture = $originalCulture - } + # Test da-DK culture format + $workflowDuration = GetWorkflowDuration -StartTime "12-12-2025 10:00:00" -EndTime $endTimeUTC + $workflowDuration | Should -Be 1 } } From 62d8f04fbb5d85e86efa55e996aa0cb7f07089d0 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova <43066499+mazhelez@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:36:35 +0100 Subject: [PATCH 13/13] Update Actions/.Modules/WorkflowPostProcessHelper.psm1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Actions/.Modules/WorkflowPostProcessHelper.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Actions/.Modules/WorkflowPostProcessHelper.psm1 b/Actions/.Modules/WorkflowPostProcessHelper.psm1 index aff5f0a3a8..1d192260d0 100644 --- a/Actions/.Modules/WorkflowPostProcessHelper.psm1 +++ b/Actions/.Modules/WorkflowPostProcessHelper.psm1 @@ -12,10 +12,10 @@ Calculate the duration of a workflow from a start time .DESCRIPTION Calculates the duration in seconds from the provided start time to the current UTC time. -Handles both DateTime objects and string representations in ISO 8601 format. +Handles both DateTime objects and string representations in any date format that can be parsed by the invariant culture (e.g., ISO 8601 or other culture-independent formats). .PARAMETER StartTime -The workflow start time as either a DateTime object or a string in ISO 8601 format +The workflow start time as either a DateTime object or a string in any date format that can be parsed by the invariant culture (such as ISO 8601 or other culture-independent formats) .PARAMETER EndTime (Optional) The end time as a DateTime object. Defaults to the current UTC time.