Skip to content

Commit faa8bc7

Browse files
authored
Merge branch 'main' into copilot/extract-powershell-code
2 parents c38cd33 + 6f564fa commit faa8bc7

13 files changed

Lines changed: 776 additions & 495 deletions

File tree

Actions/.Modules/ReadSettings.psm1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ function GetDefaultSettings
148148
"installApps" = @()
149149
"installTestApps" = @()
150150
"installOnlyReferencedApps" = $true
151+
"runTestsInAllInstalledTestApps" = $false
151152
"generateDependencyArtifact" = $false
152153
"skipUpgrade" = $false
153154
"applicationDependency" = "18.0.0.0"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<#
2+
.SYNOPSIS
3+
Helper functions for WorkflowPostProcess action
4+
5+
.DESCRIPTION
6+
Contains utility functions used by the WorkflowPostProcess action and its tests
7+
#>
8+
9+
<#
10+
.SYNOPSIS
11+
Calculate the duration of a workflow from a start time
12+
13+
.DESCRIPTION
14+
Calculates the duration in seconds from the provided start time to the current UTC time.
15+
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).
16+
17+
.PARAMETER StartTime
18+
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)
19+
20+
.PARAMETER EndTime
21+
(Optional) The end time as a DateTime object. Defaults to the current UTC time.
22+
23+
.EXAMPLE
24+
$duration = GetWorkflowDuration -StartTime "2025-12-12T10:00:00.0000000Z" -EndTime ([DateTime]::UtcNow)
25+
26+
.EXAMPLE
27+
$duration = GetWorkflowDuration -StartTime ([DateTime]::UtcNow)
28+
#>
29+
function GetWorkflowDuration {
30+
Param(
31+
[Parameter(Mandatory = $true)]
32+
$StartTime,
33+
[Parameter(Mandatory = $false)]
34+
$EndTime = [DateTime]::UtcNow
35+
)
36+
37+
if ($StartTime -is [DateTime]) {
38+
$workflowStartTime = $StartTime.ToUniversalTime()
39+
} else {
40+
$workflowStartTime = [DateTime]::Parse($StartTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal)
41+
}
42+
43+
$workflowDuration = $EndTime.ToUniversalTime().Subtract($workflowStartTime).TotalSeconds
44+
return $workflowDuration
45+
}
46+
47+
Export-ModuleMember -Function GetWorkflowDuration

Actions/.Modules/settings.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@
241241
"type": "boolean",
242242
"description": "Install only referenced apps. See https://aka.ms/ALGoSettings#installonlyreferencedapps"
243243
},
244+
"runTestsInAllInstalledTestApps": {
245+
"type": "boolean",
246+
"description": "PREVIEW: Run tests in all installed test apps. See https://aka.ms/ALGoSettings#runTestsInAllInstalledTestApps"
247+
},
244248
"generateDependencyArtifact": {
245249
"type": "boolean",
246250
"description": "See https://aka.ms/ALGoSettings#generatedependencyartifact"

Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1

Lines changed: 146 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -330,146 +330,180 @@ function ApplyWorkflowDefaultInputs {
330330
return
331331
}
332332

333+
# Get workflow_call inputs
334+
$workflowCallInputs = $yaml.Get('on:/workflow_call:/inputs:/')
335+
333336
# Apply defaults to matching inputs
334-
foreach ($default in $repoSettings.workflowDefaultInputs) {
335-
$inputName = $default.name
336-
$defaultValue = $default.value
337-
338-
# Check if this input exists in the workflow
339-
$inputSection = $inputs.Get("$($inputName):/")
340-
if (-not $inputSection) {
341-
# Input is not present in the workflow
342-
continue
337+
$workflowDispatchInputsModified = $false
338+
$workflowCallInputsModified = $false
339+
340+
foreach ($defaultInput in $repoSettings.workflowDefaultInputs) {
341+
# Apply to workflow_dispatch inputs and only update workflow_call if dispatch was modified
342+
if (ApplyWorkflowDefaultInput -workflowName $workflowName -inputs $inputs -defaultInput $defaultInput) {
343+
$workflowDispatchInputsModified = $true
344+
345+
# Only apply to workflow_call inputs if the workflow_dispatch input was modified
346+
if ($workflowCallInputs) {
347+
if (ApplyWorkflowDefaultInput -workflowName $workflowName -inputs $workflowCallInputs -defaultInput $defaultInput) {
348+
$workflowCallInputsModified = $true
349+
}
350+
}
343351
}
352+
}
344353

345-
# Get the input type from the YAML if specified
346-
$inputType = $null
347-
$typeStart = 0
348-
$typeCount = 0
349-
if ($inputSection.Find('type:', [ref] $typeStart, [ref] $typeCount)) {
350-
$typeLine = $inputSection.content[$typeStart].Trim()
351-
if ($typeLine -match 'type:\s*(.+)') {
352-
$inputType = $matches[1].Trim()
353-
}
354+
# Update the workflow_dispatch section if modified
355+
if ($workflowDispatchInputsModified) {
356+
$workflowDispatch.Replace('inputs:/', $inputs.content)
357+
$yaml.Replace('on:/workflow_dispatch:/', $workflowDispatch.content)
358+
}
359+
360+
# Update workflow_call inputs if modified
361+
if ($workflowCallInputsModified) {
362+
$yaml.Replace('on:/workflow_call:/inputs:/', $workflowCallInputs.content)
363+
}
364+
}
365+
366+
function ApplyWorkflowDefaultInput {
367+
Param(
368+
[string] $workflowName,
369+
[Yaml] $inputs,
370+
[hashtable] $defaultInput
371+
)
372+
373+
$inputName = $defaultInput.name
374+
$defaultValue = $defaultInput.value
375+
376+
# Check if this input exists in the inputs collection
377+
$inputSection = $inputs.Get("$($inputName):/")
378+
if (-not $inputSection) {
379+
# Input is not present in the workflow
380+
return $false
381+
}
382+
383+
# Get the input type from the YAML if specified
384+
$inputType = $null
385+
$typeStart = 0
386+
$typeCount = 0
387+
if ($inputSection.Find('type:', [ref] $typeStart, [ref] $typeCount)) {
388+
$typeLine = $inputSection.content[$typeStart].Trim()
389+
if ($typeLine -match 'type:\s*(.+)') {
390+
$inputType = $matches[1].Trim()
354391
}
392+
}
355393

356-
# Validate that the value type matches the input type
357-
$validationError = $null
358-
if ($inputType) {
359-
switch ($inputType) {
360-
'boolean' {
361-
if ($defaultValue -isnot [bool]) {
362-
$validationError = "Workflow '$workflowName', input '$inputName': Expected boolean value, but got $($defaultValue.GetType().Name). Please use `$true or `$false."
363-
}
394+
# Validate that the value type matches the input type
395+
$validationError = $null
396+
if ($inputType) {
397+
switch ($inputType) {
398+
'boolean' {
399+
if ($defaultValue -isnot [bool]) {
400+
$validationError = "Workflow '$workflowName', input '$inputName': Expected boolean value, but got $($defaultValue.GetType().Name). Please use `$true or `$false."
364401
}
365-
'number' {
366-
if ($defaultValue -isnot [int] -and $defaultValue -isnot [long] -and $defaultValue -isnot [double]) {
367-
$validationError = "Workflow '$workflowName', input '$inputName': Expected number value, but got $($defaultValue.GetType().Name)."
368-
}
402+
}
403+
'number' {
404+
if ($defaultValue -isnot [int] -and $defaultValue -isnot [long] -and $defaultValue -isnot [double]) {
405+
$validationError = "Workflow '$workflowName', input '$inputName': Expected number value, but got $($defaultValue.GetType().Name)."
369406
}
370-
'string' {
371-
if ($defaultValue -isnot [string]) {
372-
$validationError = "Workflow '$workflowName', input '$inputName': Expected string value, but got $($defaultValue.GetType().Name)."
373-
}
407+
}
408+
'string' {
409+
if ($defaultValue -isnot [string]) {
410+
$validationError = "Workflow '$workflowName', input '$inputName': Expected string value, but got $($defaultValue.GetType().Name)."
374411
}
375-
'choice' {
376-
# Choice inputs accept strings and must match one of the available options (case-sensitive)
377-
if ($defaultValue -isnot [string]) {
378-
$validationError = "Workflow '$workflowName', input '$inputName': Expected string value for choice input, but got $($defaultValue.GetType().Name)."
379-
}
380-
else {
381-
# Validate that the value is one of the available options
382-
$optionsStart = 0
383-
$optionsCount = 0
384-
if ($inputSection.Find('options:', [ref] $optionsStart, [ref] $optionsCount)) {
385-
$availableOptions = @()
386-
# Parse the options from the YAML (they are indented list items starting with "- ")
387-
for ($i = $optionsStart + 1; $i -lt ($optionsStart + $optionsCount); $i++) {
388-
$optionLine = $inputSection.content[$i].Trim()
389-
if ($optionLine -match '^-\s*(.+)$') {
390-
$availableOptions += $matches[1].Trim()
391-
}
412+
}
413+
'choice' {
414+
# Choice inputs accept strings and must match one of the available options (case-sensitive)
415+
if ($defaultValue -isnot [string]) {
416+
$validationError = "Workflow '$workflowName', input '$inputName': Expected string value for choice input, but got $($defaultValue.GetType().Name)."
417+
}
418+
else {
419+
# Validate that the value is one of the available options
420+
$optionsStart = 0
421+
$optionsCount = 0
422+
if ($inputSection.Find('options:', [ref] $optionsStart, [ref] $optionsCount)) {
423+
$availableOptions = @()
424+
# Parse the options from the YAML (they are indented list items starting with "- ")
425+
for ($i = $optionsStart + 1; $i -lt ($optionsStart + $optionsCount); $i++) {
426+
$optionLine = $inputSection.content[$i].Trim()
427+
if ($optionLine -match '^-\s*(.+)$') {
428+
$availableOptions += $matches[1].Trim()
392429
}
430+
}
393431

394-
if ($availableOptions.Count -gt 0 -and $availableOptions -cnotcontains $defaultValue) {
395-
$validationError = "Workflow '$workflowName', input '$inputName': Value '$defaultValue' is not a valid choice (case-sensitive match required). Available options: $($availableOptions -join ', ')."
396-
}
432+
if ($availableOptions.Count -gt 0 -and $availableOptions -cnotcontains $defaultValue) {
433+
$validationError = "Workflow '$workflowName', input '$inputName': Value '$defaultValue' is not a valid choice (case-sensitive match required). Available options: $($availableOptions -join ', ')."
397434
}
398435
}
399436
}
400437
}
401438
}
402-
else {
403-
# If no type is specified in the workflow, it defaults to string
404-
if ($defaultValue -isnot [string]) {
405-
OutputWarning "Workflow '$workflowName', input '$inputName': No type specified in workflow (defaults to string), but configured value is $($defaultValue.GetType().Name). This may cause issues."
406-
}
439+
}
440+
else {
441+
# If no type is specified in the workflow, it defaults to string
442+
if ($defaultValue -isnot [string]) {
443+
OutputWarning "Workflow '$workflowName', input '$inputName': No type specified in workflow (defaults to string), but configured value is $($defaultValue.GetType().Name). This may cause issues."
407444
}
445+
}
408446

409-
if ($validationError) {
410-
throw $validationError
411-
}
447+
if ($validationError) {
448+
throw $validationError
449+
}
412450

413-
# Convert the default value to the appropriate YAML format
414-
$yamlValue = $defaultValue
415-
if ($defaultValue -is [bool]) {
416-
$yamlValue = $defaultValue.ToString().ToLower()
417-
}
418-
elseif ($defaultValue -is [string]) {
419-
# Quote strings and escape single quotes per YAML spec
420-
$escapedValue = $defaultValue.Replace("'", "''")
421-
$yamlValue = "'$escapedValue'"
422-
}
451+
# Convert the default value to the appropriate YAML format
452+
$yamlValue = $defaultValue
453+
if ($defaultValue -is [bool]) {
454+
$yamlValue = $defaultValue.ToString().ToLower()
455+
}
456+
elseif ($defaultValue -is [string]) {
457+
# Quote strings and escape single quotes per YAML spec
458+
$escapedValue = $defaultValue.Replace("'", "''")
459+
$yamlValue = "'$escapedValue'"
460+
}
423461

424-
# Find and replace the default: line in the input section
425-
$start = 0
426-
$count = 0
427-
if ($inputSection.Find('default:', [ref] $start, [ref] $count)) {
428-
# Replace existing default value
429-
$inputSection.Replace('default:', "default: $yamlValue")
462+
# Find and replace the default: line in the input section
463+
$start = 0
464+
$count = 0
465+
466+
if ($inputSection.Find('default:', [ref] $start, [ref] $count)) {
467+
# Replace existing default value
468+
$inputSection.Replace('default:', "default: $yamlValue")
469+
}
470+
else {
471+
# Add default value - find the best place to insert it
472+
# Insert after type, required, or description (whichever comes last)
473+
$insertAfter = -1
474+
$typeLine = 0
475+
$typeCount = 0
476+
$requiredLine = 0
477+
$requiredCount = 0
478+
$descLine = 0
479+
$descCount = 0
480+
481+
if ($inputSection.Find('type:', [ref] $typeLine, [ref] $typeCount)) {
482+
$insertAfter = $typeLine + $typeCount
430483
}
431-
else {
432-
# Add default value - find the best place to insert it
433-
# Insert after type, required, or description (whichever comes last)
434-
$insertAfter = -1
435-
$typeLine = 0
436-
$typeCount = 0
437-
$requiredLine = 0
438-
$requiredCount = 0
439-
$descLine = 0
440-
$descCount = 0
441-
442-
if ($inputSection.Find('type:', [ref] $typeLine, [ref] $typeCount)) {
443-
$insertAfter = $typeLine + $typeCount
484+
if ($inputSection.Find('required:', [ref] $requiredLine, [ref] $requiredCount)) {
485+
if (($requiredLine + $requiredCount) -gt $insertAfter) {
486+
$insertAfter = $requiredLine + $requiredCount
444487
}
445-
if ($inputSection.Find('required:', [ref] $requiredLine, [ref] $requiredCount)) {
446-
if (($requiredLine + $requiredCount) -gt $insertAfter) {
447-
$insertAfter = $requiredLine + $requiredCount
448-
}
449-
}
450-
if ($inputSection.Find('description:', [ref] $descLine, [ref] $descCount)) {
451-
if (($descLine + $descCount) -gt $insertAfter) {
452-
$insertAfter = $descLine + $descCount
453-
}
454-
}
455-
456-
if ($insertAfter -eq -1) {
457-
# No other properties, insert at position 1 (after the input name)
458-
$insertAfter = 1
488+
}
489+
if ($inputSection.Find('description:', [ref] $descLine, [ref] $descCount)) {
490+
if (($descLine + $descCount) -gt $insertAfter) {
491+
$insertAfter = $descLine + $descCount
459492
}
493+
}
460494

461-
$inputSection.Insert($insertAfter, "default: $yamlValue")
495+
if ($insertAfter -eq -1) {
496+
# No other properties, insert at position 1 (after the input name)
497+
$insertAfter = 1
462498
}
463499

464-
# Update the inputs section with the modified input
465-
$inputs.Replace("$($inputName):/", $inputSection.content)
500+
$inputSection.Insert($insertAfter, "default: $yamlValue")
466501
}
467502

468-
# Update the workflow_dispatch section with modified inputs
469-
$workflowDispatch.Replace('inputs:/', $inputs.content)
503+
# Update the inputs collection with the modified input section
504+
$inputs.Replace("$($inputName):/", $inputSection.content)
470505

471-
# Update the on: section with modified workflow_dispatch
472-
$yaml.Replace('on:/workflow_dispatch:/', $workflowDispatch.content)
506+
return $true
473507
}
474508

475509
function GetWorkflowContentWithChangesFromSettings {

Actions/RunPipeline/RunPipeline.ps1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ try {
209209
}
210210
}
211211

212+
if ($settings.runTestsInAllInstalledTestApps) {
213+
# Trim parentheses from test apps. Run-ALPipeline will skip running tests in test apps wrapped in ()
214+
$install.TestApps = $install.TestApps | ForEach-Object { $_.TrimStart("(").TrimEnd(")") }
215+
}
216+
212217
# Replace secret names in install.apps and install.testApps
213218
foreach($list in @('Apps','TestApps')) {
214219
$install."$list" = @($install."$list" | ForEach-Object {
@@ -237,6 +242,7 @@ try {
237242

238243
# Analyze InstallApps and InstallTestApps before launching pipeline
239244

245+
240246
# Check if codeSignCertificateUrl+Password is used (and defined)
241247
if (!$settings.doNotSignApps -and $codeSignCertificateUrl -and $codeSignCertificatePassword -and !$settings.keyVaultCodesignCertificateName) {
242248
OutputWarning -message "Using the legacy CodeSignCertificateUrl and CodeSignCertificatePassword parameters. Consider using the new Azure Keyvault signing instead. Go to https://aka.ms/ALGoSettings#keyVaultCodesignCertificateName to find out more"

Actions/WorkflowInitialize/WorkflowInitialize.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ TestALGoRepository
3030
TestRunnerPrerequisites
3131

3232
# Create a json object that contains an entry for the workflowstarttime
33+
# Use ISO 8601 format for locale-agnostic serialization
3334
$scopeJson = @{
34-
"workflowStartTime" = [DateTime]::UtcNow
35+
"workflowStartTime" = [DateTime]::UtcNow.ToString("o")
3536
} | ConvertTo-Json -Compress
3637

3738
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "telemetryScopeJson=$scopeJson"

0 commit comments

Comments
 (0)