From 3324a09c3ae84f955eadee61d825060dcbe1206a Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 16 Oct 2025 14:52:03 +0200 Subject: [PATCH 01/92] Move AL-Go files to update to settings --- Actions/.Modules/ReadSettings.psm1 | 16 ++ .../CheckForUpdates.HelperFunctions.ps1 | 141 ++++++++++++ Actions/CheckForUpdates/CheckForUpdates.ps1 | 208 ++++++------------ Tests/CheckForUpdates.Action.Test.ps1 | 154 +++++++++++++ 4 files changed, 378 insertions(+), 141 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 78ec9b30c..8c31f45bd 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -240,6 +240,22 @@ function GetDefaultSettings "useGitSubmodules" = "false" "gitSubmodulesTokenSecretName" = "gitSubmodulesToken" "shortLivedArtifactsRetentionDays" = 1 # 0 means use GitHub default + "updateALGoFiles" = [ordered]@{ + "filesToUpdate" = @( + [ordered]@{ 'destinationPath' = (Join-Path '.github' 'workflows'); 'destinationName' = ''; 'sourcePath' = (Join-Path '.github' 'workflows'); 'filter' = '*'; 'type' = 'workflow'; 'perProject' = $false } + [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = '*.copy.md'; 'type' = 'releasenotes'; 'perProject' = $false } + [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $false } + [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings'; 'perProject' = $false } + [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings'; 'perProject' = $false } + [ordered]@{ 'destinationPath' = ([system.IO.Path]::GetDirectoryName($CustomTemplateRepoSettingsFile)); 'destinationName' = ([system.IO.Path]::GetFileName($CustomTemplateRepoSettingsFile)); 'sourcePath' = ([system.IO.Path]::GetDirectoryName($RepoSettingsFile)); 'filter' = ([system.IO.Path]::GetFileName($RepoSettingsFile)); 'type' = 'template repo settings'; 'perProject' = $false } + [ordered]@{ 'destinationPath' = ([system.IO.Path]::GetDirectoryName($CustomTemplateProjectSettingsFile)); 'destinationName' = ([system.IO.Path]::GetFileName($CustomTemplateProjectSettingsFile)); 'sourcePath' = ([system.IO.Path]::GetDirectoryName($ALGoSettingsFile)); 'filter' = ([system.IO.Path]::GetFileName($ALGoSettingsFile)); 'type' = 'template project settings'; 'perProject' = $false } + + [ordered]@{ 'destinationPath' = '.AL-Go'; 'destinationName' = ''; 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, + [ordered]@{ 'destinationPath' = '.AL-Go'; 'destinationName' = ''; 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } + ) + "filesToIgnore" = @() + "filesToRemove" = @() + } } } diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 83e79150a..f554d36ac 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -560,3 +560,144 @@ function UpdateSettingsFile { } return $modified } + +function ResolveFilePaths { + Param( + [Parameter(Mandatory=$true)] + [string] $sourceFolder, + [string] $originalSourceFolder = $null, + [string] $destinationFolder = $null, + [Parameter(Mandatory=$true)] + [array] $files, + [string[]] $projects = @() + ) + + $fullFilePaths = @() + foreach($file in $files) { + if(-not $file.sourcePath) { + throw "sourcePath is required for action $action" + } + # All files are relative to the template folder + Write-Host "Resolving files for sourcePath '$($file.sourcePath)' and filter '$($file.filter)'" + $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourcePath) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) + + Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to template folder '$sourceFolder')" + if(-not $sourceFiles) { + Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to template folder '$sourceFolder')" + continue + } + + foreach($srcFile in $sourceFiles) { + $fullFilePath = @{ + 'sourceFullPath' = $srcFile + 'originalSourceFullPath' = $null + 'type' = $null + 'destinationFullPath' = $null + } + + # Try to find the same files in the original template folder if it is specified + if ($originalSourceFolder) { + Push-Location $sourceFolder + $relativePath = Resolve-Path -Path $srcFile -Relative # resolve the path relative to the current location (template folder) + Pop-Location + if (Test-Path (Join-Path $originalSourceFolder $relativePath) -PathType Leaf) { + # If the file exists in the original template folder, use that file instead + $fullFilePath.originalSourceFullPath = Join-Path $originalSourceFolder $relativePath -Resolve + } + } + + if($file.Keys -contains 'type') { + $fullFilePath.type = $file.type # propagate the type if it exists + } + + if(-not $destinationFolder) { + # Destination folder is not specified, so we only return the source full path + $fullFilePaths += $fullFilePath + continue + } + + $dstFileName = $file.destinationName + if(-not $dstFileName) { + # If destinationName is not specified, use the source file name + $dstFileName = Split-Path -Path $srcFile -Leaf + } + + if($file.Keys -contains 'perProject' -and $file.perProject -eq $true) { + # Multiple file entries, one for each project + # Destination full path is the destination base folder + project + destinationPath + destinationName + + foreach($project in $projects) { + $projectFile = $fullFilePath.Clone() + + $projectFile.destinationFullPath = Join-Path $destinationFolder $project + $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $file.destinationPath + $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $dstFileName + + $fullFilePaths += $projectFile + } + } + else { + # Single file entry + # Destination full path is the destination base folder + destinationPath + destinationName + + $fullFilePath.destinationFullPath = Join-Path $destinationFolder $file.destinationPath + $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $dstFileName + + $fullFilePaths += $fullFilePath + } + } + } + # Remove duplicates (when sourceFullPath and destinationFullPath are the same) + $fullFilePaths = $fullFilePaths | Sort-Object sourceFullPath, destinationFullPath -Unique + + return $fullFilePaths +} + +# TODO Add tests for GetFilesToUpdate +function GetFilesToUpdate { + Param( + $settings, + $projects, + $baseFolder, + $templateFolder, + $originalTemplateFolder = $null + ) + + $filesToUpdate = $settings.updateALGoFiles.filesToUpdate + $filesToUpdate = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToUpdate -projects $projects + Write-Host "Files to update:" + $filesToUpdate | ForEach-Object { Write-Host " $(ConvertTo-Json $_)"} + + $filesToIgnore = $settings.updateALGoFiles.filesToIgnore + $filesToIgnore = ResolveFilePaths -sourceFolder $baseFolder -files $filesToIgnore -projects $projects + Write-Host "Files to ignore:" + $filesToIgnore | ForEach-Object { Write-Host " $(ConvertTo-Json $_)" } + + $filesToRemove = $settings.updateALGoFiles.filesToRemove + $filesToRemove = ResolveFilePaths -sourceFolder $baseFolder -files $filesToRemove -projects $projects + Write-Host "Files to remove:" + $filesToRemove | ForEach-Object { Write-Host " $(ConvertTo-Json $_)" } + + # Exclude files to ignore from files to update + $filesToUpdate = $filesToUpdate | Where-Object { + $fileToUpdate = $_ + $include = -not ($filesToIgnore | Where-Object { $_.sourceFullPath -eq $fileToUpdate.sourceFullPath }) + if(-not $include) { + Write-Host "Excluding file $($fileToUpdate.sourceFullPath) from update as it is in the ignore list" + } + return $include + } + + # Exclude files to remove from files to update + $filesToUpdate = $filesToUpdate | Where-Object { + $fileToUpdate = $_ + $include = -not ($filesToRemove | Where-Object { $_.sourceFullPath -eq $fileToUpdate.destinationFullPath }) # Note: comparing sourceFullPath of files to remove with destinationFullPath of files to update + if(-not $include) { + Write-Host "Excluding file $($fileToUpdate.sourceFullPath) from update as it is in the remove list" + } + return $include + } + + return $filesToUpdate, $filesToRemove +} + diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 8cf3511f2..2f6b16284 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -105,53 +105,29 @@ if (-not $isDirectALGo) { # Keep TemplateUrl and TemplateSha pointing to the custom template repository $templateBranch = $originalTemplateUrl.Split('@')[1] $templateOwner = $originalTemplateUrl.Split('/')[3] - - # If the custom template contains unusedALGoSystemFiles, we need to remove them from the current repository - if ($templateRepoSettings.ContainsKey('unusedALGoSystemFiles')) { - $unusedALGoSystemFiles += $templateRepoSettings.unusedALGoSystemFiles - } } } } -# CheckFiles is an array of hashtables with the following properties: -# dstPath: The path to the file in the current repository -# srcPath: The path to the file in the template repository -# pattern: The pattern to use when searching for files in the template repository -# type: The type of file (script, workflow, releasenotes) -# The files currently checked are: -# - All files in .github/workflows -# - All files in .github that ends with .copy.md -# - All PowerShell scripts in .AL-Go folders (all projects) -$checkfiles = @( - @{ 'dstPath' = (Join-Path '.github' 'workflows'); 'dstName' = ''; 'srcPath' = (Join-Path '.github' 'workflows'); 'pattern' = '*'; 'type' = 'workflow' }, - @{ 'dstPath' = '.github'; 'dstName' = ''; 'srcPath' = '.github'; 'pattern' = '*.copy.md'; 'type' = 'releasenotes' } - @{ 'dstPath' = '.github'; 'dstName' = ''; 'srcPath' = '.github'; 'pattern' = '*.ps1'; 'type' = 'script' } - @{ 'dstPath' = '.github'; 'dstName' = ''; 'srcPath' = '.github'; 'pattern' = 'AL-Go-Settings.json'; 'type' = 'settings' }, - @{ 'dstPath' = '.github'; 'dstName' = ''; 'srcPath' = '.github'; 'pattern' = '*.settings.json'; 'type' = 'settings' } -) - -if ($originalTemplateFolder) { - $checkfiles += @( - @{ 'dstPath' = ([system.IO.Path]::GetDirectoryName($CustomTemplateRepoSettingsFile)); 'dstName' = ([system.IO.Path]::GetFileName($CustomTemplateRepoSettingsFile)); 'SrcPath' = ([system.IO.Path]::GetDirectoryName($RepoSettingsFile)); 'pattern' = ([system.IO.Path]::GetFileName($RepoSettingsFile)); 'type' = 'template repo settings' } - @{ 'dstPath' = ([system.IO.Path]::GetDirectoryName($CustomTemplateProjectSettingsFile)); 'dstName' = ([system.IO.Path]::GetFileName($CustomTemplateProjectSettingsFile)); 'SrcPath' = ([system.IO.Path]::GetDirectoryName($ALGoSettingsFile)); 'pattern' = ([system.IO.Path]::GetFileName($ALGoSettingsFile)); ; 'type' = 'template project settings' } - ) -} - # Get the list of projects in the current repository $baseFolder = $ENV:GITHUB_WORKSPACE $projects = @(GetProjectsFromRepository -baseFolder $baseFolder -projectsFromSettings $repoSettings.projects) -Write-Host "Projects found: $($projects.Count)" -foreach($project in $projects) { - Write-Host "- $project" - $checkfiles += @( - @{ 'dstPath' = Join-Path $project '.AL-Go'; 'dstName' = ''; 'srcPath' = '.AL-Go'; 'pattern' = '*.ps1'; 'type' = 'script' }, - @{ 'dstPath' = Join-Path $project '.AL-Go'; 'dstName' = ''; 'srcPath' = '.AL-Go'; 'pattern' = 'settings.json'; 'type' = 'settings' } - ) + +$filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder + +#Exclude unusedALGoSystemFiles from $filesToUpdate and add them to $filesToRemove +$unusedFilesToRemove = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -contains (Split-Path -Path $_.sourceFullPath -Leaf) } +if ($unusedFilesToRemove) { + Write-Host "The following files are marked as unused and will be removed if they exist:" + $unusedFilesToRemove | ForEach-Object { Write-Host "- $($_.destinationFullPath)" } + + $filesToUpdate = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) } + $filesToRemove += @($unusedFilesToRemove | ForEach-Object { @{ 'sourceFullPath' = $_.destinationFullPath } }) } # $updateFiles will hold an array of files, which needs to be updated $updateFiles = @() + # $removeFiles will hold an array of files, which needs to be removed $removeFiles = @() @@ -166,121 +142,71 @@ if ($projects.Count -gt 1) { } # Loop through all folders in CheckFiles and check if there are any files that needs to be updated -foreach($checkfile in $checkfiles) { - Write-Host "Checking $($checkfile.srcPath)/$($checkfile.pattern)" - $type = $checkfile.type - $srcPath = $checkfile.srcPath - $dstPath = $checkfile.dstPath - $dstFolder = Join-Path $baseFolder $dstPath - $srcFolder = GetSrcFolder -repoType $repoSettings.type -templateUrl $templateUrl -templateFolder $templateFolder -srcPath $srcPath - $originalSrcFolder = $null - if ($originalTemplateFolder -and $type -notlike 'template*settings') { - # Get Original source folder except for template settings - these are applied from the custom template repository - $originalSrcFolder = GetSrcFolder -repoType $repoSettings.type -templateUrl $originalTemplateUrl -templateFolder $originalTemplateFolder -srcPath $srcPath +foreach($fileToUpdate in $filesToUpdate) { + $type = $fileToUpdate.type + $srcPath = $fileToUpdate.sourceFullPath + $originalSrcPath = $fileToUpdate.originalSourceFullPath + if(-not $originalSrcPath) { + $originalSrcPath = $srcPath } - if ($srcFolder) { - Push-Location -Path $srcFolder - try { - # Remove unused AL-Go system files - $unusedALGoSystemFiles | ForEach-Object { - if (Test-Path -Path (Join-Path $dstFolder $_) -PathType Leaf) { - Write-Host "Remove unused AL-Go system file: $_" - $removeFiles += @(Join-Path $dstPath $_) - } - } - # Loop through all files in the template repository matching the pattern - Get-ChildItem -Path $srcFolder -Filter $checkfile.pattern | ForEach-Object { - # Read the template file and modify it based on the settings - # Compare the modified file with the file in the current repository - if ($checkfile.dstName) { - $filename = $checkfile.dstName - } - else { - $filename = $_.Name - } - Write-Host "- $filename" - $dstFile = Join-Path $dstFolder $filename - $srcFile = $_.FullName - $originalSrcFile = $srcFile - $isFileDirectALGo = $isDirectALGo - Write-Host "SrcFolder: $srcFolder" - if ($originalSrcFolder) { - # if SrcFile is a custom template repository, we need to find the file in the "original" template repository - $fname = Join-Path $originalSrcFolder (Resolve-Path $srcFile -Relative) - if (Test-Path -Path $fname -PathType Leaf) { - Write-Host "File is available in the 'original' template repository" - $originalSrcFile = $fname - $isFileDirectALGo = IsDirectALGo -templateUrl $originalTemplateUrl - } - } - $dstFileExists = Test-Path -Path $dstFile -PathType Leaf - if ($unusedALGoSystemFiles -contains $fileName) { - # File is not used by AL-Go, remove it if it exists - # do not add it to $updateFiles if it does not exist - if ($dstFileExists) { - Write-Host "Removing $type ($(Join-Path $dstPath $filename)) as it is marked as unused." - $removeFiles += @(Join-Path $dstPath $filename) - } - return - } - switch ($type) { - "workflow" { - # For workflow files, we might need to modify the file based on the settings - $srcContent = GetWorkflowContentWithChangesFromSettings -srcFile $originalsrcFile -repoSettings $repoSettings -depth $depth -includeBuildPP $includeBuildPP - } - "settings" { - # For settings files, we need to modify the file based on the settings - $srcContent = GetModifiedSettingsContent -srcSettingsFile $originalSrcFile -dstSettingsFile $dstFile - } - Default { - # For non-workflow files, just read the file content - $srcContent = Get-ContentLF -Path $originalSrcFile - } - } - # Replace static placeholders - $srcContent = $srcContent.Replace('{TEMPLATEURL}', $templateUrl) + $dstPath = $fileToUpdate.destinationFullPath - if ($isFileDirectALGo) { - # If we are using direct AL-Go repo, we need to change the owner to the templateOwner, the repo names to AL-Go and AL-Go/Actions and the branch to templateBranch - ReplaceOwnerRepoAndBranch -srcContent ([ref]$srcContent) -templateOwner $templateOwner -templateBranch $templateBranch - } + $dstFileExists = Test-Path -Path $dstPath -PathType Leaf - if ($type -eq 'workflow' -and $originalSrcFile -ne $srcFile) { - # Apply customizations from custom template repository - Write-Host "Apply customizations from custom template repository, file: $srcFile" - [Yaml]::ApplyTemplateCustomizations([ref] $srcContent, $srcFile) - } + switch ($type) { + "workflow" { + # For workflow files, we might need to modify the file based on the settings + $srcContent = GetWorkflowContentWithChangesFromSettings -srcFile $originalSrcPath -repoSettings $repoSettings -depth $depth -includeBuildPP $includeBuildPP + } + "settings" { + # For settings files, we need to modify the file based on the settings + $srcContent = GetModifiedSettingsContent -srcSettingsFile $originalSrcPath -dstSettingsFile $dstPath + } + Default { + # For non-workflow files, just read the file content + $srcContent = Get-ContentLF -Path $originalSrcPath + } + } + # Replace static placeholders + $srcContent = $srcContent.Replace('{TEMPLATEURL}', $templateUrl) - if ($dstFileExists) { - if ($type -eq 'workflow') { - Write-Host "Apply customizations from current repository, file: $dstFile" - [Yaml]::ApplyFinalCustomizations([ref] $srcContent, $dstFile) - } + if ($isFileDirectALGo) { + # If we are using direct AL-Go repo, we need to change the owner to the templateOwner, the repo names to AL-Go and AL-Go/Actions and the branch to templateBranch + ReplaceOwnerRepoAndBranch -srcContent ([ref]$srcContent) -templateOwner $templateOwner -templateBranch $templateBranch + } - # file exists, compare and add to $updateFiles if different - $dstContent = Get-ContentLF -Path $dstFile - if ($dstContent -cne $srcContent) { - Write-Host "Updated $type ($(Join-Path $dstPath $filename)) available" - $updateFiles += @{ "DstFile" = Join-Path $dstPath $filename; "content" = $srcContent } - } - else { - Write-Host "No changes in $type ($(Join-Path $dstPath $filename))" - } - } - else { - # new file, add to $updateFiles - Write-Host "New $type ($(Join-Path $dstPath $filename)) available" - $updateFiles += @{ "DstFile" = Join-Path $dstPath $filename; "content" = $srcContent } - } - } + if ($type -eq 'workflow' -and $originalSrcPath -ne $srcPath) { + # Apply customizations from custom template repository + Write-Host "Apply customizations from custom template repository, file: $srcPath" + [Yaml]::ApplyTemplateCustomizations([ref] $srcContent, $srcPath) + } + + if ($dstFileExists) { + if ($type -eq 'workflow') { + Write-Host "Apply customizations from current repository, file: $dstFile" + [Yaml]::ApplyFinalCustomizations([ref] $srcContent, $dstFile) + } + + # file exists, compare and add to $updateFiles if different + $dstContent = Get-ContentLF -Path $dstFile + if ($dstContent -cne $srcContent) { + Write-Host "Updated $type ($(Join-Path $dstPath $filename)) available" + $updateFiles += @{ "DstFile" = Join-Path $dstPath $filename; "content" = $srcContent } } - finally { - Pop-Location + else { + Write-Host "No changes in $type ($(Join-Path $dstPath $filename))" } } + else { + # new file, add to $updateFiles + Write-Host "New $type ($(Join-Path $dstPath $filename)) available" + $updateFiles += @{ "DstFile" = Join-Path $dstPath $filename; "content" = $srcContent } + } } -$removeFiles = @($removeFiles | Select-Object -Unique) + +# Remove files that are in $filesToRemove and exist in the repository +$removeFiles = $filesToRemove | ForEach-Object { $_.sourceFullPath } | Where-Object { Test-Path -Path $_ -PathType Leaf } if ($update -ne 'Y') { # $update not set, just issue a warning in the CI/CD workflow that updates are available diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index ee498dd2a..6768aea4c 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -268,3 +268,157 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { $modifiedContent."`$schema" | Should -Be "someSchema" } } + +Describe "ResolveFilePaths" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + + $rootFolder = $PSScriptRoot + + $sourcePath = "sourcePath" + $sourceFolder = Join-Path $rootFolder $sourcePath + if (-not (Test-Path $sourceFolder)) { + New-Item -Path $sourceFolder -ItemType Directory | Out-Null + } + # Create a source folder structure + New-Item -Path (Join-Path $sourceFolder "folder/File1.txt") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $sourceFolder "folder/File2.log") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $sourceFolder "folder/File3.txt") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $sourceFolder "folder/File4.md") -ItemType File -Force | Out-Null + + $originalSourceFolder = Join-Path $rootFolder "originalSourceFolder" + if (-not (Test-Path $originalSourceFolder)) { + New-Item -Path $originalSourceFolder -ItemType Directory | Out-Null + } + New-Item -Path (Join-Path $originalSourceFolder "folder/File1.txt") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $originalSourceFolder "folder/File2.log") -ItemType File -Force | Out-Null + } + + AfterAll { + # Clean up + if (Test-Path $sourceFolder) { + Remove-Item -Path $sourceFolder -Recurse -Force + } + + if (Test-Path $originalSourceFolder) { + Remove-Item -Path $originalSourceFolder -Recurse -Force + } + } + + It 'ResolveFilePaths with specific files extensions' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $rootFolder $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = 'newFolder'; "destinationName" = "" } + @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = 'newFolder'; "destinationName" = "" } + ) + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + $fullFilePaths[0].type | Should -Be $null + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + $fullFilePaths[1].type | Should -Be $null + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") + $fullFilePaths[2].type | Should -Be $null + } + + It 'ResolveFilePaths with specific destination names' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $rootFolder $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "File1.txt"; "destinationPath" = 'newFolder'; "destinationName" = "CustomFile1.txt" } + @{ "sourcePath" = "folder"; "filter" = "File2.log"; "destinationPath" = 'newFolder'; "destinationName" = "CustomFile2.log" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile1.txt") + $fullFilePaths[0].type | Should -Be $null + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile2.log") + $fullFilePaths[1].type | Should -Be $null + } + + It 'ResolveFilePaths with type' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "folder"; "destinationName" = ""; type = "text" } + @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "folder"; "destinationName" = ""; type = "markdown" } + ) + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + # Verify destinationFullPath is not filled + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File3.txt") + $fullFilePaths[1].type | Should -Be "text" + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[2].type | Should -Be "markdown" + } + + It 'ResolveFilePaths with original source folder' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "text" } + @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "markdown" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" + + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") # File3.txt doesn't exist in original source folder, so it should still point to the source folder + $fullFilePaths[1].originalSourceFullPath | Should -Be $null + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + $fullFilePaths[1].type | Should -Be "text" + + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") # File4.md doesn't exist in original source folder, so it should still point to the source folder + $fullFilePaths[2].originalSourceFullPath | Should -Be $null + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") + $fullFilePaths[2].type | Should -Be "markdown" + } + + It 'ResolveFilePaths returns unique file paths' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; } + @{ "sourcePath" = "folder"; "filter" = "*"; "destinationPath" = "newFolder"; "destinationName" = ""; } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 4 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") + } +} From 2a0bb32fde694cbc987aa45ef1b13a2f7e8170c6 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 17 Oct 2025 16:16:21 +0200 Subject: [PATCH 02/92] Add schema for updateALGoFiles --- Actions/.Modules/settings.schema.json | 108 ++++++++++++++++++++++++-- Tests/ReadSettings.Test.ps1 | 14 ++-- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index b498eb32b..b6c3f2c5b 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -375,7 +375,9 @@ "type": "array", "items": { "type": "string", - "enum": ["includeDependencies"] + "enum": [ + "includeDependencies" + ] }, "description": "An array of settings to be overwritten by the current deliverToAppSource setting. See https://aka.ms/ALGoSettings#overwriteSettings" } @@ -608,7 +610,10 @@ "type": "array", "items": { "type": "string", - "enum": ["includeProjects", "excludeProjects"] + "enum": [ + "includeProjects", + "excludeProjects" + ] }, "description": "An array of settings to be overwritten by the current alDoc setting. See https://aka.ms/ALGoSettings#overwriteSettings" } @@ -624,7 +629,10 @@ }, "pullRequestMergeMethod": { "type": "string", - "enum": ["merge", "squash"], + "enum": [ + "merge", + "squash" + ], "default": "squash" }, "pullRequestLabels": { @@ -638,7 +646,9 @@ "type": "array", "items": { "type": "string", - "enum": ["pullRequestLabels"] + "enum": [ + "pullRequestLabels" + ] }, "description": "An array of settings to be overwritten by the current commitOptions setting. See https://aka.ms/ALGoSettings#overwriteSettings" } @@ -686,13 +696,99 @@ "type": "array", "items": { "type": "string", - "enum": ["unusedALGoSystemFiles", "projects", "additionalCountries", "appDependencies", "appFolders", "testDependencies", "testFolders", "bcptTestFolders", "pageScriptingTests", "restoreDatabases", "installApps", "installTestApps", "customCodeCops", "configPackages", "appSourceCopMandatoryAffixes", "deliverToAppSource", "appDependencyProbingPaths", "incrementalBuilds", "environments", "buildModes", "bcptThresholds", "fullBuildPatterns", "excludeEnvironments", "alDoc", "commitOptions", "trustedSigning"] + "enum": [ + "unusedALGoSystemFiles", + "projects", + "additionalCountries", + "appDependencies", + "appFolders", + "testDependencies", + "testFolders", + "bcptTestFolders", + "pageScriptingTests", + "restoreDatabases", + "installApps", + "installTestApps", + "customCodeCops", + "configPackages", + "appSourceCopMandatoryAffixes", + "deliverToAppSource", + "appDependencyProbingPaths", + "incrementalBuilds", + "environments", + "buildModes", + "bcptThresholds", + "fullBuildPatterns", + "excludeEnvironments", + "alDoc", + "commitOptions", + "trustedSigning" + ] }, "description": "An array of settings to be overwritten by the current settings. See https://aka.ms/ALGoSettings#overwriteSettings" }, "reportSuppressedDiagnostics": { "type": "boolean", "description": "Report suppressed diagnostics. See https://aka.ms/ALGoSettings#reportsuppresseddiagnostics" + }, + "updateALGoFiles": { + "type": "object", + "properties": { + "filesToUpdate": { + "type": "array", + "items": { + "type": "object", + "properties": { + "destinationPath": { + "type": "string" + }, + "destinationName": { + "type": "string" + }, + "sourcePath": { + "type": "string" + }, + "filter": { + "type": "string" + }, + "type": { + "type": "string" + }, + "perProject": { + "type": "boolean" + } + } + } + }, + "filesToIgnore": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sourcePath": { + "type": "string" + }, + "filter": { + "type": "string" + } + } + } + }, + "filesToRemove": { + "type": "array", + "items": { + "type": "object", + "properties": { + "sourcePath": { + "type": "string" + }, + "filter": { + "type": "string" + } + } + } + } + } } } -} +} \ No newline at end of file diff --git a/Tests/ReadSettings.Test.ps1 b/Tests/ReadSettings.Test.ps1 index 18dc52beb..41f388575 100644 --- a/Tests/ReadSettings.Test.ps1 +++ b/Tests/ReadSettings.Test.ps1 @@ -261,14 +261,14 @@ InModuleScope ReadSettings { # Allows testing of private functions It 'Default settings match schema' -Skip:($PSVersionTable.PSVersion.Major -lt 7) { $defaultSettings = GetDefaultSettings - Test-Json -json (ConvertTo-Json $defaultSettings) -schema $schema | Should -Be $true + Test-Json -json (ConvertTo-Json $defaultSettings -Depth 99) -schema $schema | Should -Be $true } It 'Shell setting can only be pwsh or powershell' -Skip:($PSVersionTable.PSVersion.Major -lt 7) { $defaultSettings = GetDefaultSettings $defaultSettings.shell = 42 try { - Test-Json -json (ConvertTo-Json $defaultSettings) -schema $schema + Test-Json -json (ConvertTo-Json $defaultSettings -Depth 99) -schema $schema } catch { $_.Exception.Message | Should -Be "The JSON is not valid with the schema: Value is `"integer`" but should be `"string`" at '/shell'" @@ -276,7 +276,7 @@ InModuleScope ReadSettings { # Allows testing of private functions $defaultSettings.shell = "random" try { - Test-Json -json (ConvertTo-Json $defaultSettings) -schema $schema + Test-Json -json (ConvertTo-Json $defaultSettings -Depth 99) -schema $schema } catch { $_.Exception.Message | Should -Be "The JSON is not valid with the schema: The string value is not a match for the indicated regular expression at '/shell'" @@ -288,7 +288,7 @@ InModuleScope ReadSettings { # Allows testing of private functions $defaultSettings = GetDefaultSettings $defaultSettings.projects = "not an array" try { - Test-Json -json (ConvertTo-Json $defaultSettings) -schema $schema + Test-Json -json (ConvertTo-Json $defaultSettings -Depth 99) -schema $schema } catch { $_.Exception.Message | Should -Be "The JSON is not valid with the schema: Value is `"string`" but should be `"array`" at '/projects'" @@ -297,7 +297,7 @@ InModuleScope ReadSettings { # Allows testing of private functions # If the projects setting is an array, but contains non-string values, it should throw an error $defaultSettings.projects = @("project1", 42) try { - Test-Json -json (ConvertTo-Json $defaultSettings) -schema $schema + Test-Json -json (ConvertTo-Json $defaultSettings -Depth 99) -schema $schema } catch { $_.Exception.Message | Should -Be "The JSON is not valid with the schema: Value is `"integer`" but should be `"string`" at '/projects/1'" @@ -305,9 +305,9 @@ InModuleScope ReadSettings { # Allows testing of private functions # If the projects setting is an array of strings, it should pass the schema validation $defaultSettings.projects = @("project1") - Test-Json -json (ConvertTo-Json $defaultSettings) -schema $schema | Should -Be $true + Test-Json -json (ConvertTo-Json $defaultSettings -Depth 99) -schema $schema | Should -Be $true $defaultSettings.projects = @("project1", "project2") - Test-Json -json (ConvertTo-Json $defaultSettings) -schema $schema | Should -Be $true + Test-Json -json (ConvertTo-Json $defaultSettings -Depth 99) -schema $schema | Should -Be $true } It 'overwriteSettings property resets settings from destination object (simple types)' { From a1cba253e034f578c28eaad50a3e991b07383794 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 17 Oct 2025 16:17:12 +0200 Subject: [PATCH 03/92] Remove variable --- Tests/CheckForUpdates.Action.Test.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 6768aea4c..e5a4706d3 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -277,8 +277,7 @@ Describe "ResolveFilePaths" { $rootFolder = $PSScriptRoot - $sourcePath = "sourcePath" - $sourceFolder = Join-Path $rootFolder $sourcePath + $sourceFolder = Join-Path $rootFolder "sourcePath" if (-not (Test-Path $sourceFolder)) { New-Item -Path $sourceFolder -ItemType Directory | Out-Null } @@ -314,6 +313,7 @@ Describe "ResolveFilePaths" { @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = 'newFolder'; "destinationName" = "" } @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = 'newFolder'; "destinationName" = "" } ) + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder $fullFilePaths | Should -Not -BeNullOrEmpty From 88a4a6f50aad061b1bfa6fc07a30680437207078 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 17 Oct 2025 16:19:21 +0200 Subject: [PATCH 04/92] Fix pre-commit issues --- Actions/.Modules/settings.schema.json | 2 +- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index b6c3f2c5b..59b69ad53 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -791,4 +791,4 @@ } } } -} \ No newline at end of file +} diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index f554d36ac..ace840d43 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -700,4 +700,3 @@ function GetFilesToUpdate { return $filesToUpdate, $filesToRemove } - From 1f04fb95185d38939d541ec0408f14272b19c5de Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 20 Oct 2025 13:00:42 +0200 Subject: [PATCH 05/92] Fix tests failing in PS5 --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 2 +- Tests/CheckForUpdates.Action.Test.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index ace840d43..1819a0d50 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -648,7 +648,7 @@ function ResolveFilePaths { } } # Remove duplicates (when sourceFullPath and destinationFullPath are the same) - $fullFilePaths = $fullFilePaths | Sort-Object sourceFullPath, destinationFullPath -Unique + $fullFilePaths = $fullFilePaths | Sort-Object { $_.sourceFullPath}, { $_.destinationFullPath} -Unique return $fullFilePaths } diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index e5a4706d3..7bf1a217c 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -404,8 +404,8 @@ Describe "ResolveFilePaths" { $destinationPath = "destinationPath" $destinationFolder = Join-Path $PSScriptRoot $destinationPath $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; } - @{ "sourcePath" = "folder"; "filter" = "*"; "destinationPath" = "newFolder"; "destinationName" = ""; } + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "text"; } + @{ "sourcePath" = "folder"; "filter" = "*"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "unknown"; } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder From c361aaa8f7ce64f49613a912ecc05558738dff5c Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 22 Oct 2025 08:39:32 +0200 Subject: [PATCH 06/92] Remove mandatory parameter $files --- .../CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 1819a0d50..ddcf7a0d6 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -567,11 +567,14 @@ function ResolveFilePaths { [string] $sourceFolder, [string] $originalSourceFolder = $null, [string] $destinationFolder = $null, - [Parameter(Mandatory=$true)] - [array] $files, + [array] $files = @(), [string[]] $projects = @() ) + if(-not $files) { + return @() + } + $fullFilePaths = @() foreach($file in $files) { if(-not $file.sourcePath) { From 1c308e73ac23739916470c77e83190a10d83c021 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 22 Oct 2025 10:04:01 +0200 Subject: [PATCH 07/92] Resolve downloaded template folders --- .../CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 7 +++---- Actions/CheckForUpdates/CheckForUpdates.ps1 | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index ddcf7a0d6..2f5021448 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -61,6 +61,7 @@ function DownloadTemplateRepository { InvokeWebRequest -Headers $headers -Uri $archiveUrl -OutFile "$tempName.zip" Expand-7zipArchive -Path "$tempName.zip" -DestinationPath $tempName Remove-Item -Path "$tempName.zip" + return $tempName } @@ -457,11 +458,9 @@ function GetSrcFolder { [string] $repoType, [string] $templateUrl, [string] $templateFolder, - [string] $srcPath + [string] $srcPath = '' ) - Write-Host $templateUrl - Write-Host $templateFolder - Write-Host $srcPath + if (!$templateUrl) { return '' } diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 2f6b16284..c7c707be1 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -68,6 +68,7 @@ if ($repoSettings.templateUrl -ne $templateUrl -or $templateSha -eq '') { $originalTemplateFolder = $null $templateFolder = DownloadTemplateRepository -token $token -templateUrl $templateUrl -templateSha ([ref]$templateSha) -downloadLatest $downloadLatest +$templateFolder = GetSrcFolder -repoType $repoSettings.type -templateUrl $templateUrl -templateFolder $templateFolder Write-Host "Template Folder: $templateFolder" $templateBranch = $templateUrl.Split('@')[1] @@ -99,6 +100,8 @@ if (-not $isDirectALGo) { # Download the "original" template repository - use downloadLatest if no TemplateSha is specified in the custom template repository $originalTemplateFolder = DownloadTemplateRepository -token $token -templateUrl $originalTemplateUrl -templateSha ([ref]$originalTemplateSha) -downloadLatest ($originalTemplateSha -eq '') + $originalTemplateFolder = GetSrcFolder -repoType $repoSettings.type -templateUrl $originalTemplateUrl -templateFolder $originalTemplateFolder + Write-Host "Original Template Folder: $originalTemplateFolder" # Set TemplateBranch and TemplateOwner From abf5ae3a7f6dfee4442976a74805ae28cc1750b9 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 22 Oct 2025 11:43:46 +0200 Subject: [PATCH 08/92] Fix variable name --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index c7c707be1..ff5894544 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -174,7 +174,7 @@ foreach($fileToUpdate in $filesToUpdate) { # Replace static placeholders $srcContent = $srcContent.Replace('{TEMPLATEURL}', $templateUrl) - if ($isFileDirectALGo) { + if ($isDirectALGo) { # If we are using direct AL-Go repo, we need to change the owner to the templateOwner, the repo names to AL-Go and AL-Go/Actions and the branch to templateBranch ReplaceOwnerRepoAndBranch -srcContent ([ref]$srcContent) -templateOwner $templateOwner -templateBranch $templateBranch } From 76dec4dab059ba2c892933d16c1133ef21923364 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 22 Oct 2025 14:37:53 +0200 Subject: [PATCH 09/92] Fix variable names --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index ff5894544..cec3eee39 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -187,24 +187,24 @@ foreach($fileToUpdate in $filesToUpdate) { if ($dstFileExists) { if ($type -eq 'workflow') { - Write-Host "Apply customizations from current repository, file: $dstFile" - [Yaml]::ApplyFinalCustomizations([ref] $srcContent, $dstFile) + Write-Host "Apply customizations from current repository, file: $dstPath" + [Yaml]::ApplyFinalCustomizations([ref] $srcContent, $dstPath) } # file exists, compare and add to $updateFiles if different - $dstContent = Get-ContentLF -Path $dstFile + $dstContent = Get-ContentLF -Path $dstPath if ($dstContent -cne $srcContent) { - Write-Host "Updated $type ($(Join-Path $dstPath $filename)) available" - $updateFiles += @{ "DstFile" = Join-Path $dstPath $filename; "content" = $srcContent } + Write-Host "Updated $type ($dstPath) available" + $updateFiles += @{ "DstFile" = $dstPath; "content" = $srcContent } } else { - Write-Host "No changes in $type ($(Join-Path $dstPath $filename))" + Write-Host "No changes in $type ($dstPath)" } } else { # new file, add to $updateFiles - Write-Host "New $type ($(Join-Path $dstPath $filename)) available" - $updateFiles += @{ "DstFile" = Join-Path $dstPath $filename; "content" = $srcContent } + Write-Host "New $type ($dstPath) available" + $updateFiles += @{ "DstFile" = $dstPath; "content" = $srcContent } } } From 43f5f34c987d90dcb2135f7f2967f808946c962c Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 22 Oct 2025 16:09:56 +0200 Subject: [PATCH 10/92] Enrich e2e test for custom templates --- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index 9b7594653..f883a8e14 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -55,7 +55,7 @@ $template = "https://github.com/$pteTemplate" # Login SetTokenAndRepository -github:$github -githubOwner $githubOwner -appId $e2eAppId -appKey $e2eAppKey -repository $repository -# Create tempolate repository +# Create template repository CreateAlGoRepository ` -github:$github ` -linux:$linux ` @@ -143,6 +143,34 @@ $customJobs = @( $cicdYaml.AddCustomJobsToYaml($customJobs, [CustomizationOrigin]::FinalRepository) # In the context of the template repository, these custom jobs are treated as final customizations $cicdYaml.Save($cicdWorkflow) +# Add a custom workflow file in the template repository (to be copied to the final repository, as workflow files are always propagated) +$customWorkflowFile = Join-Path $templateRepoPath '.github/workflows/CustomWorkflow.yaml' +$customWorkflowContent = @" +name: Custom Workflow + +on: + push: + branches: + - main + +jobs: + CustomJob: + runs-on: [ windows-latest ] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Run Custom Script + run: | + Write-Host 'Custom Workflow was triggered!' +"@ +Set-Content -Path $customWorkflowFile -Value $customWorkflowContent + +# Add another custom file in the template repository (to be ignored unless specifically added via the settings) +$customFileName = 'CustomTemplateFile.txt' +$customFile = Join-Path $templateRepoPath $customFileName +$customFileContent = "This is a custom file in the template repository." +Set-Content -Path $customFile -Value $customFileContent + # Push CommitAndPush -commitMessage 'Add template customizations' @@ -227,6 +255,29 @@ Pull (Join-Path (Get-Location) $CustomTemplateRepoSettingsFile) | Should -Exist (Join-Path (Get-Location) $CustomTemplateProjectSettingsFile) | Should -Exist +# Check that custom workflow file is present +(Join-Path (Get-Location) $customWorkflowFile) | Should -Exist +(Get-Content -Path (Join-Path (Get-Location) $customWorkflowFile)) | Should -Be $customWorkflowContent + +# Check that custom file is NOT present +(Join-Path (Get-Location) $customFile) | Should -Not -Exist # Custom file should not be copied by default + +# Add custom file to be copied via settings +$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "updateALGoFiles" = @{ "filesToUpdate" = @( @{ "sourcePath" = $customFile; "destinationPath" = '.' } ) } } + +# Push +CommitAndPush -commitMessage 'Add custom file to be updated when updating AL-Go system files [skip ci]' + +# Update AL-Go System Files to uptake custom file +RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $templateRepository -ghTokenWorkflow $algoauthapp -repository $repository -branch $branch | Out-Null + +# Pull changes +Pull + +# Check that custom file is now present +(Join-Path (Get-Location) $customFile) | Should -Exist +(Get-Content -Path (Join-Path (Get-Location) $customFile)) | Should -Be $customFileContent + # Run CICD $run = RunCICD -repository $repository -branch $branch -wait From c2af10e2723894210579bd8a98be1bd30ef4c105 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 23 Oct 2025 11:24:06 +0200 Subject: [PATCH 11/92] Move messages to debug --- .../CheckForUpdates.HelperFunctions.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 2f5021448..15fb04d43 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -667,18 +667,18 @@ function GetFilesToUpdate { $filesToUpdate = $settings.updateALGoFiles.filesToUpdate $filesToUpdate = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToUpdate -projects $projects - Write-Host "Files to update:" - $filesToUpdate | ForEach-Object { Write-Host " $(ConvertTo-Json $_)"} + OutputDebug "Files to update:" + $filesToUpdate | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)"} $filesToIgnore = $settings.updateALGoFiles.filesToIgnore $filesToIgnore = ResolveFilePaths -sourceFolder $baseFolder -files $filesToIgnore -projects $projects - Write-Host "Files to ignore:" - $filesToIgnore | ForEach-Object { Write-Host " $(ConvertTo-Json $_)" } + OutputDebug "Files to ignore:" + $filesToIgnore | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)" } $filesToRemove = $settings.updateALGoFiles.filesToRemove $filesToRemove = ResolveFilePaths -sourceFolder $baseFolder -files $filesToRemove -projects $projects - Write-Host "Files to remove:" - $filesToRemove | ForEach-Object { Write-Host " $(ConvertTo-Json $_)" } + OutputDebug "Files to remove:" + $filesToRemove | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)" } # Exclude files to ignore from files to update $filesToUpdate = $filesToUpdate | Where-Object { From 1d4cb2f51cd21a6a4cedc3cfd39ac4bb74876618 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 24 Oct 2025 13:19:35 +0200 Subject: [PATCH 12/92] Enhance tests --- Actions/.Modules/ReadSettings.psm1 | 35 +- .../CheckForUpdates.HelperFunctions.ps1 | 48 ++- Tests/CheckForUpdates.Action.Test.ps1 | 313 ++++++++++++++++++ 3 files changed, 366 insertions(+), 30 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 90149048e..913469abd 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -1,10 +1,17 @@ Import-Module (Join-Path -Path $PSScriptRoot "DebugLogHelper.psm1") $ALGoFolderName = '.AL-Go' -$ALGoSettingsFile = Join-Path '.AL-Go' 'settings.json' -$RepoSettingsFile = Join-Path '.github' 'AL-Go-Settings.json' -$CustomTemplateRepoSettingsFile = Join-Path '.github' 'AL-Go-TemplateRepoSettings.doNotEdit.json' -$CustomTemplateProjectSettingsFile = Join-Path '.github' 'AL-Go-TemplateProjectSettings.doNotEdit.json' +$ALFoSettingsFileName = 'settings.json' +$ALGoSettingsFile = Join-Path '.AL-Go' $ALFoSettingsFileName + +$RepoSettingsFileName = 'AL-Go-Settings.json' +$RepoSettingsFile = Join-Path '.github' $RepoSettingsFileName + +$CustomTemplateRepoSettingsFileName = 'AL-Go-TemplateRepoSettings.doNotEdit.json' +$CustomTemplateRepoSettingsFile = Join-Path '.github' $CustomTemplateRepoSettingsFileName + +$CustomTemplateProjectSettingsFileName = 'AL-Go-TemplateProjectSettings.doNotEdit.json' +$CustomTemplateProjectSettingsFile = Join-Path '.github' $CustomTemplateProjectSettingsFileName function MergeCustomObjectIntoOrderedDictionary { Param( @@ -243,16 +250,16 @@ function GetDefaultSettings "reportSuppressedDiagnostics" = $false "updateALGoFiles" = [ordered]@{ "filesToUpdate" = @( - [ordered]@{ 'destinationPath' = (Join-Path '.github' 'workflows'); 'destinationName' = ''; 'sourcePath' = (Join-Path '.github' 'workflows'); 'filter' = '*'; 'type' = 'workflow'; 'perProject' = $false } - [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = '*.copy.md'; 'type' = 'releasenotes'; 'perProject' = $false } - [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $false } - [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings'; 'perProject' = $false } - [ordered]@{ 'destinationPath' = '.github'; 'destinationName' = ''; 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings'; 'perProject' = $false } - [ordered]@{ 'destinationPath' = ([system.IO.Path]::GetDirectoryName($CustomTemplateRepoSettingsFile)); 'destinationName' = ([system.IO.Path]::GetFileName($CustomTemplateRepoSettingsFile)); 'sourcePath' = ([system.IO.Path]::GetDirectoryName($RepoSettingsFile)); 'filter' = ([system.IO.Path]::GetFileName($RepoSettingsFile)); 'type' = 'template repo settings'; 'perProject' = $false } - [ordered]@{ 'destinationPath' = ([system.IO.Path]::GetDirectoryName($CustomTemplateProjectSettingsFile)); 'destinationName' = ([system.IO.Path]::GetFileName($CustomTemplateProjectSettingsFile)); 'sourcePath' = ([system.IO.Path]::GetDirectoryName($ALGoSettingsFile)); 'filter' = ([system.IO.Path]::GetFileName($ALGoSettingsFile)); 'type' = 'template project settings'; 'perProject' = $false } - - [ordered]@{ 'destinationPath' = '.AL-Go'; 'destinationName' = ''; 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, - [ordered]@{ 'destinationPath' = '.AL-Go'; 'destinationName' = ''; 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } + [ordered]@{ 'sourcePath' = (Join-Path '.github' 'workflows'); 'filter' = '*'; 'type' = 'workflow' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md'; 'type' = 'releasenotes' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } + [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'template repo settings' } + [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALFoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'template project settings' } + + [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, + [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } ) "filesToIgnore" = @() "filesToRemove" = @() diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 15fb04d43..4e81c47d4 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -576,16 +576,34 @@ function ResolveFilePaths { $fullFilePaths = @() foreach($file in $files) { - if(-not $file.sourcePath) { - throw "sourcePath is required for action $action" + if($file.Keys -notcontains 'sourcePath') { + $file.sourcePath = '' # Default to current folder } + + if($file.Keys -notcontains 'filter') { + $file.filter = '' # Default to all files + } + + if($file.Keys -notcontains 'type') { + $file.type = $null # Default to null type + } + + if($file.Keys -notcontains 'destinationPath') { + # If destinationPath is not specified, use the sourcePath, so that the file structure is preserved + $file.destinationPath = $file.sourcePath + } + + if($file.Keys -notcontains 'perProject') { + $file.perProject = $false # Default to false + } + # All files are relative to the template folder Write-Host "Resolving files for sourcePath '$($file.sourcePath)' and filter '$($file.filter)'" $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourcePath) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) - Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to template folder '$sourceFolder')" + Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder')" if(-not $sourceFiles) { - Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to template folder '$sourceFolder')" + Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder')" continue } @@ -608,20 +626,18 @@ function ResolveFilePaths { } } - if($file.Keys -contains 'type') { - $fullFilePath.type = $file.type # propagate the type if it exists - } + # Set type + $fullFilePath.type = $file.type if(-not $destinationFolder) { - # Destination folder is not specified, so we only return the source full path + # Destination folder is not specified, no need to calculate destinationFullPath as it will not be used $fullFilePaths += $fullFilePath continue } - $dstFileName = $file.destinationName - if(-not $dstFileName) { - # If destinationName is not specified, use the source file name - $dstFileName = Split-Path -Path $srcFile -Leaf + $destinationName = $(Split-Path -Path $srcFile -Leaf) + if($file.Keys -contains 'destinationName' -and ($file.destinationName)) { + $destinationName = $file.destinationName } if($file.Keys -contains 'perProject' -and $file.perProject -eq $true) { @@ -633,7 +649,7 @@ function ResolveFilePaths { $projectFile.destinationFullPath = Join-Path $destinationFolder $project $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $file.destinationPath - $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $dstFileName + $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $destinationName $fullFilePaths += $projectFile } @@ -643,7 +659,7 @@ function ResolveFilePaths { # Destination full path is the destination base folder + destinationPath + destinationName $fullFilePath.destinationFullPath = Join-Path $destinationFolder $file.destinationPath - $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $dstFileName + $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $destinationName $fullFilePaths += $fullFilePath } @@ -671,7 +687,7 @@ function GetFilesToUpdate { $filesToUpdate | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)"} $filesToIgnore = $settings.updateALGoFiles.filesToIgnore - $filesToIgnore = ResolveFilePaths -sourceFolder $baseFolder -files $filesToIgnore -projects $projects + $filesToIgnore = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -files $filesToIgnore -projects $projects OutputDebug "Files to ignore:" $filesToIgnore | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)" } @@ -700,5 +716,5 @@ function GetFilesToUpdate { return $include } - return $filesToUpdate, $filesToRemove + return @($filesToUpdate), @($filesToRemove) } diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 7bf1a217c..ca5934524 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -421,4 +421,317 @@ Describe "ResolveFilePaths" { $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") } + } + +Describe "ReplaceOwnerRepoAndBranch" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + } + + It "Replaces owner, repo, and branch in workflow content" { + $srcContent = [ref]@" +jobs: + build: + uses: microsoft/AL-Go-Actions@main +"@ + $templateOwner = "contoso" + $templateBranch = "dev" + ReplaceOwnerRepoAndBranch -srcContent $srcContent -templateOwner $templateOwner -templateBranch $templateBranch + $srcContent.Value | Should -Be @" +jobs: + build: + uses: contoso/AL-Go/Actions@dev +"@ + } +} + +Describe "IsDirectALGo" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + } + It "Returns true for direct AL-Go repo URL" { + IsDirectALGo -templateUrl "https://github.com/contoso/AL-Go@main" | Should -Be True + } + It "Returns false for non-direct AL-Go repo URL" { + IsDirectALGo -templateUrl "https://github.com/contoso/OtherRepo@main" | Should -Be False + } +} + +Describe "GetFilesToUpdate" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + + # Create template folder with test files + $templateFolder = Join-Path $PSScriptRoot "template" + New-Item -ItemType Directory -Path $templateFolder -Force | Out-Null + + New-Item -ItemType Directory -Path (Join-Path $templateFolder "subfolder") -Force | Out-Null + + $testPSFile = Join-Path $templateFolder "test.ps1" + Set-Content -Path $testPSFile -Value "# test ps file" + + $testTxtFile = Join-Path $templateFolder "test.txt" + Set-Content -Path $testTxtFile -Value "test txt file" + + $testTxtFile2 = Join-Path $templateFolder "test2.txt" + Set-Content -Path $testTxtFile2 -Value "test txt file 2" + + $testSubfolderFile = Join-Path $templateFolder "subfolder/testsub.txt" + Set-Content -Path $testSubfolderFile -Value "test subfolder txt file" + + $testSubfolderFile2 = Join-Path $templateFolder "subfolder/testsub2.txt" + Set-Content -Path $testSubfolderFile2 -Value "test subfolder txt file 2" + + # Display the created files structure for template folder + # . + # ├── test.ps1 + # ├── test.txt + # └── test2.txt + # └── subfolder + # └── testsub.txt + } + + AfterAll { + if (Test-Path $templateFolder) { + Remove-Item -Path $templateFolder -Recurse -Force + } + } + + It "Returns the correct files to update with filters" { + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.ps1" }) + filesToIgnore = @() + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt" }) + filesToIgnore = @() + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 2 + $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + } + + It 'Returns the correct files when there are files to ignore' { + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.*" }) + filesToIgnore = @(@{ filter = "test.txt" }) + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 2 + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt" }) + filesToIgnore = @(@{ filter = "*" }) + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + # No files to update + $filesToUpdate | Should -BeNullOrEmpty + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + } + + It 'Returns the correct files within subfolders' { + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ sourcePath = 'subfolder'; filter = "*.txt" }) + filesToIgnore = @() + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 2 + $filesToUpdate[0].sourceFullPath | Should -Be $testSubfolderFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'subfolder/testsub.txt') + $filesToUpdate[1].sourceFullPath | Should -Be $testSubfolderFile2 + $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'subfolder/testsub2.txt') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ sourcePath = 'subfolder'; filter = "*.txt" }) + filesToIgnore = @(@{ sourcePath = 'subfolder'; filter = "testsub2.txt" }) + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testSubfolderFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'subfolder/testsub.txt') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + } + + It 'Returns the correct files with destinationPath' { + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) + filesToIgnore = @() + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 2 + $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test2.txt') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) + filesToIgnore = @(@{ filter = "test2.txt" }) + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + } + + It 'Returns the correct files with destinationName' { + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) + filesToIgnore = @() + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "test.ps1"; destinationPath = 'dstPath'; destinationName = "renamed.txt" }) + filesToIgnore = @() + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') + } + + It 'Return the correct files with types' { + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.ps1"; type = "script" }) + filesToIgnore = @() + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToUpdate[0].type | Should -Be "script" + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + + $settings = @{ + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt"; type = "text" }) + filesToIgnore = @(@{ filter = "test.txt" }) + filesToRemove = @() + } + } + + $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile2 + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToUpdate[0].type | Should -Be "text" + + # No files to remove + $filesToRemove | Should -BeNullOrEmpty + } +} \ No newline at end of file From d81a53cd37b3927ad080cf7c8e2cf562190ece64 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 24 Oct 2025 13:20:51 +0200 Subject: [PATCH 13/92] Fix error with missing sourceFullPath --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index cec3eee39..fd64536cc 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -209,7 +209,7 @@ foreach($fileToUpdate in $filesToUpdate) { } # Remove files that are in $filesToRemove and exist in the repository -$removeFiles = $filesToRemove | ForEach-Object { $_.sourceFullPath } | Where-Object { Test-Path -Path $_ -PathType Leaf } +$removeFiles = $filesToRemove | Where-Object { $_ -and (Test-Path -Path $_ -PathType Leaf) } | ForEach-Object { $_.sourceFullPath } if ($update -ne 'Y') { # $update not set, just issue a warning in the CI/CD workflow that updates are available From 550b11cb14ff14cc36cb70d09960309a994692e1 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 24 Oct 2025 13:28:11 +0200 Subject: [PATCH 14/92] pre-commit fixes --- Tests/CheckForUpdates.Action.Test.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index ca5934524..84c63ca1b 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -734,4 +734,4 @@ Describe "GetFilesToUpdate" { # No files to remove $filesToRemove | Should -BeNullOrEmpty } -} \ No newline at end of file +} From e7aa5652b22d8258a07fd6a7a7277a3d7414514a Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 24 Oct 2025 17:10:50 +0200 Subject: [PATCH 15/92] Log improvements --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index fd64536cc..2424fddbc 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -194,11 +194,11 @@ foreach($fileToUpdate in $filesToUpdate) { # file exists, compare and add to $updateFiles if different $dstContent = Get-ContentLF -Path $dstPath if ($dstContent -cne $srcContent) { - Write-Host "Updated $type ($dstPath) available" + Write-Host "Available updates for $type ($dstPath)" $updateFiles += @{ "DstFile" = $dstPath; "content" = $srcContent } } else { - Write-Host "No changes in $type ($dstPath)" + Write-Host "No updates for $type ($dstPath)" } } else { From 15700ffa7141859189b6e0904e8ed6136035fcd0 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 24 Oct 2025 17:24:20 +0200 Subject: [PATCH 16/92] Do not fallback to original template for custom template files --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 4e81c47d4..697284583 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -585,7 +585,7 @@ function ResolveFilePaths { } if($file.Keys -notcontains 'type') { - $file.type = $null # Default to null type + $file.type = '' # Default to empty type } if($file.Keys -notcontains 'destinationPath') { @@ -616,7 +616,7 @@ function ResolveFilePaths { } # Try to find the same files in the original template folder if it is specified - if ($originalSourceFolder) { + if ($originalSourceFolder -and $file.type -notcontains 'template') { Push-Location $sourceFolder $relativePath = Resolve-Path -Path $srcFile -Relative # resolve the path relative to the current location (template folder) Pop-Location From ccf2fc825008901df49952d39560924c364cefed Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 28 Oct 2025 12:17:30 +0100 Subject: [PATCH 17/92] Fix issue when downloading original template --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 2424fddbc..2a74c7424 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -77,7 +77,7 @@ $templateInfo = "$templateOwner/$($templateUrl.Split('/')[4])" $isDirectALGo = IsDirectALGo -templateUrl $templateUrl if (-not $isDirectALGo) { - $templateRepoSettingsFile = Join-Path $templateFolder "*/$RepoSettingsFile" + $templateRepoSettingsFile = Join-Path $templateFolder $RepoSettingsFile if (Test-Path -Path $templateRepoSettingsFile -PathType Leaf) { $templateRepoSettings = Get-Content $templateRepoSettingsFile -Encoding UTF8 | ConvertFrom-Json | ConvertTo-HashTable -Recurse if ($templateRepoSettings.Keys -contains "templateUrl" -and $templateRepoSettings.templateUrl -ne $templateUrl) { From 5cb2f3a9e84a8525f1211cb711f1ae5f4cadf870 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 28 Oct 2025 12:51:47 +0100 Subject: [PATCH 18/92] Fix contains and add a test --- .../CheckForUpdates.HelperFunctions.ps1 | 7 +--- Tests/CheckForUpdates.Action.Test.ps1 | 39 ++++++++++++++++--- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 697284583..722de377e 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -611,12 +611,12 @@ function ResolveFilePaths { $fullFilePath = @{ 'sourceFullPath' = $srcFile 'originalSourceFullPath' = $null - 'type' = $null + 'type' = $file.type 'destinationFullPath' = $null } # Try to find the same files in the original template folder if it is specified - if ($originalSourceFolder -and $file.type -notcontains 'template') { + if ($originalSourceFolder -and (-not $file.type.Contains('template'))) { Push-Location $sourceFolder $relativePath = Resolve-Path -Path $srcFile -Relative # resolve the path relative to the current location (template folder) Pop-Location @@ -626,9 +626,6 @@ function ResolveFilePaths { } } - # Set type - $fullFilePath.type = $file.type - if(-not $destinationFolder) { # Destination folder is not specified, no need to calculate destinationFullPath as it will not be used $fullFilePaths += $fullFilePath diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 84c63ca1b..f112155b9 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -320,13 +320,13 @@ Describe "ResolveFilePaths" { $fullFilePaths.Count | Should -Be 3 $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") - $fullFilePaths[0].type | Should -Be $null + $fullFilePaths[0].type | Should -Be "" $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - $fullFilePaths[1].type | Should -Be $null + $fullFilePaths[1].type | Should -Be "" $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") - $fullFilePaths[2].type | Should -Be $null + $fullFilePaths[2].type | Should -Be "" } It 'ResolveFilePaths with specific destination names' { @@ -343,10 +343,10 @@ Describe "ResolveFilePaths" { $fullFilePaths.Count | Should -Be 2 $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile1.txt") - $fullFilePaths[0].type | Should -Be $null + $fullFilePaths[0].type | Should -Be "" $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile2.log") - $fullFilePaths[1].type | Should -Be $null + $fullFilePaths[1].type | Should -Be "" } It 'ResolveFilePaths with type' { @@ -422,6 +422,35 @@ Describe "ResolveFilePaths" { $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") } + It 'ResolveFilePaths populates the originalSourceFullPath property only if the type does not contain "template"' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "text template"; } + @{ "sourcePath" = "folder"; "filter" = "*.log"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "log"; } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + + # First file has type containing "template", so originalSourceFullPath should be $null + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].originalSourceFullPath | Should -Be $null + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + + + # Second file has type not containing "template", so originalSourceFullPath should be populated + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + $fullFilePaths[1].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") + + # Third file has is not present in original source folder, so originalSourceFullPath should be $null + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[2].originalSourceFullPath | Should -Be $null + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + } } Describe "ReplaceOwnerRepoAndBranch" { From 04acd08d2f4bdcb8caafd829efd0ee032b64076d Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 28 Oct 2025 17:16:56 +0100 Subject: [PATCH 19/92] Use relative paths --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 27 +++++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 2a74c7424..e8323a002 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -185,31 +185,42 @@ foreach($fileToUpdate in $filesToUpdate) { [Yaml]::ApplyTemplateCustomizations([ref] $srcContent, $srcPath) } + # Get the relative path for the dstPath from the base folder + Push-Location -Path $baseFolder + $relativeDstPath = Resolve-Path -Path $dstPath -Relative + Pop-Location + if ($dstFileExists) { if ($type -eq 'workflow') { - Write-Host "Apply customizations from current repository, file: $dstPath" + Write-Host "Apply customizations from current repository, file: $relativeDstPath" [Yaml]::ApplyFinalCustomizations([ref] $srcContent, $dstPath) } # file exists, compare and add to $updateFiles if different $dstContent = Get-ContentLF -Path $dstPath if ($dstContent -cne $srcContent) { - Write-Host "Available updates for $type ($dstPath)" - $updateFiles += @{ "DstFile" = $dstPath; "content" = $srcContent } + Write-Host "Available updates for $type ($relativeDstPath)" + $updateFiles += @{ "DstFile" = $relativeDstPath; "content" = $srcContent } } else { - Write-Host "No updates for $type ($dstPath)" + Write-Host "No updates for $type ($relativeDstPath)" } } else { # new file, add to $updateFiles - Write-Host "New $type ($dstPath) available" - $updateFiles += @{ "DstFile" = $dstPath; "content" = $srcContent } + Write-Host "New $type ($relativeDstPath) available" + $updateFiles += @{ "DstFile" = $relativeDstPath; "content" = $srcContent } } } +Push-Location -Path $baseFolder # Remove files that are in $filesToRemove and exist in the repository -$removeFiles = $filesToRemove | Where-Object { $_ -and (Test-Path -Path $_ -PathType Leaf) } | ForEach-Object { $_.sourceFullPath } +$removeFiles = $filesToRemove | Where-Object { $_ -and (Test-Path -Path $_ -PathType Leaf) } | ForEach-Object { + $relativePath = Resolve-Path -Path $_ -Relative + Write-Host "File marked for removal: $relativePath" + $relativePath +} +Pop-Location if ($update -ne 'Y') { # $update not set, just issue a warning in the CI/CD workflow that updates are available @@ -246,7 +257,7 @@ else { exit } - # If $directCommit, then changes are made directly to the default branch + # Clone into a new folder, create a new branch (if not direct commit), and set the location to the new folder $serverUrl, $branch = CloneIntoNewFolder -actor $actor -token $repoWriteToken -updateBranch $updateBranch -DirectCommit $directCommit -newBranchPrefix 'update-al-go-system-files' invoke-git status From 423bae147e11ce63551bf20f37868306758aefd0 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 28 Oct 2025 17:19:25 +0100 Subject: [PATCH 20/92] Do not use Resolve-Path as the file might not exist --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index e8323a002..e664cba4f 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -186,9 +186,7 @@ foreach($fileToUpdate in $filesToUpdate) { } # Get the relative path for the dstPath from the base folder - Push-Location -Path $baseFolder - $relativeDstPath = Resolve-Path -Path $dstPath -Relative - Pop-Location + $relativeDstPath = $dstPath.Substring($baseFolder.Length + 1) if ($dstFileExists) { if ($type -eq 'workflow') { From 11d13db8223dc92344ef5a7635929ef11e222385 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 10:42:43 +0100 Subject: [PATCH 21/92] Fix typo --- Actions/.Modules/ReadSettings.psm1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 1d952dad9..2cfbdbcc5 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -1,8 +1,8 @@ Import-Module (Join-Path -Path $PSScriptRoot "DebugLogHelper.psm1") $ALGoFolderName = '.AL-Go' -$ALFoSettingsFileName = 'settings.json' -$ALGoSettingsFile = Join-Path '.AL-Go' $ALFoSettingsFileName +$ALGoSettingsFileName = 'settings.json' +$ALGoSettingsFile = Join-Path '.AL-Go' $ALGoSettingsFileName $RepoSettingsFileName = 'AL-Go-Settings.json' $RepoSettingsFile = Join-Path '.github' $RepoSettingsFileName @@ -256,7 +256,7 @@ function GetDefaultSettings [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'template repo settings' } - [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALFoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'template project settings' } + [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'template project settings' } [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } From af1e0b8d9041950f7dd149fba8127f5f462138b2 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 12:15:59 +0100 Subject: [PATCH 22/92] Recalculate IsDirectALGo if custom template --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index e664cba4f..936b35e93 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -108,6 +108,11 @@ if (-not $isDirectALGo) { # Keep TemplateUrl and TemplateSha pointing to the custom template repository $templateBranch = $originalTemplateUrl.Split('@')[1] $templateOwner = $originalTemplateUrl.Split('/')[3] + + $isDirectALGo = IsDirectALGo -templateUrl $originalTemplateUrl + if ($isDirectALGo) { + Trace-Information -Message "Original template repository is direct AL-Go" + } } } } From fa4a3105ad4a2a1c182b9ba400ff55ff03ce3172 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 12:40:37 +0100 Subject: [PATCH 23/92] Improve code comment --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 936b35e93..8bde847f9 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -190,7 +190,7 @@ foreach($fileToUpdate in $filesToUpdate) { [Yaml]::ApplyTemplateCustomizations([ref] $srcContent, $srcPath) } - # Get the relative path for the dstPath from the base folder + # Get the relative path for the dstPath from the base folder. Don't use Resolve-Path as it will fail if the destination file doesn't exist $relativeDstPath = $dstPath.Substring($baseFolder.Length + 1) if ($dstFileExists) { From 7715271bf6f7a1818ebea3d2624e9178bf61b2bf Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 12:53:58 +0100 Subject: [PATCH 24/92] Add check for files from original template --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 722de377e..ce1eaa18c 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -607,6 +607,12 @@ function ResolveFilePaths { continue } + # If originalSourceFolder is not specified and the file type is 'template', skip the file + # This is the case when the file is from the original template folder, but no original template folder is specified (there is no original template) + if(!$originalSourceFolder -and $file.type.Contains('template')) { + continue; + } + foreach($srcFile in $sourceFiles) { $fullFilePath = @{ 'sourceFullPath' = $srcFile From 71fdce4136c04f1cda67ddc85dc7616da867d2fc Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 13:27:04 +0100 Subject: [PATCH 25/92] Fix types --- Actions/.Modules/ReadSettings.psm1 | 4 ++-- .../CheckForUpdates.HelperFunctions.ps1 | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 2cfbdbcc5..7ff596f9d 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -255,8 +255,8 @@ function GetDefaultSettings [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } - [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'template repo settings' } - [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'template project settings' } + [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'original template' } + [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'original template' } [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index ce1eaa18c..418f9d0c5 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -597,6 +597,12 @@ function ResolveFilePaths { $file.perProject = $false # Default to false } + # If originalSourceFolder is not specified and the file type is 'template', skip the file + # This is the case when the file is from the original template folder, but no original template folder is specified (there is no original template) + if(!$originalSourceFolder -and $file.type -eq 'original template') { + continue; + } + # All files are relative to the template folder Write-Host "Resolving files for sourcePath '$($file.sourcePath)' and filter '$($file.filter)'" $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourcePath) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) @@ -607,12 +613,6 @@ function ResolveFilePaths { continue } - # If originalSourceFolder is not specified and the file type is 'template', skip the file - # This is the case when the file is from the original template folder, but no original template folder is specified (there is no original template) - if(!$originalSourceFolder -and $file.type.Contains('template')) { - continue; - } - foreach($srcFile in $sourceFiles) { $fullFilePath = @{ 'sourceFullPath' = $srcFile @@ -622,7 +622,7 @@ function ResolveFilePaths { } # Try to find the same files in the original template folder if it is specified - if ($originalSourceFolder -and (-not $file.type.Contains('template'))) { + if ($originalSourceFolder -and ($file.type -ne 'original template')) { Push-Location $sourceFolder $relativePath = Resolve-Path -Path $srcFile -Relative # resolve the path relative to the current location (template folder) Pop-Location From fc2f2582308740b511359ab85b4e981510ab2597 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 13:33:08 +0100 Subject: [PATCH 26/92] Renames, for more clarity --- Actions/.Modules/ReadSettings.psm1 | 4 ++-- .../CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 7ff596f9d..e948f6d6a 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -255,8 +255,8 @@ function GetDefaultSettings [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } - [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'original template' } - [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'original template' } + [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'custom template' } + [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'custom template' } [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 418f9d0c5..4cb0f0d72 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -597,9 +597,8 @@ function ResolveFilePaths { $file.perProject = $false # Default to false } - # If originalSourceFolder is not specified and the file type is 'template', skip the file - # This is the case when the file is from the original template folder, but no original template folder is specified (there is no original template) - if(!$originalSourceFolder -and $file.type -eq 'original template') { + # If originalSourceFolder is not specified, it means there is no custom template, so skip custom template files + if(!$originalSourceFolder -and $file.type -eq 'custom template') { continue; } @@ -621,8 +620,8 @@ function ResolveFilePaths { 'destinationFullPath' = $null } - # Try to find the same files in the original template folder if it is specified - if ($originalSourceFolder -and ($file.type -ne 'original template')) { + # Try to find the same files in the original template folder if it is specified. Exclude custom template files + if ($originalSourceFolder -and ($file.type -ne 'custom template')) { Push-Location $sourceFolder $relativePath = Resolve-Path -Path $srcFile -Relative # resolve the path relative to the current location (template folder) Pop-Location From 9c4e59143947d5c7a7ef95ba2ac487f887b50038 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 13:42:57 +0100 Subject: [PATCH 27/92] Add logs when skipping custom template-specific files --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 4cb0f0d72..b029400f4 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -599,6 +599,7 @@ function ResolveFilePaths { # If originalSourceFolder is not specified, it means there is no custom template, so skip custom template files if(!$originalSourceFolder -and $file.type -eq 'custom template') { + Write-Host "Skipping custom template file with sourcePath '$($file.sourcePath)' as there is no original source folder specified" continue; } From 43c7bf5874001b1742718869729d36f1874c296f Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 29 Oct 2025 14:27:12 +0100 Subject: [PATCH 28/92] Fix failing test --- Tests/CheckForUpdates.Action.Test.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index f112155b9..954872795 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -426,7 +426,7 @@ Describe "ResolveFilePaths" { $destinationPath = "destinationPath" $destinationFolder = Join-Path $PSScriptRoot $destinationPath $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "text template"; } + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "custom template"; } @{ "sourcePath" = "folder"; "filter" = "*.log"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "log"; } ) From d1851d483ed57df8ec3286a3c6251c8bf906440e Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 00:50:12 +0100 Subject: [PATCH 29/92] Encapsulate unusedALGoFiles and add tons of tests --- Actions/.Modules/ReadSettings.psm1 | 16 +- .../CheckForUpdates.HelperFunctions.ps1 | 113 +++-- Actions/CheckForUpdates/CheckForUpdates.ps1 | 24 +- Tests/CheckForUpdates.Action.Test.ps1 | 386 ++++++++++++------ 4 files changed, 363 insertions(+), 176 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index e948f6d6a..33d073b82 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -249,20 +249,8 @@ function GetDefaultSettings "shortLivedArtifactsRetentionDays" = 1 # 0 means use GitHub default "reportSuppressedDiagnostics" = $false "updateALGoFiles" = [ordered]@{ - "filesToUpdate" = @( - [ordered]@{ 'sourcePath' = (Join-Path '.github' 'workflows'); 'filter' = '*'; 'type' = 'workflow' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md'; 'type' = 'releasenotes' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } - [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'custom template' } - [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'custom template' } - - [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, - [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } - ) - "filesToIgnore" = @() - "filesToRemove" = @() + "filesToUpdate" = @() + "filesToExclude" = @() } } } diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index b029400f4..9022fc83f 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -674,7 +674,72 @@ function ResolveFilePaths { return $fullFilePaths } -# TODO Add tests for GetFilesToUpdate +function GetDefaultFilesToUpdate { + Param( + $settings, + [switch] $includeCustomTemplateFiles + ) + + $filesToUpdate = @( + [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*'; 'type' = 'workflow' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md'; 'type' = 'releasenotes' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } + + [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, + [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } + ) + + if($includeCustomTemplateFiles) { + # If there is an original template folder, we also need to include custom template files (RepoSettings and ProjectSettings) + $filesToUpdate += @( + [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'custom template' } + [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'custom template' } + ) + } + + return $filesToUpdate +} + +function GetDefaultFilesToExclude { + Param( + $settings + ) + $filesToExclude = @() + + $includeBuildPP = $settings.type -eq 'PTE' -and $settings.powerPlatformSolutionFolder -ne '' + if (!$includeBuildPP) { + # Remove PowerPlatform workflows if no PowerPlatformSolution exists + $filesToExclude += @( + [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '_BuildPowerPlatformSolution.yaml' } + [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = 'PushPowerPlatformChanges.yaml' } + [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = 'PullPowerPlatformChanges.yaml' } + ) + } + + return @($filesToExclude) +} + +<# +.SYNOPSIS + Get the list of files to update and exclude based on settings +.DESCRIPTION + This function gets the list of files to update and exclude based on the provided settings. + The unusedALGoSystemFiles setting is also applied to exclude files from the update list and add them to the exclude list. +.PARAMETER settings + The settings object containing the updateALGoFiles configuration. +.PARAMETER projects + The list of projects in the repository. +.PARAMETER baseFolder + The base folder of the repository. +.PARAMETER templateFolder + The folder where the template files are located. +.PARAMETER originalTemplateFolder + The folder where the original template files are located (if any). In case of custom templates. +.OUTPUTS + An array containing two elements: the list of files to update and the list of files to exclude. +#> function GetFilesToUpdate { Param( $settings, @@ -684,40 +749,38 @@ function GetFilesToUpdate { $originalTemplateFolder = $null ) - $filesToUpdate = $settings.updateALGoFiles.filesToUpdate + $filesToUpdate = GetDefaultFilesToUpdate -settings $settings -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) + $filesToUpdate += $settings.updateALGoFiles.filesToUpdate $filesToUpdate = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToUpdate -projects $projects - OutputDebug "Files to update:" - $filesToUpdate | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)"} - $filesToIgnore = $settings.updateALGoFiles.filesToIgnore - $filesToIgnore = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -files $filesToIgnore -projects $projects - OutputDebug "Files to ignore:" - $filesToIgnore | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)" } + $filesToExclude = GetDefaultFilesToExclude -settings $settings + $filesToExclude += $settings.updateALGoFiles.filesToExclude + $filesToExclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExclude -projects $projects - $filesToRemove = $settings.updateALGoFiles.filesToRemove - $filesToRemove = ResolveFilePaths -sourceFolder $baseFolder -files $filesToRemove -projects $projects - OutputDebug "Files to remove:" - $filesToRemove | ForEach-Object { OutputDebug " $(ConvertTo-Json $_)" } - - # Exclude files to ignore from files to update + # Exclude files from filesToUpdate that are in filesToExclude $filesToUpdate = $filesToUpdate | Where-Object { $fileToUpdate = $_ - $include = -not ($filesToIgnore | Where-Object { $_.sourceFullPath -eq $fileToUpdate.sourceFullPath }) + $include = -not ($filesToExclude | Where-Object { $_.sourceFullPath -eq $fileToUpdate.sourceFullPath }) if(-not $include) { - Write-Host "Excluding file $($fileToUpdate.sourceFullPath) from update as it is in the ignore list" + Write-Host "Excluding file $($fileToUpdate.sourceFullPath) from update as it is in the exclude list" } return $include } - # Exclude files to remove from files to update - $filesToUpdate = $filesToUpdate | Where-Object { - $fileToUpdate = $_ - $include = -not ($filesToRemove | Where-Object { $_.sourceFullPath -eq $fileToUpdate.destinationFullPath }) # Note: comparing sourceFullPath of files to remove with destinationFullPath of files to update - if(-not $include) { - Write-Host "Excluding file $($fileToUpdate.sourceFullPath) from update as it is in the remove list" - } - return $include + # Apply unusedALGoSystemFiles logic + $unusedALGoSystemFiles = $settings.unusedALGoSystemFiles + + # Exclude unusedALGoSystemFiles from $filesToUpdate and add them to $filesToExclude + $unusedFilesToExclude = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -contains (Split-Path -Path $_.sourceFullPath -Leaf) } + if ($unusedFilesToExclude) { + # TODO: Trace-DeprecationWarning "The 'unusedALGoSystemFiles' setting is deprecated and will be removed in future versions. Please use 'updateALGoFiles.filesToExclude' instead." -DeprecationTag "unusedALGoSystemFiles" + + Write-Host "The following files are marked as unused and will be removed if they exist:" + $unusedFilesToExclude | ForEach-Object { Write-Host "- $($_.destinationFullPath)" } + + $filesToUpdate = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) } + $filesToExclude += @($unusedFilesToExclude) } - return @($filesToUpdate), @($filesToRemove) + return @($filesToUpdate), @($filesToExclude) } diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 8bde847f9..16765e7a7 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -54,12 +54,6 @@ if ($token) { # Get Repo settings as a hashtable (do NOT read any specific project settings, nor any specific workflow, user or branch settings) $repoSettings = ReadSettings -buildMode '' -project '' -workflowName '' -userName '' -branchName '' | ConvertTo-HashTable -recurse $templateSha = $repoSettings.templateSha -$unusedALGoSystemFiles = $repoSettings.unusedALGoSystemFiles -$includeBuildPP = $repoSettings.type -eq 'PTE' -and $repoSettings.powerPlatformSolutionFolder -ne '' -if (!$includeBuildPP) { - # Remove PowerPlatform workflows if no PowerPlatformSolution exists - $unusedALGoSystemFiles += @('_BuildPowerPlatformSolution.yaml','PushPowerPlatformChanges.yaml','PullPowerPlatformChanges.yaml') -} # If templateUrl has changed, download latest version of the template repository (ignore templateSha) if ($repoSettings.templateUrl -ne $templateUrl -or $templateSha -eq '') { @@ -121,17 +115,7 @@ if (-not $isDirectALGo) { $baseFolder = $ENV:GITHUB_WORKSPACE $projects = @(GetProjectsFromRepository -baseFolder $baseFolder -projectsFromSettings $repoSettings.projects) -$filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder - -#Exclude unusedALGoSystemFiles from $filesToUpdate and add them to $filesToRemove -$unusedFilesToRemove = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -contains (Split-Path -Path $_.sourceFullPath -Leaf) } -if ($unusedFilesToRemove) { - Write-Host "The following files are marked as unused and will be removed if they exist:" - $unusedFilesToRemove | ForEach-Object { Write-Host "- $($_.destinationFullPath)" } - - $filesToUpdate = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) } - $filesToRemove += @($unusedFilesToRemove | ForEach-Object { @{ 'sourceFullPath' = $_.destinationFullPath } }) -} +$filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder # $updateFiles will hold an array of files, which needs to be updated $updateFiles = @() @@ -217,9 +201,9 @@ foreach($fileToUpdate in $filesToUpdate) { } Push-Location -Path $baseFolder -# Remove files that are in $filesToRemove and exist in the repository -$removeFiles = $filesToRemove | Where-Object { $_ -and (Test-Path -Path $_ -PathType Leaf) } | ForEach-Object { - $relativePath = Resolve-Path -Path $_ -Relative +# Remove files that are in $filesToExclude and exist in the repository +$removeFiles = $filesToExclude | Where-Object { $_ -and (Test-Path -Path $_.destinationFullPath -PathType Leaf) } | ForEach-Object { + $relativePath = Resolve-Path -Path $_.destinationFullPath -Relative Write-Host "File marked for removal: $relativePath" $relativePath } diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 954872795..e0b9d5575 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -80,11 +80,11 @@ Describe('YamlClass Tests') { $count | Should -be 19 # Replace all occurances of 'shell: powershell' with 'shell: pwsh' - $yaml.ReplaceAll('shell: powershell','shell: pwsh') + $yaml.ReplaceAll('shell: powershell', 'shell: pwsh') $yaml.content[46].Trim() | Should -be 'shell: pwsh' # Replace Permissions - $yaml.Replace('Permissions:/',@('contents: write','actions: read')) + $yaml.Replace('Permissions:/', @('contents: write', 'actions: read')) $yaml.content[44].Trim() | Should -be 'shell: pwsh' $yaml.content.Count | Should -be 72 @@ -293,6 +293,14 @@ Describe "ResolveFilePaths" { } New-Item -Path (Join-Path $originalSourceFolder "folder/File1.txt") -ItemType File -Force | Out-Null New-Item -Path (Join-Path $originalSourceFolder "folder/File2.log") -ItemType File -Force | Out-Null + + # File tree: + # sourceFolder + # └── folder + # ├── File1.txt + # ├── File2.log + # ├── File3.txt + # └── File4.md } AfterAll { @@ -451,6 +459,68 @@ Describe "ResolveFilePaths" { $fullFilePaths[2].originalSourceFullPath | Should -Be $null $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") } + + It 'ResolveFilePaths with a single project' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + @{ "sourcePath" = "folder"; "filter" = "*.md"; type = "markdown"; } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("SomeProject") + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" + + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File3.txt") + $fullFilePaths[1].type | Should -Be "text" + + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[2].type | Should -Be "markdown" + } + + It 'ResolveFilePaths with multiple projects' { + $destinationPath = "destinationPath" + $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $files = @( + @{ "sourcePath" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + @{ "sourcePath" = "folder"; "filter" = "*.md"; type = "markdown"; } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA", "ProjectB") + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 5 + + # ProjectA files + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" + + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File1.txt") + $fullFilePaths[1].type | Should -Be "text" + + # ProjectB files + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") + $fullFilePaths[2].type | Should -Be "text" + + $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File3.txt") + $fullFilePaths[3].type | Should -Be "text" + + # Non-per-project file + $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[4].type | Should -Be "markdown" + } } Describe "ReplaceOwnerRepoAndBranch" { @@ -491,7 +561,7 @@ Describe "IsDirectALGo" { } } -Describe "GetFilesToUpdate" { +Describe "GetFilesToUpdate (general files to update logic)" { BeforeAll { $actionName = "CheckForUpdates" $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve @@ -535,14 +605,15 @@ Describe "GetFilesToUpdate" { It "Returns the correct files to update with filters" { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.ps1" }) - filesToIgnore = @() - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.ps1" }) + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToUpdate | Should -Not -BeNullOrEmpty $filesToUpdate.Count | Should -Be 1 @@ -550,17 +621,18 @@ Describe "GetFilesToUpdate" { $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt" }) - filesToIgnore = @() - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt" }) + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToUpdate | Should -Not -BeNullOrEmpty $filesToUpdate.Count | Should -Be 2 $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile @@ -569,198 +641,278 @@ Describe "GetFilesToUpdate" { $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty } - It 'Returns the correct files when there are files to ignore' { + It 'Returns the correct files with destinationPath' { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.*" }) - filesToIgnore = @(@{ filter = "test.txt" }) - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToUpdate | Should -Not -BeNullOrEmpty $filesToUpdate.Count | Should -Be 2 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test2.txt') # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt" }) - filesToIgnore = @(@{ filter = "*" }) - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) + filesToExclude = @(@{ filter = "test2.txt" }) } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # No files to update - $filesToUpdate | Should -BeNullOrEmpty - # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + + # One file to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be $testTxtFile2 + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') } - It 'Returns the correct files within subfolders' { + It 'Returns the correct files with destinationName' { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ sourcePath = 'subfolder'; filter = "*.txt" }) - filesToIgnore = @() - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 2 - $filesToUpdate[0].sourceFullPath | Should -Be $testSubfolderFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'subfolder/testsub.txt') - $filesToUpdate[1].sourceFullPath | Should -Be $testSubfolderFile2 - $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'subfolder/testsub2.txt') + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ sourcePath = 'subfolder'; filter = "*.txt" }) - filesToIgnore = @(@{ sourcePath = 'subfolder'; filter = "testsub2.txt" }) - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "test.ps1"; destinationPath = 'dstPath'; destinationName = "renamed.txt" }) + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToUpdate | Should -Not -BeNullOrEmpty $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testSubfolderFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'subfolder/testsub.txt') - - # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') } - It 'Returns the correct files with destinationPath' { + It 'Return the correct files with types' { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) - filesToIgnore = @() - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.ps1"; type = "script" }) + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 2 - $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') - $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test2.txt') + $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToUpdate[0].type | Should -Be "script" # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) - filesToIgnore = @(@{ filter = "test2.txt" }) - filesToRemove = @() + type = "NotPTE" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*.txt"; type = "text" }) + filesToExclude = @(@{ filter = "test.txt" }) } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToUpdate | Should -Not -BeNullOrEmpty $filesToUpdate.Count | Should -Be 1 + $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile2 + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToUpdate[0].type | Should -Be "text" + + # One file to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + } + + It 'Return the correct files when unusedALGoSystemFiles is specified' { + $settings = @{ + type = "nonPTE" + unusedALGoSystemFiles = @("test.ps1") + updateALGoFiles = @{ + filesToUpdate = @(@{ filter = "*" }) + filesToExclude = @() + } + } + + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 2 $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - # No files to remove - $filesToRemove | Should -BeNullOrEmpty + # One file to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be $testPSFile + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') } +} - It 'Returns the correct files with destinationName' { +Describe 'GetFilesToUpdate (real template)' { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realPTETemplateFolder', Justification = 'False positive.')] + $realPTETemplateFolder = Join-Path $PSScriptRoot "../Templates/Per Tenant Extension" -Resolve + } + + It 'Return the correct files to exclude when type is PTE and powerPlatformSolutionFolder is not empty' { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) - filesToIgnore = @() - filesToRemove = @() + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @() + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') + $filesToUpdate.Count | Should -Be 24 + $filesToUpdate.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToUpdate.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToUpdate.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") # No files to remove - $filesToRemove | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty + } + It 'Return PP files in filesToExclude when type is PTE but powerPlatformSolutionFolder is empty' { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "test.ps1"; destinationPath = 'dstPath'; destinationName = "renamed.txt" }) - filesToIgnore = @() - filesToRemove = @() + type = "PTE" + powerPlatformSolutionFolder = "" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @() + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') + $filesToUpdate.Count | Should -Be 21 + + $filesToUpdate | ForEach-Object { + $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + } + + # All PP files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 3 + + $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/_BuildPowerPlatformSolution.yaml") + + $filesToExclude[1].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PullPowerPlatformChanges.yaml") + + $filesToExclude[2].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToExclude[2].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PushPowerPlatformChanges.yaml") } - It 'Return the correct files with types' { + It 'Return the correct files when unusedALGoSystemFiles is specified' { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.ps1"; type = "script" }) - filesToIgnore = @() - filesToRemove = @() + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @("Test Next Major.settings.json") + updateALGoFiles = @{ + filesToUpdate = @() + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') - $filesToUpdate[0].type | Should -Be "script" + $filesToUpdate.Count | Should -Be 23 - # No files to remove - $filesToRemove | Should -BeNullOrEmpty + # Two files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/Test Next Major.settings.json') + } + It 'Return the correct files when unusedALGoSystemFiles is specified and no PP solution is present' { $settings = @{ - updateALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; type = "text" }) - filesToIgnore = @(@{ filter = "test.txt" }) - filesToRemove = @() + type = "PTE" + powerPlatformSolutionFolder = "" + unusedALGoSystemFiles = @("Test Next Major.settings.json", "_BuildPowerPlatformSolution.yaml") + updateALGoFiles = @{ + filesToUpdate = @() + filesToExclude = @() } } - $filesToUpdate, $filesToRemove = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile2 - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - $filesToUpdate[0].type | Should -Be "text" + $filesToUpdate.Count | Should -Be 20 - # No files to remove - $filesToRemove | Should -BeNullOrEmpty + # Four files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 4 + + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") } } From 9c6cd0e938acfac752b0e3164efab074d9f66e6d Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 00:55:14 +0100 Subject: [PATCH 30/92] Remove unused parameter --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 9022fc83f..6c4c499d2 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -676,7 +676,6 @@ function ResolveFilePaths { function GetDefaultFilesToUpdate { Param( - $settings, [switch] $includeCustomTemplateFiles ) @@ -749,7 +748,7 @@ function GetFilesToUpdate { $originalTemplateFolder = $null ) - $filesToUpdate = GetDefaultFilesToUpdate -settings $settings -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) + $filesToUpdate = GetDefaultFilesToUpdate -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) $filesToUpdate += $settings.updateALGoFiles.filesToUpdate $filesToUpdate = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToUpdate -projects $projects From fda7bfa0fc4a8748f8d85cf1acd501a61f7935aa Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 00:57:18 +0100 Subject: [PATCH 31/92] Fix settings schema --- Actions/.Modules/settings.schema.json | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index 59b69ad53..d4459e6fe 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -760,21 +760,7 @@ } } }, - "filesToIgnore": { - "type": "array", - "items": { - "type": "object", - "properties": { - "sourcePath": { - "type": "string" - }, - "filter": { - "type": "string" - } - } - } - }, - "filesToRemove": { + "filesToExclude": { "type": "array", "items": { "type": "object", @@ -791,4 +777,4 @@ } } } -} +} \ No newline at end of file From 447b94c8205222db3396dcc15fd4dbecb231a46b Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 01:26:38 +0100 Subject: [PATCH 32/92] Add test to include custom template files --- Actions/.Modules/ReadSettings.psm1 | 2 +- .../CheckForUpdates.HelperFunctions.ps1 | 2 ++ Tests/CheckForUpdates.Action.Test.ps1 | 35 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 33d073b82..87d7d1b5b 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -620,4 +620,4 @@ function SanitizeWorkflowName { } Export-ModuleMember -Function ReadSettings -Export-ModuleMember -Variable ALGoFolderName, ALGoSettingsFile, RepoSettingsFile, CustomTemplateRepoSettingsFile, CustomTemplateProjectSettingsFile +Export-ModuleMember -Variable ALGoFolderName, ALGoSettingsFile, RepoSettingsFile, CustomTemplateRepoSettingsFile, CustomTemplateProjectSettingsFile, RepoSettingsFileName, ALGoSettingsFileName,CustomTemplateRepoSettingsFileName, CustomTemplateProjectSettingsFileName diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 6c4c499d2..e76837964 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -1,3 +1,5 @@ +Import-Module (Join-Path $PSScriptRoot '../.Modules/ReadSettings.psm1') -DisableNameChecking -Force + <# .SYNOPSIS Downloads a template repository and returns the path to the downloaded folder diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index e0b9d5575..0b43c44f1 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -806,6 +806,9 @@ Describe 'GetFilesToUpdate (real template)' { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realPTETemplateFolder', Justification = 'False positive.')] $realPTETemplateFolder = Join-Path $PSScriptRoot "../Templates/Per Tenant Extension" -Resolve + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realAppSourceAppTemplateFolder', Justification = 'False positive.')] + $realAppSourceAppTemplateFolder = Join-Path $PSScriptRoot "../Templates/AppSource App" -Resolve } It 'Return the correct files to exclude when type is PTE and powerPlatformSolutionFolder is not empty' { @@ -915,4 +918,36 @@ Describe 'GetFilesToUpdate (real template)' { $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") } + + It 'Returns the custom template settings files when there is a custom template' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @() + updateALGoFiles = @{ + filesToUpdate = @() + filesToExclude = @() + } + } + + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -originalTemplateFolder $realAppSourceAppTemplateFolder # Indicate custom template + + $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToUpdate.Count | Should -Be 26 + + $repoSettingsFiles = $filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $realPTETemplateFolder ".github/AL-Go-Settings.json") } + $repoSettingsFiles | Should -Not -BeNullOrEmpty + $repoSettingsFiles.Count | Should -Be 2 + + $repoSettingsFiles[0].originalSourceFullPath | Should -Be (Join-Path $realAppSourceAppTemplateFolder ".github/AL-Go-Settings.json") + $repoSettingsFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-Settings.json') + $repoSettingsFiles[0].type | Should -Be 'settings' + + $repoSettingsFiles[1].originalSourceFullPath | Should -Be $null # Because type is 'custom template', originalSourceFullPath should be $null + $repoSettingsFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') + $repoSettingsFiles[1].type | Should -Be 'custom template' + + # No files to remove + $filesToExclude | Should -BeNullOrEmpty + } } From 238ddaa71fc707585e57b93bc5b60cd7ddfd5f60 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 11:25:54 +0100 Subject: [PATCH 33/92] Add type prop only where needed. Add origin property to differentiate between files from original template and custom template. --- Actions/.Modules/settings.schema.json | 3 - .../CheckForUpdates.HelperFunctions.ps1 | 32 +++-- Tests/CheckForUpdates.Action.Test.ps1 | 115 ++++++++++++------ 3 files changed, 99 insertions(+), 51 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index d4459e6fe..66c48f651 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -751,9 +751,6 @@ "filter": { "type": "string" }, - "type": { - "type": "string" - }, "perProject": { "type": "boolean" } diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index e76837964..f90167cb9 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -586,8 +586,12 @@ function ResolveFilePaths { $file.filter = '' # Default to all files } + if($file.Keys -notcontains 'origin') { + $file.origin = 'template' # Default to template + } + if($file.Keys -notcontains 'type') { - $file.type = '' # Default to empty type + $file.type = '' # Default to empty } if($file.Keys -notcontains 'destinationPath') { @@ -600,7 +604,7 @@ function ResolveFilePaths { } # If originalSourceFolder is not specified, it means there is no custom template, so skip custom template files - if(!$originalSourceFolder -and $file.type -eq 'custom template') { + if(!$originalSourceFolder -and $file.origin -eq 'custom template') { Write-Host "Skipping custom template file with sourcePath '$($file.sourcePath)' as there is no original source folder specified" continue; } @@ -609,7 +613,7 @@ function ResolveFilePaths { Write-Host "Resolving files for sourcePath '$($file.sourcePath)' and filter '$($file.filter)'" $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourcePath) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) - Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder')" + Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder', origin '$($file.origin)')" if(-not $sourceFiles) { Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder')" continue @@ -624,7 +628,7 @@ function ResolveFilePaths { } # Try to find the same files in the original template folder if it is specified. Exclude custom template files - if ($originalSourceFolder -and ($file.type -ne 'custom template')) { + if ($originalSourceFolder -and ($file.origin -ne 'custom template')) { Push-Location $sourceFolder $relativePath = Resolve-Path -Path $srcFile -Relative # resolve the path relative to the current location (template folder) Pop-Location @@ -650,6 +654,10 @@ function ResolveFilePaths { # Destination full path is the destination base folder + project + destinationPath + destinationName foreach($project in $projects) { + if($project -eq '.') { + $project = '' # If project is '.', it means the root folder, so we use an empty string + } + $projectFile = $fullFilePath.Clone() $projectFile.destinationFullPath = Join-Path $destinationFolder $project @@ -670,8 +678,8 @@ function ResolveFilePaths { } } } - # Remove duplicates (when sourceFullPath and destinationFullPath are the same) - $fullFilePaths = $fullFilePaths | Sort-Object { $_.sourceFullPath}, { $_.destinationFullPath} -Unique + # Remove duplicates + $fullFilePaths = $fullFilePaths | Sort-Object { $_.sourceFullPath}, { $_.originalSourceFullPath}, { $_.destinationFullPath}, { $_.type } -Unique return $fullFilePaths } @@ -683,20 +691,20 @@ function GetDefaultFilesToUpdate { $filesToUpdate = @( [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*'; 'type' = 'workflow' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md'; 'type' = 'releasenotes' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1'; 'type' = 'script' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } - [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'type' = 'script'; 'perProject' = $true }, - [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'type' = 'settings'; 'perProject' = $true } + [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'perProject' = $true }, + [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'perProject' = $true; 'type' = 'settings' } ) if($includeCustomTemplateFiles) { # If there is an original template folder, we also need to include custom template files (RepoSettings and ProjectSettings) $filesToUpdate += @( - [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'type' = 'custom template' } - [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'type' = 'custom template' } + [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'origin' = 'custom template' } + [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'origin' = 'custom template' } ) } diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 0b43c44f1..f69b629d7 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -318,8 +318,8 @@ Describe "ResolveFilePaths" { $destinationPath = "destinationPath" $destinationFolder = Join-Path $rootFolder $destinationPath $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = 'newFolder'; "destinationName" = "" } - @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = 'newFolder'; "destinationName" = "" } + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = 'newFolder'; "destinationName" = '' } + @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = 'newFolder'; "destinationName" = '' } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder @@ -328,13 +328,13 @@ Describe "ResolveFilePaths" { $fullFilePaths.Count | Should -Be 3 $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") - $fullFilePaths[0].type | Should -Be "" + $fullFilePaths[0].type | Should -Be '' $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - $fullFilePaths[1].type | Should -Be "" + $fullFilePaths[1].type | Should -Be '' $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") - $fullFilePaths[2].type | Should -Be "" + $fullFilePaths[2].type | Should -Be '' } It 'ResolveFilePaths with specific destination names' { @@ -351,18 +351,18 @@ Describe "ResolveFilePaths" { $fullFilePaths.Count | Should -Be 2 $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile1.txt") - $fullFilePaths[0].type | Should -Be "" + $fullFilePaths[0].type | Should -Be '' $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile2.log") - $fullFilePaths[1].type | Should -Be "" + $fullFilePaths[1].type | Should -Be '' } It 'ResolveFilePaths with type' { $destinationPath = "destinationPath" $destinationFolder = Join-Path $PSScriptRoot $destinationPath $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "folder"; "destinationName" = ""; type = "text" } - @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "folder"; "destinationName" = ""; type = "markdown" } + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "folder"; "destinationName" = ''; type = "text" } + @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "folder"; "destinationName" = ''; type = "markdown" } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder @@ -384,8 +384,8 @@ Describe "ResolveFilePaths" { $destinationPath = "destinationPath" $destinationFolder = Join-Path $PSScriptRoot $destinationPath $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "text" } - @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "markdown" } + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "text" } + @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "markdown" } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder @@ -408,34 +408,60 @@ Describe "ResolveFilePaths" { $fullFilePaths[2].type | Should -Be "markdown" } - It 'ResolveFilePaths returns unique file paths' { + It 'ResolveFilePaths returns unique file paths based on the file type' { $destinationPath = "destinationPath" $destinationFolder = Join-Path $PSScriptRoot $destinationPath $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "text"; } - @{ "sourcePath" = "folder"; "filter" = "*"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "unknown"; } + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "text"; } + @{ "sourcePath" = "folder"; "filter" = "*"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "unknown"; } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 4 + $fullFilePaths.Count | Should -Be 6 + + # File1.txt is matched twice, so it should appear twice with different types $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") + $fullFilePaths[0].originalSourceFullPath | Should -Be $null + $fullFilePaths[0].type | Should -Be "text" + + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + $fullFilePaths[1].originalSourceFullPath | Should -Be $null + $fullFilePaths[1].type | Should -Be "unknown" + + # File2.log is matched only once, so it should appear only once with type "unknown" + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") + $fullFilePaths[2].originalSourceFullPath | Should -Be $null + $fullFilePaths[2].type | Should -Be "unknown" + + # File3.txt is matched twice, so it should appear twice with different types + $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + $fullFilePaths[3].originalSourceFullPath | Should -Be $null + $fullFilePaths[3].type | Should -Be "text" + + $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + $fullFilePaths[4].originalSourceFullPath | Should -Be $null + $fullFilePaths[4].type | Should -Be "unknown" + + # File4.md is matched only once, so it should appear only once with type "unknown" + $fullFilePaths[5].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[5].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") + $fullFilePaths[5].originalSourceFullPath | Should -Be $null + $fullFilePaths[5].type | Should -Be "unknown" } - It 'ResolveFilePaths populates the originalSourceFullPath property only if the type does not contain "template"' { + It 'ResolveFilePaths populates the originalSourceFullPath property only if the origin is not a custom template' { $destinationPath = "destinationPath" $destinationFolder = Join-Path $PSScriptRoot $destinationPath $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "custom template"; } - @{ "sourcePath" = "folder"; "filter" = "*.log"; "destinationPath" = "newFolder"; "destinationName" = ""; type = "log"; } + @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ''; "origin" = "custom template"; } + @{ "sourcePath" = "folder"; "filter" = "*.log"; "destinationPath" = "newFolder"; "destinationName" = ''; } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder @@ -837,7 +863,7 @@ Describe 'GetFilesToUpdate (real template)' { It 'Return PP files in filesToExclude when type is PTE but powerPlatformSolutionFolder is empty' { $settings = @{ type = "PTE" - powerPlatformSolutionFolder = "" + powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() updateALGoFiles = @{ filesToUpdate = @() @@ -896,7 +922,7 @@ Describe 'GetFilesToUpdate (real template)' { It 'Return the correct files when unusedALGoSystemFiles is specified and no PP solution is present' { $settings = @{ type = "PTE" - powerPlatformSolutionFolder = "" + powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @("Test Next Major.settings.json", "_BuildPowerPlatformSolution.yaml") updateALGoFiles = @{ filesToUpdate = @() @@ -930,24 +956,41 @@ Describe 'GetFilesToUpdate (real template)' { } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -originalTemplateFolder $realAppSourceAppTemplateFolder # Indicate custom template + $customTemplateFolder = $realPTETemplateFolder + $originalTemplateFolder = $realAppSourceAppTemplateFolder + $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 26 - $repoSettingsFiles = $filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $realPTETemplateFolder ".github/AL-Go-Settings.json") } + # Check repo settings files + $repoSettingsFiles = $filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".github/AL-Go-Settings.json") } $repoSettingsFiles | Should -Not -BeNullOrEmpty $repoSettingsFiles.Count | Should -Be 2 - $repoSettingsFiles[0].originalSourceFullPath | Should -Be (Join-Path $realAppSourceAppTemplateFolder ".github/AL-Go-Settings.json") - $repoSettingsFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-Settings.json') - $repoSettingsFiles[0].type | Should -Be 'settings' + $repoSettingsFiles[0].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null + $repoSettingsFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') + $repoSettingsFiles[0].type | Should -Be '' - $repoSettingsFiles[1].originalSourceFullPath | Should -Be $null # Because type is 'custom template', originalSourceFullPath should be $null - $repoSettingsFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') - $repoSettingsFiles[1].type | Should -Be 'custom template' + $repoSettingsFiles[1].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".github/AL-Go-Settings.json") + $repoSettingsFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-Settings.json') + $repoSettingsFiles[1].type | Should -Be 'settings' - # No files to remove + # Check project settings files + $projectSettingsFilesFromCustomTemplate = @($filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".AL-Go/settings.json") }) + + $projectSettingsFilesFromCustomTemplate | Should -Not -BeNullOrEmpty + $projectSettingsFilesFromCustomTemplate.Count | Should -Be 2 + + $projectSettingsFilesFromCustomTemplate[0].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null + $projectSettingsFilesFromCustomTemplate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateProjectSettings.doNotEdit.json') + $projectSettingsFilesFromCustomTemplate[0].type | Should -Be '' + + + $projectSettingsFilesFromCustomTemplate[1].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".AL-Go/settings.json") + $projectSettingsFilesFromCustomTemplate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.AL-Go/settings.json') + $projectSettingsFilesFromCustomTemplate[1].type | Should -Be 'settings' + + # No files to exclude $filesToExclude | Should -BeNullOrEmpty } } From ba74db8b97443ac3540a1a17cbc4a8be588a2175 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 11:47:45 +0100 Subject: [PATCH 34/92] Reuse variables --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index f90167cb9..f06d45f16 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -605,7 +605,7 @@ function ResolveFilePaths { # If originalSourceFolder is not specified, it means there is no custom template, so skip custom template files if(!$originalSourceFolder -and $file.origin -eq 'custom template') { - Write-Host "Skipping custom template file with sourcePath '$($file.sourcePath)' as there is no original source folder specified" + Write-Host "Skipping custom template file(s) with sourcePath '$($file.sourcePath)' as there is no original source folder specified" continue; } @@ -693,11 +693,11 @@ function GetDefaultFilesToUpdate { [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*'; 'type' = 'workflow' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = 'AL-Go-Settings.json'; 'type' = 'settings' } + [ordered]@{ 'sourcePath' = '.github'; 'filter' = "$RepoSettingsFileName"; 'type' = 'settings' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'perProject' = $true }, - [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = 'settings.json'; 'perProject' = $true; 'type' = 'settings' } + [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = "$ALGoSettingsFileName"; 'perProject' = $true; 'type' = 'settings' } ) if($includeCustomTemplateFiles) { From fd5bf30159bff366ad85afe649d54f07ec63d8ff Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 11:51:53 +0100 Subject: [PATCH 35/92] Revert formatting changes in settings schema --- Actions/.Modules/settings.schema.json | 47 +++------------------------ 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index 66c48f651..4b780f3de 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -375,9 +375,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "includeDependencies" - ] + "enum": ["includeDependencies"] }, "description": "An array of settings to be overwritten by the current deliverToAppSource setting. See https://aka.ms/ALGoSettings#overwriteSettings" } @@ -610,10 +608,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "includeProjects", - "excludeProjects" - ] + "enum": ["includeProjects", "excludeProjects"] }, "description": "An array of settings to be overwritten by the current alDoc setting. See https://aka.ms/ALGoSettings#overwriteSettings" } @@ -629,10 +624,7 @@ }, "pullRequestMergeMethod": { "type": "string", - "enum": [ - "merge", - "squash" - ], + "enum": ["merge", "squash"], "default": "squash" }, "pullRequestLabels": { @@ -646,9 +638,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "pullRequestLabels" - ] + "enum": ["pullRequestLabels"] }, "description": "An array of settings to be overwritten by the current commitOptions setting. See https://aka.ms/ALGoSettings#overwriteSettings" } @@ -696,34 +686,7 @@ "type": "array", "items": { "type": "string", - "enum": [ - "unusedALGoSystemFiles", - "projects", - "additionalCountries", - "appDependencies", - "appFolders", - "testDependencies", - "testFolders", - "bcptTestFolders", - "pageScriptingTests", - "restoreDatabases", - "installApps", - "installTestApps", - "customCodeCops", - "configPackages", - "appSourceCopMandatoryAffixes", - "deliverToAppSource", - "appDependencyProbingPaths", - "incrementalBuilds", - "environments", - "buildModes", - "bcptThresholds", - "fullBuildPatterns", - "excludeEnvironments", - "alDoc", - "commitOptions", - "trustedSigning" - ] + "enum": ["unusedALGoSystemFiles", "projects", "additionalCountries", "appDependencies", "appFolders", "testDependencies", "testFolders", "bcptTestFolders", "pageScriptingTests", "restoreDatabases", "installApps", "installTestApps", "customCodeCops", "configPackages", "appSourceCopMandatoryAffixes", "deliverToAppSource", "appDependencyProbingPaths", "incrementalBuilds", "environments", "buildModes", "bcptThresholds", "fullBuildPatterns", "excludeEnvironments", "alDoc", "commitOptions", "trustedSigning"] }, "description": "An array of settings to be overwritten by the current settings. See https://aka.ms/ALGoSettings#overwriteSettings" }, From 637504c3da0b6b4c5b2849d41daf1162ea54506f Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 11:52:43 +0100 Subject: [PATCH 36/92] Remove -Force switch when importing module --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index f06d45f16..50cef5fde 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -1,4 +1,4 @@ -Import-Module (Join-Path $PSScriptRoot '../.Modules/ReadSettings.psm1') -DisableNameChecking -Force +Import-Module (Join-Path $PSScriptRoot '../.Modules/ReadSettings.psm1') -DisableNameChecking <# .SYNOPSIS From db8941805075a44235e5cf1498b4ac1185ef6004 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 11:57:21 +0100 Subject: [PATCH 37/92] Fix e2e test --- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index f883a8e14..c4d47c106 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -263,7 +263,7 @@ Pull (Join-Path (Get-Location) $customFile) | Should -Not -Exist # Custom file should not be copied by default # Add custom file to be copied via settings -$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "updateALGoFiles" = @{ "filesToUpdate" = @( @{ "sourcePath" = $customFile; "destinationPath" = '.' } ) } } +$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "updateALGoFiles" = @{ "filesToUpdate" = @( @{ "filter" = $customFile } ) } } # Push CommitAndPush -commitMessage 'Add custom file to be updated when updating AL-Go system files [skip ci]' From 2b94623ee3d55177dc9e051edcf7b90f4b83809d Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 12:24:12 +0100 Subject: [PATCH 38/92] Formatting --- Actions/.Modules/ReadSettings.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 87d7d1b5b..2a9114050 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -620,4 +620,4 @@ function SanitizeWorkflowName { } Export-ModuleMember -Function ReadSettings -Export-ModuleMember -Variable ALGoFolderName, ALGoSettingsFile, RepoSettingsFile, CustomTemplateRepoSettingsFile, CustomTemplateProjectSettingsFile, RepoSettingsFileName, ALGoSettingsFileName,CustomTemplateRepoSettingsFileName, CustomTemplateProjectSettingsFileName +Export-ModuleMember -Variable ALGoFolderName, ALGoSettingsFile, RepoSettingsFile, CustomTemplateRepoSettingsFile, CustomTemplateProjectSettingsFile, RepoSettingsFileName, ALGoSettingsFileName, CustomTemplateRepoSettingsFileName, CustomTemplateProjectSettingsFileName From 2d2aa7af4c2413515e754e03a7142e1fd060c53b Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 12:25:55 +0100 Subject: [PATCH 39/92] Fix pre-commit --- Actions/.Modules/settings.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index 4b780f3de..4f43b5216 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -737,4 +737,4 @@ } } } -} \ No newline at end of file +} From b78dbc6c35f15dd66680f0bde33f6747b850bc34 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 12:34:53 +0100 Subject: [PATCH 40/92] Remove includeBuildPP variable and calculate it when needed --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 4 ++-- Actions/CheckForUpdates/CheckForUpdates.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 50cef5fde..b1a6a402e 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -308,8 +308,7 @@ function GetWorkflowContentWithChangesFromSettings { Param( [string] $srcFile, [hashtable] $repoSettings, - [int] $depth, - [bool] $includeBuildPP + [int] $depth ) $baseName = [System.IO.Path]::GetFileNameWithoutExtension($srcFile) @@ -390,6 +389,7 @@ function GetWorkflowContentWithChangesFromSettings { # PullRequestHandler, CICD, Current, NextMinor and NextMajor workflows all include a build step. # If the dependency depth is higher than 1, we need to add multiple dependent build jobs to the workflow if ($baseName -eq 'PullRequestHandler' -or $baseName -eq 'CICD' -or $baseName -eq 'Current' -or $baseName -eq 'NextMinor' -or $baseName -eq 'NextMajor') { + $includeBuildPP = $repoSettings.type -eq 'PTE' -and $repoSettings.powerPlatformSolutionFolder -ne '' ModifyBuildWorkflows -yaml $yaml -depth $depth -includeBuildPP $includeBuildPP } diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 16765e7a7..8da5acdb1 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -149,7 +149,7 @@ foreach($fileToUpdate in $filesToUpdate) { switch ($type) { "workflow" { # For workflow files, we might need to modify the file based on the settings - $srcContent = GetWorkflowContentWithChangesFromSettings -srcFile $originalSrcPath -repoSettings $repoSettings -depth $depth -includeBuildPP $includeBuildPP + $srcContent = GetWorkflowContentWithChangesFromSettings -srcFile $originalSrcPath -repoSettings $repoSettings -depth $depth } "settings" { # For settings files, we need to modify the file based on the settings From 516922f3848a85ff1cf35cd5f8fa65d3516ee059 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 13:47:09 +0100 Subject: [PATCH 41/92] Remove $type --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 8da5acdb1..c90a0e76f 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -186,16 +186,16 @@ foreach($fileToUpdate in $filesToUpdate) { # file exists, compare and add to $updateFiles if different $dstContent = Get-ContentLF -Path $dstPath if ($dstContent -cne $srcContent) { - Write-Host "Available updates for $type ($relativeDstPath)" + Write-Host "Available updates for file $relativeDstPath" $updateFiles += @{ "DstFile" = $relativeDstPath; "content" = $srcContent } } else { - Write-Host "No updates for $type ($relativeDstPath)" + Write-Host "No updates for file $relativeDstPath" } } else { # new file, add to $updateFiles - Write-Host "New $type ($relativeDstPath) available" + Write-Host "New file available: $relativeDstPath" $updateFiles += @{ "DstFile" = $relativeDstPath; "content" = $srcContent } } } From 6654920d0a42bede02d5a86cf7ae748c8cdb8ae0 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 13:51:10 +0100 Subject: [PATCH 42/92] Add logging --- .../CheckForUpdates.HelperFunctions.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index b1a6a402e..33f22ef6b 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -614,6 +614,7 @@ function ResolveFilePaths { $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourcePath) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder', origin '$($file.origin)')" + if(-not $sourceFiles) { Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder')" continue @@ -627,6 +628,8 @@ function ResolveFilePaths { 'destinationFullPath' = $null } + Write-Host "Processing file '$($srcFile)'" + # Try to find the same files in the original template folder if it is specified. Exclude custom template files if ($originalSourceFolder -and ($file.origin -ne 'custom template')) { Push-Location $sourceFolder @@ -640,8 +643,9 @@ function ResolveFilePaths { if(-not $destinationFolder) { # Destination folder is not specified, no need to calculate destinationFullPath as it will not be used - $fullFilePaths += $fullFilePath - continue + Write-Host "No destination folder specified, skipping destinationFullPath calculation. Adding sourceFullPath '$($fullFilePath.sourceFullPath)' and originalSourceFullPath '$($fullFilePath.originalSourceFullPath)' to the list." + $fullFilePaths += $fullFilePath + continue } $destinationName = $(Split-Path -Path $srcFile -Leaf) @@ -664,6 +668,7 @@ function ResolveFilePaths { $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $file.destinationPath $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $destinationName + Write-Host "Adding per-project file for project '$project': sourceFullPath '$($projectFile.sourceFullPath)', originalSourceFullPath '$($projectFile.originalSourceFullPath)', destinationFullPath '$($projectFile.destinationFullPath)'" $fullFilePaths += $projectFile } } @@ -674,6 +679,7 @@ function ResolveFilePaths { $fullFilePath.destinationFullPath = Join-Path $destinationFolder $file.destinationPath $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $destinationName + Write-Host "Adding file: sourceFullPath '$($fullFilePath.sourceFullPath)', originalSourceFullPath '$($fullFilePath.originalSourceFullPath)', destinationFullPath '$($fullFilePath.destinationFullPath)'" $fullFilePaths += $fullFilePath } } From 42de096c7b4644922b7dba0ea9f43c3ea96978b0 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 13:53:33 +0100 Subject: [PATCH 43/92] Make destinationFolder mandatory --- .../CheckForUpdates.HelperFunctions.ps1 | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 33f22ef6b..40923ac3d 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -567,7 +567,8 @@ function ResolveFilePaths { [Parameter(Mandatory=$true)] [string] $sourceFolder, [string] $originalSourceFolder = $null, - [string] $destinationFolder = $null, + [Parameter(Mandatory=$true)] + [string] $destinationFolder, [array] $files = @(), [string[]] $projects = @() ) @@ -641,13 +642,6 @@ function ResolveFilePaths { } } - if(-not $destinationFolder) { - # Destination folder is not specified, no need to calculate destinationFullPath as it will not be used - Write-Host "No destination folder specified, skipping destinationFullPath calculation. Adding sourceFullPath '$($fullFilePath.sourceFullPath)' and originalSourceFullPath '$($fullFilePath.originalSourceFullPath)' to the list." - $fullFilePaths += $fullFilePath - continue - } - $destinationName = $(Split-Path -Path $srcFile -Leaf) if($file.Keys -contains 'destinationName' -and ($file.destinationName)) { $destinationName = $file.destinationName From 1909b22794414b21c077ed4e2a5371f9594a0a6d Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 14:54:31 +0100 Subject: [PATCH 44/92] Filter workflow files better --- .../CheckForUpdates.HelperFunctions.ps1 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 40923ac3d..1cf2692fb 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -690,7 +690,8 @@ function GetDefaultFilesToUpdate { ) $filesToUpdate = @( - [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*'; 'type' = 'workflow' } + [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*.yaml'; 'type' = 'workflow' } + [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*.yml'; 'type' = 'workflow' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1' } [ordered]@{ 'sourcePath' = '.github'; 'filter' = "$RepoSettingsFileName"; 'type' = 'settings' } @@ -738,24 +739,27 @@ function GetDefaultFilesToExclude { The unusedALGoSystemFiles setting is also applied to exclude files from the update list and add them to the exclude list. .PARAMETER settings The settings object containing the updateALGoFiles configuration. -.PARAMETER projects - The list of projects in the repository. .PARAMETER baseFolder The base folder of the repository. .PARAMETER templateFolder The folder where the template files are located. .PARAMETER originalTemplateFolder The folder where the original template files are located (if any). In case of custom templates. +.PARAMETER projects + The list of projects in the repository. .OUTPUTS An array containing two elements: the list of files to update and the list of files to exclude. #> function GetFilesToUpdate { Param( + [Parameter(Mandatory=$true)] $settings, - $projects, + [Parameter(Mandatory=$true)] $baseFolder, + [Parameter(Mandatory=$true)] $templateFolder, - $originalTemplateFolder = $null + $originalTemplateFolder = $null, + $projects = @() ) $filesToUpdate = GetDefaultFilesToUpdate -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) From aa714ecce67d3aaaba732a9071a483e7c69df834 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 15:30:34 +0100 Subject: [PATCH 45/92] Add defensive check for when the parent folder is the root folder --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index c90a0e76f..33158e0cc 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -255,7 +255,7 @@ else { $updateFiles | ForEach-Object { # Create the destination folder if it doesn't exist $path = [System.IO.Path]::GetDirectoryName($_.DstFile) - if (-not (Test-Path -path $path -PathType Container)) { + if ($path -and -not (Test-Path -path $path -PathType Container)) { New-Item -Path $path -ItemType Directory | Out-Null } if (([System.IO.Path]::GetFileName($_.DstFile) -eq "RELEASENOTES.copy.md") -and (Test-Path $_.DstFile)) { From ef7f7dce7d60db87cb0ed73279fefe20ed0e9a2f Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 16:37:00 +0100 Subject: [PATCH 46/92] Rename updateALGoFiles => customALGoFiles --- Actions/.Modules/ReadSettings.psm1 | 2 +- Actions/.Modules/settings.schema.json | 2 +- .../CheckForUpdates.HelperFunctions.ps1 | 8 +++--- Tests/CheckForUpdates.Action.Test.ps1 | 28 +++++++++---------- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 2 +- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 2a9114050..e33278fd0 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -248,7 +248,7 @@ function GetDefaultSettings "gitSubmodulesTokenSecretName" = "gitSubmodulesToken" "shortLivedArtifactsRetentionDays" = 1 # 0 means use GitHub default "reportSuppressedDiagnostics" = $false - "updateALGoFiles" = [ordered]@{ + "customALGoFiles" = [ordered]@{ "filesToUpdate" = @() "filesToExclude" = @() } diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index 4f43b5216..d995708ef 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -694,7 +694,7 @@ "type": "boolean", "description": "Report suppressed diagnostics. See https://aka.ms/ALGoSettings#reportsuppresseddiagnostics" }, - "updateALGoFiles": { + "customALGoFiles": { "type": "object", "properties": { "filesToUpdate": { diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 1cf2692fb..5787abee6 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -738,7 +738,7 @@ function GetDefaultFilesToExclude { This function gets the list of files to update and exclude based on the provided settings. The unusedALGoSystemFiles setting is also applied to exclude files from the update list and add them to the exclude list. .PARAMETER settings - The settings object containing the updateALGoFiles configuration. + The settings object containing the customALGoFiles configuration. .PARAMETER baseFolder The base folder of the repository. .PARAMETER templateFolder @@ -763,11 +763,11 @@ function GetFilesToUpdate { ) $filesToUpdate = GetDefaultFilesToUpdate -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) - $filesToUpdate += $settings.updateALGoFiles.filesToUpdate + $filesToUpdate += $settings.customALGoFiles.filesToUpdate $filesToUpdate = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToUpdate -projects $projects $filesToExclude = GetDefaultFilesToExclude -settings $settings - $filesToExclude += $settings.updateALGoFiles.filesToExclude + $filesToExclude += $settings.customALGoFiles.filesToExclude $filesToExclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExclude -projects $projects # Exclude files from filesToUpdate that are in filesToExclude @@ -786,7 +786,7 @@ function GetFilesToUpdate { # Exclude unusedALGoSystemFiles from $filesToUpdate and add them to $filesToExclude $unusedFilesToExclude = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -contains (Split-Path -Path $_.sourceFullPath -Leaf) } if ($unusedFilesToExclude) { - # TODO: Trace-DeprecationWarning "The 'unusedALGoSystemFiles' setting is deprecated and will be removed in future versions. Please use 'updateALGoFiles.filesToExclude' instead." -DeprecationTag "unusedALGoSystemFiles" + # TODO: Trace-DeprecationWarning "The 'unusedALGoSystemFiles' setting is deprecated and will be removed in future versions. Please use 'customALGoFiles.filesToExclude' instead." -DeprecationTag "unusedALGoSystemFiles" Write-Host "The following files are marked as unused and will be removed if they exist:" $unusedFilesToExclude | ForEach-Object { Write-Host "- $($_.destinationFullPath)" } diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index f69b629d7..5c65be8f4 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -633,7 +633,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "*.ps1" }) filesToExclude = @() } @@ -652,7 +652,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "*.txt" }) filesToExclude = @() } @@ -674,7 +674,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) filesToExclude = @() } @@ -695,7 +695,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) filesToExclude = @(@{ filter = "test2.txt" }) } @@ -719,7 +719,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) filesToExclude = @() } @@ -738,7 +738,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "test.ps1"; destinationPath = 'dstPath'; destinationName = "renamed.txt" }) filesToExclude = @() } @@ -756,7 +756,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "*.ps1"; type = "script" }) filesToExclude = @() } @@ -776,7 +776,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "*.txt"; type = "text" }) filesToExclude = @(@{ filter = "test.txt" }) } @@ -801,7 +801,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $settings = @{ type = "nonPTE" unusedALGoSystemFiles = @("test.ps1") - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @(@{ filter = "*" }) filesToExclude = @() } @@ -842,7 +842,7 @@ Describe 'GetFilesToUpdate (real template)' { type = "PTE" powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @() filesToExclude = @() } @@ -865,7 +865,7 @@ Describe 'GetFilesToUpdate (real template)' { type = "PTE" powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @() filesToExclude = @() } @@ -901,7 +901,7 @@ Describe 'GetFilesToUpdate (real template)' { type = "PTE" powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @("Test Next Major.settings.json") - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @() filesToExclude = @() } @@ -924,7 +924,7 @@ Describe 'GetFilesToUpdate (real template)' { type = "PTE" powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @("Test Next Major.settings.json", "_BuildPowerPlatformSolution.yaml") - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @() filesToExclude = @() } @@ -950,7 +950,7 @@ Describe 'GetFilesToUpdate (real template)' { type = "PTE" powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @() - updateALGoFiles = @{ + customALGoFiles = @{ filesToUpdate = @() filesToExclude = @() } diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index c4d47c106..8a87a39d1 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -263,7 +263,7 @@ Pull (Join-Path (Get-Location) $customFile) | Should -Not -Exist # Custom file should not be copied by default # Add custom file to be copied via settings -$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "updateALGoFiles" = @{ "filesToUpdate" = @( @{ "filter" = $customFile } ) } } +$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "customALGoFiles" = @{ "filesToUpdate" = @( @{ "filter" = $customFile } ) } } # Push CommitAndPush -commitMessage 'Add custom file to be updated when updating AL-Go system files [skip ci]' From 7369d172f54ef79100efd240af224d3614e49228 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 17:09:42 +0100 Subject: [PATCH 47/92] Add docs --- Actions/.Modules/settings.schema.json | 4 +--- RELEASENOTES.md | 4 ++++ Scenarios/CustomizingALGoForGitHub.md | 18 ++++++++++++++++++ Scenarios/settings.md | 1 + 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index d995708ef..a5e393872 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -695,6 +695,7 @@ "description": "Report suppressed diagnostics. See https://aka.ms/ALGoSettings#reportsuppresseddiagnostics" }, "customALGoFiles": { + "description": "An object containing the custom AL-Go files configuration. See https://aka.ms/ALGoSettings#customALGoFiles", "type": "object", "properties": { "filesToUpdate": { @@ -705,9 +706,6 @@ "destinationPath": { "type": "string" }, - "destinationName": { - "type": "string" - }, "sourcePath": { "type": "string" }, diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b9b727cd..c60d3652a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,3 +1,7 @@ +### Custom AL-Go files + +AL-Go for GitHub now supports updating files from your custom templates via the new `customALGoFiles` setting. Read more at [customALGoFiles](https://aka.ms/algosettings#customALGoFiles). + ### Issues - Issue 1961 KeyVault access in PR pipeline diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index beb2029ed..3dd85cdf9 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -201,6 +201,24 @@ Repositories based on your custom template will notify you that changes are avai > [!TIP] > You can setup the Update AL-Go System Files workflow to run on a schedule to uptake new releases of AL-Go for GitHub regularly. +## Using custom template files + +When using custom template repositories, often you need to add custom files that are related to AL-Go for GitHub, but are not part of the official AL-Go templates. Such files can be script overrides for certain AL-Go functionality, workflows that complement AL-Go capabilities or workflows that easier to manage centrally. + +In order to instruct AL-Go to sync such files, you need to define setting `customALGoFiles`. The setting is an object that can contain two properties: `filesToUpdate` and `filesToExclude`. + +`filesToUpdate`, as the name suggests, is an array of file configurations that will instruct AL-Go which files to update. Every item in the array may contain the following properties: + +- `sourcePath`: A path, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. +- `filter`: A string to use for filtering in the specified source path. _Example_: `*.ps1`. +- `destinationPath`: A path, relative to repository that is being updated, where the files should be placed. _Example_: `src/templateScripts`. +- `perProject`: A boolean that indicates whether the matched files should be propagated for all available AL-Go projects. In that case, `destinationPath` is relative to the project folder. _Example_: `.AL-Go/scripts`. + +`filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (ignore) during the update. Every item in the array may contain the following properties: + +- `sourcePath`: A path, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. +- `filter`: A string to use for filtering in the specified source path. _Example_: `notRelevantScript.ps1`. + ## Forking AL-Go for GitHub and making your "own" **public** version Using a fork of AL-Go for GitHub to have your "own" public version of AL-Go for GitHub gives you the maximum customization capabilities. It does however also come with the most work. diff --git a/Scenarios/settings.md b/Scenarios/settings.md index 48ea6ab0c..d84d76799 100644 --- a/Scenarios/settings.md +++ b/Scenarios/settings.md @@ -232,6 +232,7 @@ Please read the release notes carefully when installing new versions of AL-Go fo | BcContainerHelperVersion | This setting can be set to a specific version (ex. 3.0.8) of BcContainerHelper to force AL-Go to use this version. **latest** means that AL-Go will use the latest released version. **preview** means that AL-Go will use the latest preview version. **dev** means that AL-Go will use the dev branch of containerhelper. | latest (or preview for AL-Go preview) | | unusedALGoSystemFiles | An array of AL-Go System Files, which won't be updated during Update AL-Go System Files. They will instead be removed.
Use this setting with care, as this can break the AL-Go for GitHub functionality and potentially leave your repo no longer functional. | [ ] | | reportSuppressedDiagnostics | If this setting is set to true, the AL compiler will report diagnostics which are suppressed in the code using the pragma `#pragma warning disable `. This can be useful if you want to ensure that no warnings are suppressed in your code. | false | +| customALGoFiles | An object to configure custom AL-Go files, that will be updated during "Update AL-Go System Files" workflow. The object can contain properties `filesToUpdate` and `fileToExclude`. Read more at [Customizing AL-Go](CustomizingALGoForGitHub.md#Using-custom-template-files). | `{ "filesToUpdate": [], "filesToExclude": [] }` ## Overwrite settings From 432ea51162e5da9bbdfdc8dc9ec1844e100f6725 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 30 Oct 2025 23:11:22 +0100 Subject: [PATCH 48/92] Fix relative paths in e2e test --- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index 8a87a39d1..57f1b521b 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -144,7 +144,8 @@ $cicdYaml.AddCustomJobsToYaml($customJobs, [CustomizationOrigin]::FinalRepositor $cicdYaml.Save($cicdWorkflow) # Add a custom workflow file in the template repository (to be copied to the final repository, as workflow files are always propagated) -$customWorkflowFile = Join-Path $templateRepoPath '.github/workflows/CustomWorkflow.yaml' +$customWorkflowfileRelativePath = '.github/workflows/CustomWorkflow.yaml' +$customWorkflowFile = Join-Path $templateRepoPath $customWorkflowfileRelativePath $customWorkflowContent = @" name: Custom Workflow @@ -256,14 +257,14 @@ Pull (Join-Path (Get-Location) $CustomTemplateProjectSettingsFile) | Should -Exist # Check that custom workflow file is present -(Join-Path (Get-Location) $customWorkflowFile) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customWorkflowFile)) | Should -Be $customWorkflowContent +(Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Exist +(Get-Content -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath)) | Should -Be $customWorkflowContent # Check that custom file is NOT present -(Join-Path (Get-Location) $customFile) | Should -Not -Exist # Custom file should not be copied by default +(Join-Path (Get-Location) $customFileName) | Should -Not -Exist # Custom file should not be copied by default # Add custom file to be copied via settings -$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "customALGoFiles" = @{ "filesToUpdate" = @( @{ "filter" = $customFile } ) } } +$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "customALGoFiles" = @{ "filesToUpdate" = @( @{ "filter" = $customFileName } ) } } # Push CommitAndPush -commitMessage 'Add custom file to be updated when updating AL-Go system files [skip ci]' @@ -275,8 +276,8 @@ RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $templateRepository -g Pull # Check that custom file is now present -(Join-Path (Get-Location) $customFile) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customFile)) | Should -Be $customFileContent +(Join-Path (Get-Location) $customFileName) | Should -Exist +(Get-Content -Path (Join-Path (Get-Location) $customFileName)) | Should -Be $customFileContent # Run CICD $run = RunCICD -repository $repository -branch $branch -wait From 0a03c23731ebf818e7922090195af1483e575d81 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova <43066499+mazhelez@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:59:16 +0100 Subject: [PATCH 49/92] Update Scenarios/CustomizingALGoForGitHub.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Scenarios/CustomizingALGoForGitHub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index 3dd85cdf9..e2d024114 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -203,7 +203,7 @@ Repositories based on your custom template will notify you that changes are avai ## Using custom template files -When using custom template repositories, often you need to add custom files that are related to AL-Go for GitHub, but are not part of the official AL-Go templates. Such files can be script overrides for certain AL-Go functionality, workflows that complement AL-Go capabilities or workflows that easier to manage centrally. +When using custom template repositories, often you need to add custom files that are related to AL-Go for GitHub, but are not part of the official AL-Go templates. Such files can be script overrides for certain AL-Go functionality, workflows that complement AL-Go capabilities or workflows that are easier to manage centrally. In order to instruct AL-Go to sync such files, you need to define setting `customALGoFiles`. The setting is an object that can contain two properties: `filesToUpdate` and `filesToExclude`. From bc92fbb994243573655c2c19542281b7ac1e2ad4 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova <43066499+mazhelez@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:59:23 +0100 Subject: [PATCH 50/92] Update Scenarios/settings.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Scenarios/settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scenarios/settings.md b/Scenarios/settings.md index d84d76799..dfde1051e 100644 --- a/Scenarios/settings.md +++ b/Scenarios/settings.md @@ -232,7 +232,7 @@ Please read the release notes carefully when installing new versions of AL-Go fo | BcContainerHelperVersion | This setting can be set to a specific version (ex. 3.0.8) of BcContainerHelper to force AL-Go to use this version. **latest** means that AL-Go will use the latest released version. **preview** means that AL-Go will use the latest preview version. **dev** means that AL-Go will use the dev branch of containerhelper. | latest (or preview for AL-Go preview) | | unusedALGoSystemFiles | An array of AL-Go System Files, which won't be updated during Update AL-Go System Files. They will instead be removed.
Use this setting with care, as this can break the AL-Go for GitHub functionality and potentially leave your repo no longer functional. | [ ] | | reportSuppressedDiagnostics | If this setting is set to true, the AL compiler will report diagnostics which are suppressed in the code using the pragma `#pragma warning disable `. This can be useful if you want to ensure that no warnings are suppressed in your code. | false | -| customALGoFiles | An object to configure custom AL-Go files, that will be updated during "Update AL-Go System Files" workflow. The object can contain properties `filesToUpdate` and `fileToExclude`. Read more at [Customizing AL-Go](CustomizingALGoForGitHub.md#Using-custom-template-files). | `{ "filesToUpdate": [], "filesToExclude": [] }` +| customALGoFiles | An object to configure custom AL-Go files, that will be updated during "Update AL-Go System Files" workflow. The object can contain properties `filesToUpdate` and `filesToExclude`. Read more at [Customizing AL-Go](CustomizingALGoForGitHub.md#Using-custom-template-files). | `{ "filesToUpdate": [], "filesToExclude": [] }` ## Overwrite settings From cda01b5d57e8bb3176ff5e036273e0ebaf474260 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 12 Nov 2025 10:20:40 +0100 Subject: [PATCH 51/92] Move workflow-specific lines --- Actions/CheckForUpdates/CheckForUpdates.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 33158e0cc..99c27e223 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -150,6 +150,8 @@ foreach($fileToUpdate in $filesToUpdate) { "workflow" { # For workflow files, we might need to modify the file based on the settings $srcContent = GetWorkflowContentWithChangesFromSettings -srcFile $originalSrcPath -repoSettings $repoSettings -depth $depth + # Replace static placeholders + $srcContent = $srcContent.Replace('{TEMPLATEURL}', $templateUrl) } "settings" { # For settings files, we need to modify the file based on the settings @@ -160,8 +162,6 @@ foreach($fileToUpdate in $filesToUpdate) { $srcContent = Get-ContentLF -Path $originalSrcPath } } - # Replace static placeholders - $srcContent = $srcContent.Replace('{TEMPLATEURL}', $templateUrl) if ($isDirectALGo) { # If we are using direct AL-Go repo, we need to change the owner to the templateOwner, the repo names to AL-Go and AL-Go/Actions and the branch to templateBranch From 34006c77cf36b3dda369c2e07b9b72632236e750 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 17 Nov 2025 11:29:03 +0100 Subject: [PATCH 52/92] Refactor ResolveFilePaths function to improve uniqueness logic and update test cases for clarity --- .../CheckForUpdates.HelperFunctions.ps1 | 10 +- Tests/CheckForUpdates.Action.Test.ps1 | 115 ++++++------------ 2 files changed, 40 insertions(+), 85 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 5787abee6..9d42a56de 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -679,7 +679,7 @@ function ResolveFilePaths { } } # Remove duplicates - $fullFilePaths = $fullFilePaths | Sort-Object { $_.sourceFullPath}, { $_.originalSourceFullPath}, { $_.destinationFullPath}, { $_.type } -Unique + $fullFilePaths = $fullFilePaths | Sort-Object { $_.destinationFullPath } -Unique return $fullFilePaths } @@ -733,20 +733,22 @@ function GetDefaultFilesToExclude { <# .SYNOPSIS - Get the list of files to update and exclude based on settings + Get the list of files from the template repository to update and exclude based on the provided settings. .DESCRIPTION This function gets the list of files to update and exclude based on the provided settings. The unusedALGoSystemFiles setting is also applied to exclude files from the update list and add them to the exclude list. .PARAMETER settings The settings object containing the customALGoFiles configuration. .PARAMETER baseFolder - The base folder of the repository. + The base folder of the repository. This is the target folder where the files will be updated. .PARAMETER templateFolder The folder where the template files are located. .PARAMETER originalTemplateFolder - The folder where the original template files are located (if any). In case of custom templates. + The folder where the original template files are located (if any). + If originalTemplateFolder is provided, it means that there is a custom template in use and custom template files should be included. .PARAMETER projects The list of projects in the repository. + The projects are used to resolve per-project files. .OUTPUTS An array containing two elements: the list of files to update and the list of files to exclude. #> diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 5c65be8f4..73e5826bd 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -408,54 +408,6 @@ Describe "ResolveFilePaths" { $fullFilePaths[2].type | Should -Be "markdown" } - It 'ResolveFilePaths returns unique file paths based on the file type' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $PSScriptRoot $destinationPath - $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "text"; } - @{ "sourcePath" = "folder"; "filter" = "*"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "unknown"; } - ) - - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 6 - - # File1.txt is matched twice, so it should appear twice with different types - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") - $fullFilePaths[0].originalSourceFullPath | Should -Be $null - $fullFilePaths[0].type | Should -Be "text" - - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") - $fullFilePaths[1].originalSourceFullPath | Should -Be $null - $fullFilePaths[1].type | Should -Be "unknown" - - # File2.log is matched only once, so it should appear only once with type "unknown" - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") - $fullFilePaths[2].originalSourceFullPath | Should -Be $null - $fullFilePaths[2].type | Should -Be "unknown" - - # File3.txt is matched twice, so it should appear twice with different types - $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - $fullFilePaths[3].originalSourceFullPath | Should -Be $null - $fullFilePaths[3].type | Should -Be "text" - - $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - $fullFilePaths[4].originalSourceFullPath | Should -Be $null - $fullFilePaths[4].type | Should -Be "unknown" - - # File4.md is matched only once, so it should appear only once with type "unknown" - $fullFilePaths[5].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[5].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") - $fullFilePaths[5].originalSourceFullPath | Should -Be $null - $fullFilePaths[5].type | Should -Be "unknown" - } - It 'ResolveFilePaths populates the originalSourceFullPath property only if the origin is not a custom template' { $destinationPath = "destinationPath" $destinationFolder = Join-Path $PSScriptRoot $destinationPath @@ -498,17 +450,18 @@ Describe "ResolveFilePaths" { $fullFilePaths | Should -Not -BeNullOrEmpty $fullFilePaths.Count | Should -Be 3 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File1.txt") - $fullFilePaths[0].type | Should -Be "text" - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File3.txt") + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[0].type | Should -Be "markdown" + + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File1.txt") $fullFilePaths[1].type | Should -Be "text" - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") - $fullFilePaths[2].type | Should -Be "markdown" + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File3.txt") + $fullFilePaths[2].type | Should -Be "text" } It 'ResolveFilePaths with multiple projects' { @@ -524,28 +477,28 @@ Describe "ResolveFilePaths" { $fullFilePaths | Should -Not -BeNullOrEmpty $fullFilePaths.Count | Should -Be 5 - # ProjectA files - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") - $fullFilePaths[0].type | Should -Be "text" + # Non-per-project file + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[0].type | Should -Be "markdown" + # ProjectA files $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") $fullFilePaths[1].type | Should -Be "text" - # ProjectB files $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") $fullFilePaths[2].type | Should -Be "text" - $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File3.txt") + # ProjectB files + $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File1.txt") $fullFilePaths[3].type | Should -Be "text" - # Non-per-project file - $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") - $fullFilePaths[4].type | Should -Be "markdown" + $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File3.txt") + $fullFilePaths[4].type | Should -Be "text" } } @@ -964,16 +917,17 @@ Describe 'GetFilesToUpdate (real template)' { # Check repo settings files $repoSettingsFiles = $filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".github/AL-Go-Settings.json") } + $repoSettingsFiles | Should -Not -BeNullOrEmpty $repoSettingsFiles.Count | Should -Be 2 - $repoSettingsFiles[0].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null - $repoSettingsFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') - $repoSettingsFiles[0].type | Should -Be '' + $repoSettingsFiles[0].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".github/AL-Go-Settings.json") + $repoSettingsFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-Settings.json') + $repoSettingsFiles[0].type | Should -Be 'settings' - $repoSettingsFiles[1].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".github/AL-Go-Settings.json") - $repoSettingsFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-Settings.json') - $repoSettingsFiles[1].type | Should -Be 'settings' + $repoSettingsFiles[1].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null + $repoSettingsFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') + $repoSettingsFiles[1].type | Should -Be '' # Check project settings files $projectSettingsFilesFromCustomTemplate = @($filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".AL-Go/settings.json") }) @@ -981,14 +935,13 @@ Describe 'GetFilesToUpdate (real template)' { $projectSettingsFilesFromCustomTemplate | Should -Not -BeNullOrEmpty $projectSettingsFilesFromCustomTemplate.Count | Should -Be 2 - $projectSettingsFilesFromCustomTemplate[0].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null - $projectSettingsFilesFromCustomTemplate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateProjectSettings.doNotEdit.json') - $projectSettingsFilesFromCustomTemplate[0].type | Should -Be '' - + $projectSettingsFilesFromCustomTemplate[0].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".AL-Go/settings.json") + $projectSettingsFilesFromCustomTemplate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.AL-Go/settings.json') + $projectSettingsFilesFromCustomTemplate[0].type | Should -Be 'settings' - $projectSettingsFilesFromCustomTemplate[1].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".AL-Go/settings.json") - $projectSettingsFilesFromCustomTemplate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.AL-Go/settings.json') - $projectSettingsFilesFromCustomTemplate[1].type | Should -Be 'settings' + $projectSettingsFilesFromCustomTemplate[1].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null + $projectSettingsFilesFromCustomTemplate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateProjectSettings.doNotEdit.json') + $projectSettingsFilesFromCustomTemplate[1].type | Should -Be '' # No files to exclude $filesToExclude | Should -BeNullOrEmpty From 6f07b7df521c3fa9908f4a6027182d4df40bd172 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 17 Nov 2025 13:34:58 +0100 Subject: [PATCH 53/92] Update documentation for custom template files to clarify file syncing behavior and exclusion settings --- Scenarios/CustomizingALGoForGitHub.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index e2d024114..b66fcaea6 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -203,9 +203,9 @@ Repositories based on your custom template will notify you that changes are avai ## Using custom template files -When using custom template repositories, often you need to add custom files that are related to AL-Go for GitHub, but are not part of the official AL-Go templates. Such files can be script overrides for certain AL-Go functionality, workflows that complement AL-Go capabilities or workflows that are easier to manage centrally. +When updating AL-Go for GitHub, only specific system files from the template repository are synced to your end repository by default. Files such as `README.md`, `.gitignore`, and other documentation or non-system files are not updated by AL-Go for GitHub. By default, AL-Go syncs workflow files in `.github/workflows`, PowerShell scripts in `.github` and `.AL-Go`, and configuration files required for AL-Go operations. When using custom template repositories, you may need to add additional files related to AL-Go for GitHub, such as script overrides, complementary workflows, or centrally managed files not part of the official AL-Go templates. -In order to instruct AL-Go to sync such files, you need to define setting `customALGoFiles`. The setting is an object that can contain two properties: `filesToUpdate` and `filesToExclude`. +In order to instruct AL-Go which files to look for at the template repository, you need to define the `customALGoFiles` setting. The setting is an object that can contain two properties: `filesToUpdate` and `filesToExclude`. `filesToUpdate`, as the name suggests, is an array of file configurations that will instruct AL-Go which files to update. Every item in the array may contain the following properties: @@ -214,11 +214,21 @@ In order to instruct AL-Go to sync such files, you need to define setting `custo - `destinationPath`: A path, relative to repository that is being updated, where the files should be placed. _Example_: `src/templateScripts`. - `perProject`: A boolean that indicates whether the matched files should be propagated for all available AL-Go projects. In that case, `destinationPath` is relative to the project folder. _Example_: `.AL-Go/scripts`. -`filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (ignore) during the update. Every item in the array may contain the following properties: +> [!NOTE] +> `filesToUpdate` is used to define all the template files that will be used by AL-Go for GitHub. If a template file is not matched, it will be ignored. Please pay attention, when changing the file configurations: there might be template files that were previously propagated to your repositories. In case these files are no longer matched via `filesToUpdate`, AL-Go for GitHub will ignore them and you might have to remove them manually. + +`filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (remove) during the update. Every item in the array may contain the following properties: - `sourcePath`: A path, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. - `filter`: A string to use for filtering in the specified source path. _Example_: `notRelevantScript.ps1`. +> [!NOTE] `filesToExclude` is an array containing a subset of files from `filesToUpdate`. These files are specifically marked to be excluded from the update process. During the AL-Go update: +> - Any file matched in `filesToExclude` will not be updated. +> - If a file matched by `filesToExclude` exists in the destination (end) repository, it will be removed as part of the update. +> +> This mechanism allows for fine-grained control over which files are propagated to the end repository and which should be explicitly removed, ensuring that unwanted files are not carried forward during updates. + + ## Forking AL-Go for GitHub and making your "own" **public** version Using a fork of AL-Go for GitHub to have your "own" public version of AL-Go for GitHub gives you the maximum customization capabilities. It does however also come with the most work. From 0cc9e6f2a5be2710d9c25fdd34ba95340bc6e63f Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 17 Nov 2025 16:14:05 +0100 Subject: [PATCH 54/92] Fix e2e test --- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index 57f1b521b..1b88657ac 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -258,7 +258,7 @@ Pull # Check that custom workflow file is present (Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath)) | Should -Be $customWorkflowContent +(Get-Content -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) -Raw) | Should -Be $customWorkflowContent # Check that custom file is NOT present (Join-Path (Get-Location) $customFileName) | Should -Not -Exist # Custom file should not be copied by default @@ -277,7 +277,7 @@ Pull # Check that custom file is now present (Join-Path (Get-Location) $customFileName) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customFileName)) | Should -Be $customFileContent +(Get-Content -Path (Join-Path (Get-Location) $customFileName) -Raw) | Should -Be $customFileContent # Run CICD $run = RunCICD -repository $repository -branch $branch -wait From 646f103480ad85b9a8638587c497b1448472b8ad Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 13:08:15 +0100 Subject: [PATCH 55/92] Fix line endings in custom workflow and file content checks in runtest.ps1 --- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index 1b88657ac..2b4c84b01 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -258,7 +258,7 @@ Pull # Check that custom workflow file is present (Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) -Raw) | Should -Be $customWorkflowContent +(Get-Content -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) -Raw) | Should -Be $customWorkflowContent.Replace("`r", "").TrimEnd("`n") # Check that custom file is NOT present (Join-Path (Get-Location) $customFileName) | Should -Not -Exist # Custom file should not be copied by default @@ -277,7 +277,7 @@ Pull # Check that custom file is now present (Join-Path (Get-Location) $customFileName) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customFileName) -Raw) | Should -Be $customFileContent +(Get-Content -Path (Join-Path (Get-Location) $customFileName)) | Should -Be $customFileContent.Replace("`r", "").TrimEnd("`n") # Run CICD $run = RunCICD -repository $repository -branch $branch -wait From 8cd4177c38ce91ae0cb8357c1e7331c542008f0d Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 13:10:23 +0100 Subject: [PATCH 56/92] Simplify destination name assignment in ResolveFilePaths function --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 9d42a56de..451066f32 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -642,7 +642,7 @@ function ResolveFilePaths { } } - $destinationName = $(Split-Path -Path $srcFile -Leaf) + $destinationName = Split-Path -Path $srcFile -Leaf if($file.Keys -contains 'destinationName' -and ($file.destinationName)) { $destinationName = $file.destinationName } From 5680d21d2d7c0d7665875c289b38638dc10577d4 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 13:11:54 +0100 Subject: [PATCH 57/92] Refactor ResolveFilePaths function to improve variable naming and clarity in file path assignments --- .../CheckForUpdates.HelperFunctions.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 451066f32..a68660393 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -656,14 +656,14 @@ function ResolveFilePaths { $project = '' # If project is '.', it means the root folder, so we use an empty string } - $projectFile = $fullFilePath.Clone() + $fullProjectFilePath = $fullFilePath.Clone() - $projectFile.destinationFullPath = Join-Path $destinationFolder $project - $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $file.destinationPath - $projectFile.destinationFullPath = Join-Path $projectFile.destinationFullPath $destinationName + $fullProjectFilePath.destinationFullPath = Join-Path $destinationFolder $project + $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $file.destinationPath + $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $destinationName - Write-Host "Adding per-project file for project '$project': sourceFullPath '$($projectFile.sourceFullPath)', originalSourceFullPath '$($projectFile.originalSourceFullPath)', destinationFullPath '$($projectFile.destinationFullPath)'" - $fullFilePaths += $projectFile + Write-Host "Adding per-project file for project '$project': sourceFullPath '$($fullProjectFilePath.sourceFullPath)', originalSourceFullPath '$($fullProjectFilePath.originalSourceFullPath)', destinationFullPath '$($fullProjectFilePath.destinationFullPath)'" + $fullFilePaths += $fullProjectFilePath } } else { From a1002724d8ac1e1252da683663a3704585c65bcc Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 13:21:54 +0100 Subject: [PATCH 58/92] Rename 'sourcePath' and 'destinationPath' to 'sourceFolder' and 'destinationFolder' for consistency across schema and helper functions --- Actions/.Modules/settings.schema.json | 6 +- .../CheckForUpdates.HelperFunctions.ps1 | 56 ++++++++-------- Scenarios/CustomizingALGoForGitHub.md | 8 +-- Tests/CheckForUpdates.Action.Test.ps1 | 66 +++++++++---------- 4 files changed, 68 insertions(+), 68 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index a5e393872..a7c2c7b22 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -703,10 +703,10 @@ "items": { "type": "object", "properties": { - "destinationPath": { + "destinationFolder": { "type": "string" }, - "sourcePath": { + "sourceFolder": { "type": "string" }, "filter": { @@ -723,7 +723,7 @@ "items": { "type": "object", "properties": { - "sourcePath": { + "sourceFolder": { "type": "string" }, "filter": { diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index a68660393..f100b7a05 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -579,8 +579,8 @@ function ResolveFilePaths { $fullFilePaths = @() foreach($file in $files) { - if($file.Keys -notcontains 'sourcePath') { - $file.sourcePath = '' # Default to current folder + if($file.Keys -notcontains 'sourceFolder') { + $file.sourceFolder = '' # Default to current folder } if($file.Keys -notcontains 'filter') { @@ -595,9 +595,9 @@ function ResolveFilePaths { $file.type = '' # Default to empty } - if($file.Keys -notcontains 'destinationPath') { - # If destinationPath is not specified, use the sourcePath, so that the file structure is preserved - $file.destinationPath = $file.sourcePath + if($file.Keys -notcontains 'destinationFolder') { + # If destinationFolder is not specified, use the sourceFolder, so that the file structure is preserved + $file.destinationFolder = $file.sourceFolder } if($file.Keys -notcontains 'perProject') { @@ -606,18 +606,18 @@ function ResolveFilePaths { # If originalSourceFolder is not specified, it means there is no custom template, so skip custom template files if(!$originalSourceFolder -and $file.origin -eq 'custom template') { - Write-Host "Skipping custom template file(s) with sourcePath '$($file.sourcePath)' as there is no original source folder specified" + Write-Host "Skipping custom template file(s) with source folder '$($file.sourceFolder)' as there is no original source folder specified" continue; } # All files are relative to the template folder - Write-Host "Resolving files for sourcePath '$($file.sourcePath)' and filter '$($file.filter)'" - $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourcePath) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) + Write-Host "Resolving files for source folder '$($file.sourceFolder)' and filter '$($file.filter)'" + $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourceFolder) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) - Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder', origin '$($file.origin)')" + Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder', origin '$($file.origin)')" if(-not $sourceFiles) { - Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourcePath)' (relative to folder '$sourceFolder')" + Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder')" continue } @@ -649,7 +649,7 @@ function ResolveFilePaths { if($file.Keys -contains 'perProject' -and $file.perProject -eq $true) { # Multiple file entries, one for each project - # Destination full path is the destination base folder + project + destinationPath + destinationName + # Destination full path is the destination base folder + project + destinationFolder + destinationName foreach($project in $projects) { if($project -eq '.') { @@ -659,7 +659,7 @@ function ResolveFilePaths { $fullProjectFilePath = $fullFilePath.Clone() $fullProjectFilePath.destinationFullPath = Join-Path $destinationFolder $project - $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $file.destinationPath + $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $file.destinationFolder $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $destinationName Write-Host "Adding per-project file for project '$project': sourceFullPath '$($fullProjectFilePath.sourceFullPath)', originalSourceFullPath '$($fullProjectFilePath.originalSourceFullPath)', destinationFullPath '$($fullProjectFilePath.destinationFullPath)'" @@ -668,9 +668,9 @@ function ResolveFilePaths { } else { # Single file entry - # Destination full path is the destination base folder + destinationPath + destinationName + # Destination full path is the destination base folder + destinationFolder + destinationName - $fullFilePath.destinationFullPath = Join-Path $destinationFolder $file.destinationPath + $fullFilePath.destinationFullPath = Join-Path $destinationFolder $file.destinationFolder $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $destinationName Write-Host "Adding file: sourceFullPath '$($fullFilePath.sourceFullPath)', originalSourceFullPath '$($fullFilePath.originalSourceFullPath)', destinationFullPath '$($fullFilePath.destinationFullPath)'" @@ -690,22 +690,22 @@ function GetDefaultFilesToUpdate { ) $filesToUpdate = @( - [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*.yaml'; 'type' = 'workflow' } - [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '*.yml'; 'type' = 'workflow' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.copy.md' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.ps1' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = "$RepoSettingsFileName"; 'type' = 'settings' } - [ordered]@{ 'sourcePath' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } - - [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = '*.ps1'; 'perProject' = $true }, - [ordered]@{ 'sourcePath' = '.AL-Go'; 'filter' = "$ALGoSettingsFileName"; 'perProject' = $true; 'type' = 'settings' } + [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = '*.yaml'; 'type' = 'workflow' } + [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = '*.yml'; 'type' = 'workflow' } + [ordered]@{ 'sourceFolder' = '.github'; 'filter' = '*.copy.md' } + [ordered]@{ 'sourceFolder' = '.github'; 'filter' = '*.ps1' } + [ordered]@{ 'sourceFolder' = '.github'; 'filter' = "$RepoSettingsFileName"; 'type' = 'settings' } + [ordered]@{ 'sourceFolder' = '.github'; 'filter' = '*.settings.json'; 'type' = 'settings' } + + [ordered]@{ 'sourceFolder' = '.AL-Go'; 'filter' = '*.ps1'; 'perProject' = $true }, + [ordered]@{ 'sourceFolder' = '.AL-Go'; 'filter' = "$ALGoSettingsFileName"; 'perProject' = $true; 'type' = 'settings' } ) if($includeCustomTemplateFiles) { # If there is an original template folder, we also need to include custom template files (RepoSettings and ProjectSettings) $filesToUpdate += @( - [ordered]@{ 'sourcePath' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'origin' = 'custom template' } - [ordered]@{ 'sourcePath' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationPath' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'origin' = 'custom template' } + [ordered]@{ 'sourceFolder' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'origin' = 'custom template' } + [ordered]@{ 'sourceFolder' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationFolder' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'origin' = 'custom template' } ) } @@ -722,9 +722,9 @@ function GetDefaultFilesToExclude { if (!$includeBuildPP) { # Remove PowerPlatform workflows if no PowerPlatformSolution exists $filesToExclude += @( - [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = '_BuildPowerPlatformSolution.yaml' } - [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = 'PushPowerPlatformChanges.yaml' } - [ordered]@{ 'sourcePath' = '.github/workflows'; 'filter' = 'PullPowerPlatformChanges.yaml' } + [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = '_BuildPowerPlatformSolution.yaml' } + [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = 'PushPowerPlatformChanges.yaml' } + [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = 'PullPowerPlatformChanges.yaml' } ) } diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index b66fcaea6..35e657f60 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -209,17 +209,17 @@ In order to instruct AL-Go which files to look for at the template repository, y `filesToUpdate`, as the name suggests, is an array of file configurations that will instruct AL-Go which files to update. Every item in the array may contain the following properties: -- `sourcePath`: A path, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. +- `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. - `filter`: A string to use for filtering in the specified source path. _Example_: `*.ps1`. -- `destinationPath`: A path, relative to repository that is being updated, where the files should be placed. _Example_: `src/templateScripts`. -- `perProject`: A boolean that indicates whether the matched files should be propagated for all available AL-Go projects. In that case, `destinationPath` is relative to the project folder. _Example_: `.AL-Go/scripts`. +- `destinationFolder`: A path to a folder, relative to repository that is being updated, where the files should be placed. _Example_: `src/templateScripts`. +- `perProject`: A boolean that indicates whether the matched files should be propagated for all available AL-Go projects. In that case, `destinationFolder` is relative to the project folder. _Example_: `.AL-Go/scripts`. > [!NOTE] > `filesToUpdate` is used to define all the template files that will be used by AL-Go for GitHub. If a template file is not matched, it will be ignored. Please pay attention, when changing the file configurations: there might be template files that were previously propagated to your repositories. In case these files are no longer matched via `filesToUpdate`, AL-Go for GitHub will ignore them and you might have to remove them manually. `filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (remove) during the update. Every item in the array may contain the following properties: -- `sourcePath`: A path, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. +- `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. - `filter`: A string to use for filtering in the specified source path. _Example_: `notRelevantScript.ps1`. > [!NOTE] `filesToExclude` is an array containing a subset of files from `filesToUpdate`. These files are specifically marked to be excluded from the update process. During the AL-Go update: diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 73e5826bd..ac181a12b 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -277,7 +277,7 @@ Describe "ResolveFilePaths" { $rootFolder = $PSScriptRoot - $sourceFolder = Join-Path $rootFolder "sourcePath" + $sourceFolder = Join-Path $rootFolder "sourceFolder" if (-not (Test-Path $sourceFolder)) { New-Item -Path $sourceFolder -ItemType Directory | Out-Null } @@ -315,11 +315,11 @@ Describe "ResolveFilePaths" { } It 'ResolveFilePaths with specific files extensions' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $rootFolder $destinationPath + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $rootFolder $destinationFolder $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = 'newFolder'; "destinationName" = '' } - @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = 'newFolder'; "destinationName" = '' } + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = 'newFolder'; "destinationName" = '' } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = 'newFolder'; "destinationName" = '' } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder @@ -338,11 +338,11 @@ Describe "ResolveFilePaths" { } It 'ResolveFilePaths with specific destination names' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $rootFolder $destinationPath + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $rootFolder $destinationFolder $files = @( - @{ "sourcePath" = "folder"; "filter" = "File1.txt"; "destinationPath" = 'newFolder'; "destinationName" = "CustomFile1.txt" } - @{ "sourcePath" = "folder"; "filter" = "File2.log"; "destinationPath" = 'newFolder'; "destinationName" = "CustomFile2.log" } + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationFolder" = 'newFolder'; "destinationName" = "CustomFile1.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File2.log"; "destinationFolder" = 'newFolder'; "destinationName" = "CustomFile2.log" } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder @@ -358,11 +358,11 @@ Describe "ResolveFilePaths" { } It 'ResolveFilePaths with type' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "folder"; "destinationName" = ''; type = "text" } - @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "folder"; "destinationName" = ''; type = "markdown" } + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "folder"; "destinationName" = ''; type = "text" } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = "folder"; "destinationName" = ''; type = "markdown" } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder @@ -381,11 +381,11 @@ Describe "ResolveFilePaths" { } It 'ResolveFilePaths with original source folder' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "text" } - @{ "sourcePath" = "folder"; "filter" = "*.md"; "destinationPath" = "newFolder"; "destinationName" = ''; type = "markdown" } + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "newFolder"; "destinationName" = ''; type = "text" } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = "newFolder"; "destinationName" = ''; type = "markdown" } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder @@ -409,11 +409,11 @@ Describe "ResolveFilePaths" { } It 'ResolveFilePaths populates the originalSourceFullPath property only if the origin is not a custom template' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; "destinationPath" = "newFolder"; "destinationName" = ''; "origin" = "custom template"; } - @{ "sourcePath" = "folder"; "filter" = "*.log"; "destinationPath" = "newFolder"; "destinationName" = ''; } + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "newFolder"; "destinationName" = ''; "origin" = "custom template"; } + @{ "sourceFolder" = "folder"; "filter" = "*.log"; "destinationFolder" = "newFolder"; "destinationName" = ''; } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder @@ -439,11 +439,11 @@ Describe "ResolveFilePaths" { } It 'ResolveFilePaths with a single project' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } - @{ "sourcePath" = "folder"; "filter" = "*.md"; type = "markdown"; } + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; type = "markdown"; } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("SomeProject") @@ -465,11 +465,11 @@ Describe "ResolveFilePaths" { } It 'ResolveFilePaths with multiple projects' { - $destinationPath = "destinationPath" - $destinationFolder = Join-Path $PSScriptRoot $destinationPath + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder $files = @( - @{ "sourcePath" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } - @{ "sourcePath" = "folder"; "filter" = "*.md"; type = "markdown"; } + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; type = "markdown"; } ) $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA", "ProjectB") @@ -623,12 +623,12 @@ Describe "GetFilesToUpdate (general files to update logic)" { $filesToExclude | Should -BeNullOrEmpty } - It 'Returns the correct files with destinationPath' { + It 'Returns the correct files with destinationFolder' { $settings = @{ type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) + filesToUpdate = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) filesToExclude = @() } } @@ -649,7 +649,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; destinationPath = "customFolder" }) + filesToUpdate = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) filesToExclude = @(@{ filter = "test2.txt" }) } } @@ -692,7 +692,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "test.ps1"; destinationPath = 'dstPath'; destinationName = "renamed.txt" }) + filesToUpdate = @(@{ filter = "test.ps1"; destinationFolder = 'dstPath'; destinationName = "renamed.txt" }) filesToExclude = @() } } From 512cb4a843cbc121540946158c7686ebdc0bd768 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 13:33:26 +0100 Subject: [PATCH 59/92] Enhance documentation for ResolveFilePaths function with detailed SYNOPSIS, DESCRIPTION, and PARAMETER sections --- .../CheckForUpdates.HelperFunctions.ps1 | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index f100b7a05..b7a31d863 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -562,6 +562,46 @@ function UpdateSettingsFile { return $modified } +<# +.SYNOPSIS +Resolves file paths based on the provided source folder, destination folder, and file specifications. + +.DESCRIPTION +This function takes a source folder, an optional original source folder, a destination folder, and an array of file specifications. It resolves the full paths for each specified file, considering their origin (template or custom template), type, and whether they are per-project files. +The function returns an array of hashtables containing the resolved source and destination file paths. +The function is used to determine which files need to be copied from the template repository to the target repository during the AL-Go update process. +sourceFolder: The base folder of the template used to resolve the source file paths. +originalSourceFolder: The base folder of the original template used to check for original files (can be $null). This is in the case of custom templates, where if the file exists in the original template, it should be used instead of the custom template file. +destinationFolder: The base folder used to construct the destination file paths. This is typically the root folder of the target repository. + +.PARAMETER sourceFolder +The base folder where the source files are located. + +.PARAMETER originalSourceFolder +The original source folder to check for original files (can be $null). All files of origin 'custom template' are skipped if this parameter is $null. + +.PARAMETER destinationFolder +The base folder used to construct the destination file paths. + +.PARAMETER files +An array of hashtables specifying the files to resolve. Each hashtable can contain the following keys: +- sourceFolder: The subfolder within the source folder to search for files (default is current folder). +- filter: The file filter to apply when searching for files (default is all files). +- origin: The origin of the files, either 'template' or 'custom template' (default is 'template'). +- type: The type of the files (default is empty). +- destinationFolder: The subfolder within the destination folder where the files should be placed (default is the same as sourceFolder). +- perProject: A boolean indicating whether the files are per project (default is false). + +.PARAMETER projects +An array of project names used when resolving per-project file paths. + +.OUTPUTS +An array of hashtables, each containing: +- sourceFullPath: The full path to the source file. +- originalSourceFullPath: The full path to the original source file (if found, otherwise $null). +- type: The type of the file. +- destinationFullPath: The full path to the destination file. +#> function ResolveFilePaths { Param( [Parameter(Mandatory=$true)] From f0c108cb82cb205eb00b2149615627acf4b43f81 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 13:49:16 +0100 Subject: [PATCH 60/92] Update deprecation warnings for unusedALGoSystemFiles setting and mark it as deprecated in documentation --- .../CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 3 +-- DEPRECATIONS.md | 8 ++++++++ Scenarios/settings.md | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index b7a31d863..63ccf5061 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -821,14 +821,13 @@ function GetFilesToUpdate { } return $include } - # Apply unusedALGoSystemFiles logic $unusedALGoSystemFiles = $settings.unusedALGoSystemFiles # Exclude unusedALGoSystemFiles from $filesToUpdate and add them to $filesToExclude $unusedFilesToExclude = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -contains (Split-Path -Path $_.sourceFullPath -Leaf) } if ($unusedFilesToExclude) { - # TODO: Trace-DeprecationWarning "The 'unusedALGoSystemFiles' setting is deprecated and will be removed in future versions. Please use 'customALGoFiles.filesToExclude' instead." -DeprecationTag "unusedALGoSystemFiles" + Trace-DeprecationWarning "The 'unusedALGoSystemFiles' setting is deprecated and will be removed in future versions." -DeprecationTag "unusedALGoSystemFiles" Write-Host "The following files are marked as unused and will be removed if they exist:" $unusedFilesToExclude | ForEach-Object { Write-Host "- $($_.destinationFullPath)" } diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index bdf268968..16ed42f2c 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -10,6 +10,14 @@ Old versions of AL-Go for GitHub uses old and unsupported versions of GitHub act When handling support requests, we will request that you to use the latest version of AL-Go for GitHub and in general, fixes will only be made available in a preview version of AL-Go for GitHub and subsequently in the next version released. +## Changes in effect after October 1st 2026 + + + +### Setting `unusedALGoSystemFiles` will no longer be supported + +The recommended approach is to use the [`customALGoFiles.filesToExclude`](http://aka.ms/algosettings#customALGoFiles) setting to specify files from the AL-Go template that should be excluded from your repositories. + ## Changes in effect after October 1st 2025 diff --git a/Scenarios/settings.md b/Scenarios/settings.md index dfde1051e..ad3bd4ada 100644 --- a/Scenarios/settings.md +++ b/Scenarios/settings.md @@ -118,7 +118,7 @@ The repository settings are only read from the repository settings file (.github | keyVaultCodesignCertificateName | When developing AppSource Apps, your app needs to be code signed.
Name of a certificate stored in your KeyVault that can be used to codesigning. To use this setting you will also need enable Azure KeyVault in your AL-Go project. Read [this](UseAzureKeyVault.md) for more information | | | applicationInsightsConnectionStringSecretName | This setting specifies the name (**NOT the secret**) of a secret containing the application insights connection string for the apps. | applicationInsightsConnectionString | | storageContextSecretName | This setting specifies the name (**NOT the secret**) of a secret containing a json string with StorageAccountName, ContainerName, BlobName and StorageAccountKey or SAS Token. If this secret exists, AL-Go will upload builds to this storage account for every successful build.
The BcContainerHelper function New-ALGoStorageContext can create a .json structure with this content. | StorageContext | -| alwaysBuildAllProjects | This setting only makes sense if the repository is setup for multiple projects.
Standard behavior of the CI/CD workflow is to only build the projects, in which files have changes when running the workflow due to a push or a pull request | false | +| alwaysBuildAllProjects (**deprecated**) | This setting only makes sense if the repository is setup for multiple projects.
Standard behavior of the CI/CD workflow is to only build the projects, in which files have changes when running the workflow due to a push or a pull request | false | | fullBuildPatterns | Use this setting to list important files and folders. Changes to any of these files and folders would trigger a full Pull Request build (all AL-Go projects will be built).
*Examples*:
1. Specifying `fullBuildPatterns` as `[ "Build/*" ]` means that any changes from a PR to the `Build` folder would trigger a full build.
2. Specifying `fullBuildPatterns` as `[ "*" ]` means that any changes from a PR would trigger a full build and it is equivalent to setting `alwaysBuildAllProjects` to `true`. | [ ] | | skipUpgrade | This setting is used to signal to the pipeline to NOT run upgrade and ignore previous releases of the app. | false | | cacheImageName | When using self-hosted runners, cacheImageName specifies the prefix for the docker image created for increased performance | my | @@ -230,7 +230,7 @@ Please read the release notes carefully when installing new versions of AL-Go fo | doNotRunBcptTests | This setting forces the pipeline to NOT run the performance tests in testFolders. Performance tests are still being built and published. Note this setting can be set in a [workflow specific settings file](#where-are-the-settings-located) to only apply to that workflow | false | | memoryLimit | Specifies the memory limit for the build container. By default, this is left to BcContainerHelper to handle and will currently be set to 8G | 8G | | BcContainerHelperVersion | This setting can be set to a specific version (ex. 3.0.8) of BcContainerHelper to force AL-Go to use this version. **latest** means that AL-Go will use the latest released version. **preview** means that AL-Go will use the latest preview version. **dev** means that AL-Go will use the dev branch of containerhelper. | latest (or preview for AL-Go preview) | -| unusedALGoSystemFiles | An array of AL-Go System Files, which won't be updated during Update AL-Go System Files. They will instead be removed.
Use this setting with care, as this can break the AL-Go for GitHub functionality and potentially leave your repo no longer functional. | [ ] | +| unusedALGoSystemFiles (**deprecated**) | An array of AL-Go System Files, which won't be updated during Update AL-Go System Files. They will instead be removed.
Use this setting with care, as this can break the AL-Go for GitHub functionality and potentially leave your repo no longer functional. | [ ] | | reportSuppressedDiagnostics | If this setting is set to true, the AL compiler will report diagnostics which are suppressed in the code using the pragma `#pragma warning disable `. This can be useful if you want to ensure that no warnings are suppressed in your code. | false | | customALGoFiles | An object to configure custom AL-Go files, that will be updated during "Update AL-Go System Files" workflow. The object can contain properties `filesToUpdate` and `filesToExclude`. Read more at [Customizing AL-Go](CustomizingALGoForGitHub.md#Using-custom-template-files). | `{ "filesToUpdate": [], "filesToExclude": [] }` From 09a7eed158c980f946487bf9464b25d29273397f Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 13:51:46 +0100 Subject: [PATCH 61/92] Fix precommit issues --- Scenarios/CustomizingALGoForGitHub.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index 35e657f60..247afe44a 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -223,12 +223,12 @@ In order to instruct AL-Go which files to look for at the template repository, y - `filter`: A string to use for filtering in the specified source path. _Example_: `notRelevantScript.ps1`. > [!NOTE] `filesToExclude` is an array containing a subset of files from `filesToUpdate`. These files are specifically marked to be excluded from the update process. During the AL-Go update: +> > - Any file matched in `filesToExclude` will not be updated. > - If a file matched by `filesToExclude` exists in the destination (end) repository, it will be removed as part of the update. > > This mechanism allows for fine-grained control over which files are propagated to the end repository and which should be explicitly removed, ensuring that unwanted files are not carried forward during updates. - ## Forking AL-Go for GitHub and making your "own" **public** version Using a fork of AL-Go for GitHub to have your "own" public version of AL-Go for GitHub gives you the maximum customization capabilities. It does however also come with the most work. From 18786ac6df8d37dfa5b64424e935a99ff6a2688e Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 14:17:58 +0100 Subject: [PATCH 62/92] Fix e2e test --- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index 2b4c84b01..f1f18ca94 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -166,6 +166,11 @@ jobs: "@ Set-Content -Path $customWorkflowFile -Value $customWorkflowContent +if($linux) { + # Modify workflow to run on ubuntu-latest if the test is running on linux. AL-Go will not modify workflow files based on platform, so we need to do it here to ensure the test works correctly. + $customWorkflowContent = $customWorkflowContent -replace 'windows-latest', 'ubuntu-latest' +} + # Add another custom file in the template repository (to be ignored unless specifically added via the settings) $customFileName = 'CustomTemplateFile.txt' $customFile = Join-Path $templateRepoPath $customFileName @@ -258,7 +263,7 @@ Pull # Check that custom workflow file is present (Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) -Raw) | Should -Be $customWorkflowContent.Replace("`r", "").TrimEnd("`n") +Get-ContentLF -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Be $customWorkflowContent.Replace("`r", "").TrimEnd("`n") # Check that custom file is NOT present (Join-Path (Get-Location) $customFileName) | Should -Not -Exist # Custom file should not be copied by default @@ -277,7 +282,7 @@ Pull # Check that custom file is now present (Join-Path (Get-Location) $customFileName) | Should -Exist -(Get-Content -Path (Join-Path (Get-Location) $customFileName)) | Should -Be $customFileContent.Replace("`r", "").TrimEnd("`n") +Get-ContentLF -Path (Join-Path (Get-Location) $customFileName)| Should -Be $customFileContent.Replace("`r", "").TrimEnd("`n") # Run CICD $run = RunCICD -repository $repository -branch $branch -wait From 4133ffe517ee601949c66576c4d2fc45948f7637 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 14:46:50 +0100 Subject: [PATCH 63/92] Add scenariosFilter input to E2E workflow for selective scenario execution --- .github/workflows/E2E.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/E2E.yaml b/.github/workflows/E2E.yaml index 2f0e9430e..c113c3e76 100644 --- a/.github/workflows/E2E.yaml +++ b/.github/workflows/E2E.yaml @@ -25,6 +25,11 @@ on: description: Run the end to end scenario tests type: boolean default: true + scenariosFilter: + description: Filter to run specific scenarios (separated by comma) + required: false + type: string + default: '*' runUpgradeTests: description: Run the end to end upgrade tests type: boolean @@ -169,6 +174,7 @@ jobs: id: Analyze env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + _scenariosFilter: ${{ github.event.inputs.scenariosFilter }} run: | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 $modulePath = Join-Path "." "e2eTests\e2eTestHelper.psm1" -resolve @@ -222,9 +228,13 @@ jobs: Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "releases=$releasesJson" Write-Host "releases=$releasesJson" + $scenariosFilter = "$($env:_scenariosFilter)" -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } + $allScenarios = @(Get-ChildItem -Path (Join-Path $ENV:GITHUB_WORKSPACE "e2eTests/scenarios/*/runtest.ps1") + $filteredScenarios = $allScenarios | Where-Object { $scenariosFilter | ForEach-Object { $_.Directory.Name -like $_ } } + $scenariosJson = @{ "matrix" = @{ - "include" = @(Get-ChildItem -path (Join-Path $ENV:GITHUB_WORKSPACE "e2eTests/scenarios/*/runtest.ps1") | ForEach-Object { @{ "Scenario" = $_.Directory.Name } } ) + "include" = $filteredScenarios | ForEach-Object { @{ "Scenario" = $_.Directory.Name } } }; "max-parallel" = $maxParallel "fail-fast" = $false From 6e501bbc13665d6714bf4a58c7381b1ca7003a1b Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 14:53:53 +0100 Subject: [PATCH 64/92] Fix scenarios filtering --- .github/workflows/E2E.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/E2E.yaml b/.github/workflows/E2E.yaml index c113c3e76..8493a25d0 100644 --- a/.github/workflows/E2E.yaml +++ b/.github/workflows/E2E.yaml @@ -229,12 +229,12 @@ jobs: Write-Host "releases=$releasesJson" $scenariosFilter = "$($env:_scenariosFilter)" -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } - $allScenarios = @(Get-ChildItem -Path (Join-Path $ENV:GITHUB_WORKSPACE "e2eTests/scenarios/*/runtest.ps1") - $filteredScenarios = $allScenarios | Where-Object { $scenariosFilter | ForEach-Object { $_.Directory.Name -like $_ } } + $allScenarios = @(Get-ChildItem -Path (Join-Path $ENV:GITHUB_WORKSPACE "e2eTests/scenarios/*/runtest.ps1") | ForEach-Object { $_.Directory.Name }) + $filteredScenarios = $allScenarios | Where-Object { $scenario = $_; $scenariosFilter | ForEach-Object { $scenario -like $_ } } $scenariosJson = @{ "matrix" = @{ - "include" = $filteredScenarios | ForEach-Object { @{ "Scenario" = $_.Directory.Name } } + "include" = $filteredScenarios | ForEach-Object { @{ "Scenario" = $_ } } }; "max-parallel" = $maxParallel "fail-fast" = $false From a70211519a9f53eacba374c834ad28ffdd560db0 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 15:02:35 +0100 Subject: [PATCH 65/92] Fix array wrapping for scenarios in E2E workflow --- .github/workflows/E2E.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/E2E.yaml b/.github/workflows/E2E.yaml index 8493a25d0..2b20bed57 100644 --- a/.github/workflows/E2E.yaml +++ b/.github/workflows/E2E.yaml @@ -234,7 +234,7 @@ jobs: $scenariosJson = @{ "matrix" = @{ - "include" = $filteredScenarios | ForEach-Object { @{ "Scenario" = $_ } } + "include" = @($filteredScenarios | ForEach-Object { @{ "Scenario" = $_ } }) }; "max-parallel" = $maxParallel "fail-fast" = $false From 8a5d7e2f0a5aab5e6f00682edcc0373a661fa6e6 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 15:20:44 +0100 Subject: [PATCH 66/92] Add summary table for file update and exclusion management in AL-Go for GitHub --- Scenarios/CustomizingALGoForGitHub.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index 247afe44a..0d81c90e7 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -229,6 +229,15 @@ In order to instruct AL-Go which files to look for at the template repository, y > > This mechanism allows for fine-grained control over which files are propagated to the end repository and which should be explicitly removed, ensuring that unwanted files are not carried forward during updates. +The following table summarizes how AL-Go for GitHub manages file updates and exclusions when using custom template files: + +| File name | In template repo | In end repo | Matched by `filesToUpdate` | Matched by `filesToExclude` | Result | +|---|---|---|---|---|---| +| file.ps1 | Yes | Yes/No | Yes | No | The file is **updated/created** in the end repo | +| file.ps1 | Yes | Yes | Yes/No| Yes | The file is **removed** from the end repo, as it's matched for exclusion | +| file.ps1 | Yes | No | Yes/No | Yes | The file is **_not_ created** in the end repo, as it's matched for exclusion | +| file.ps1 | No | Yes/No | Yes/No | Yes/No | No action. The file is **_not_ updated/created/removed** in the end repo, as it's not present in the template repo | + ## Forking AL-Go for GitHub and making your "own" **public** version Using a fork of AL-Go for GitHub to have your "own" public version of AL-Go for GitHub gives you the maximum customization capabilities. It does however also come with the most work. From 9b8e78cde7ff167edd8849a0ac80c9bd2d2b8b4d Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Tue, 18 Nov 2025 15:42:24 +0100 Subject: [PATCH 67/92] Enhance GetFilesToUpdate function documentation with detailed file representation --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index d0cc52ff3..cbd1f8ac2 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -791,6 +791,11 @@ function GetDefaultFilesToExclude { The projects are used to resolve per-project files. .OUTPUTS An array containing two elements: the list of files to update and the list of files to exclude. + Files are represented as hashtables with the following keys: + - sourceFullPath: The full path to the source file in the template repository. + - originalSourceFullPath: The full path to the original source file in the original template repository (if any). + - type: The type of the file (e.g., workflow, settings). + - destinationFullPath: The full path to the destination file in the target repository. #> function GetFilesToUpdate { Param( From 4a62d50cbad9781f94fbedfc81dc525c9ced2c79 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 19 Nov 2025 13:11:56 +0100 Subject: [PATCH 68/92] Refine file exclusion logic in GetFilesToUpdate function to ensure only relevant files are excluded from the update list --- .../CheckForUpdates.HelperFunctions.ps1 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index cbd1f8ac2..94b6d2ba3 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -817,6 +817,16 @@ function GetFilesToUpdate { $filesToExclude += $settings.customALGoFiles.filesToExclude $filesToExclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExclude -projects $projects + # Exclude files from filesToExclude that are not in filesToUpdate + $filesToExclude = $filesToExclude | Where-Object { + $fileToExclude = $_ + $include = $filesToUpdate | Where-Object { $_.sourceFullPath -eq $fileToExclude.sourceFullPath } + if(-not $include) { + Write-Host "Excluding file $($fileToExclude.sourceFullPath) from exclude list as it is not in the update list" + } + return $include + } + # Exclude files from filesToUpdate that are in filesToExclude $filesToUpdate = $filesToUpdate | Where-Object { $fileToUpdate = $_ @@ -826,6 +836,7 @@ function GetFilesToUpdate { } return $include } + # Apply unusedALGoSystemFiles logic $unusedALGoSystemFiles = $settings.unusedALGoSystemFiles From 355f1f07332927e458be802322c7d3ebfa75e5e9 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 19 Nov 2025 13:12:19 +0100 Subject: [PATCH 69/92] Enhance documentation for file update and exclusion management in AL-Go for GitHub with detailed examples and clarifications --- Scenarios/CustomizingALGoForGitHub.md | 113 +++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index 0d81c90e7..43d8dfcfb 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -217,26 +217,113 @@ In order to instruct AL-Go which files to look for at the template repository, y > [!NOTE] > `filesToUpdate` is used to define all the template files that will be used by AL-Go for GitHub. If a template file is not matched, it will be ignored. Please pay attention, when changing the file configurations: there might be template files that were previously propagated to your repositories. In case these files are no longer matched via `filesToUpdate`, AL-Go for GitHub will ignore them and you might have to remove them manually. -`filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (remove) during the update. Every item in the array may contain the following properties: +`filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (remove) from `filesToUpdate`. Every item in the array may contain the following properties: - `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. - `filter`: A string to use for filtering in the specified source path. _Example_: `notRelevantScript.ps1`. -> [!NOTE] `filesToExclude` is an array containing a subset of files from `filesToUpdate`. These files are specifically marked to be excluded from the update process. During the AL-Go update: -> -> - Any file matched in `filesToExclude` will not be updated. -> - If a file matched by `filesToExclude` exists in the destination (end) repository, it will be removed as part of the update. -> +> [!NOTE] `filesToExclude` is an array of file configurations already included in `filesToUpdate`. These files are specifically marked to be excluded from the update process. > This mechanism allows for fine-grained control over which files are propagated to the end repository and which should be explicitly removed, ensuring that unwanted files are not carried forward during updates. -The following table summarizes how AL-Go for GitHub manages file updates and exclusions when using custom template files: +The following table summarizes how AL-Go for GitHub manages file updates and exclusions when using custom template files. Say, there is a file (e.g. `file.ps1`) in the template repository. + +| File is present in end repo | File is matched by `filesToUpdate` | File is matched by `filesToExclude` | Result | +|---|---|---|---| +| Yes/No | Yes | No | The file is **updated/created** in the end repo | +| Yes | Yes | Yes | The file is **removed** from the end repo, as it's matched for exclusion | +| Yes | No | Yes | The files is **_not_** removed as it was not matched as update | +| No | Yes/No | Yes | The file is **_not_ created** in the end repo, as it's matched for exclusion | + +### Examples of using custom template files + +Below are examples of how to use the `filesToUpdate` and `filesToExclude` settings in your AL-Go configuration. + +#### Example 1: Updating specific scripts for all projects + +```json +"customALGoFiles": { + "filesToUpdate": [ + { + "sourceFolder": ".github/customScripts", + "filter": "*.ps1", + "destinationFolder": ".AL-Go/scripts", + "perProject": true + } + ] +} +``` +This configuration will copy all PowerShell scripts from `.github/customScripts` in your template repository to the `.AL-Go/scripts` folder in each project of your target repository. + +#### Example 2: Excluding a specific script from updates + +```json +"customALGoFiles": { + "filesToUpdate": [ + { + "sourceFolder": ".github/customScripts", + "filter": "*.ps1", + "destinationFolder": ".AL-Go/scripts", + "perProject": true + } + ], + "filesToExclude": [ + { + "sourceFolder": ".github/customScripts", + "filter": "DoNotPropagate.ps1" + } + ] +} +``` +This will update all `.ps1` scripts except `DoNotPropagate.ps1`, which will be excluded from the update process. + +#### Example 3: Updating workflow files and excluding one + +```json +"customALGoFiles": { + "filesToUpdate": [ + { + "sourceFolder": ".github/workflows", + "filter": "*.yaml", + "destinationFolder": ".github/workflows" + } + ], + "filesToExclude": [ + { + "sourceFolder": ".github/workflows", + "filter": "experimental-workflow.yaml" + } + ] +} +``` +All workflow YAML files will be updated except `experimental-workflow.yaml`, which will be removed from the target repository if present. + +#### Example 4: Multiple update and exclude rules + +```json +"customALGoFiles": { + "filesToUpdate": [ + { + "sourceFolder": "shared/config", + "filter": "*.json", + "destinationFolder": "config" + }, + { + "sourceFolder": ".github/scripts", + "filter": "*.ps1", + "destinationFolder": ".github/scripts" + } + ], + "filesToExclude": [ + { + "sourceFolder": "shared/config", + "filter": "legacy-config.json" + } + ] +} +``` +This configuration updates all JSON files from `shared/config` and all PowerShell scripts from `.github/scripts`, but excludes `legacy-config.json` from being updated or created. -| File name | In template repo | In end repo | Matched by `filesToUpdate` | Matched by `filesToExclude` | Result | -|---|---|---|---|---|---| -| file.ps1 | Yes | Yes/No | Yes | No | The file is **updated/created** in the end repo | -| file.ps1 | Yes | Yes | Yes/No| Yes | The file is **removed** from the end repo, as it's matched for exclusion | -| file.ps1 | Yes | No | Yes/No | Yes | The file is **_not_ created** in the end repo, as it's matched for exclusion | -| file.ps1 | No | Yes/No | Yes/No | Yes/No | No action. The file is **_not_ updated/created/removed** in the end repo, as it's not present in the template repo | +These examples demonstrate how you can fine-tune which files are propagated from your template repository and which are excluded, giving you granular control over your AL-Go customization process. ## Forking AL-Go for GitHub and making your "own" **public** version From d72da45995e10d82179cddd8f26357e32962917e Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Wed, 19 Nov 2025 13:13:22 +0100 Subject: [PATCH 70/92] Fix formatting inconsistencies in the summary table for file update and exclusion management in Customizing AL-Go for GitHub --- Scenarios/CustomizingALGoForGitHub.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index 43d8dfcfb..a7df85c8c 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -229,10 +229,10 @@ The following table summarizes how AL-Go for GitHub manages file updates and exc | File is present in end repo | File is matched by `filesToUpdate` | File is matched by `filesToExclude` | Result | |---|---|---|---| -| Yes/No | Yes | No | The file is **updated/created** in the end repo | +| Yes/No | Yes | No | The file is **updated/created** in the end repo | | Yes | Yes | Yes | The file is **removed** from the end repo, as it's matched for exclusion | | Yes | No | Yes | The files is **_not_** removed as it was not matched as update | -| No | Yes/No | Yes | The file is **_not_ created** in the end repo, as it's matched for exclusion | +| No | Yes/No | Yes | The file is **_not_ created** in the end repo, as it's matched for exclusion | ### Examples of using custom template files @@ -252,6 +252,7 @@ Below are examples of how to use the `filesToUpdate` and `filesToExclude` settin ] } ``` + This configuration will copy all PowerShell scripts from `.github/customScripts` in your template repository to the `.AL-Go/scripts` folder in each project of your target repository. #### Example 2: Excluding a specific script from updates @@ -274,6 +275,7 @@ This configuration will copy all PowerShell scripts from `.github/customScripts` ] } ``` + This will update all `.ps1` scripts except `DoNotPropagate.ps1`, which will be excluded from the update process. #### Example 3: Updating workflow files and excluding one @@ -295,6 +297,7 @@ This will update all `.ps1` scripts except `DoNotPropagate.ps1`, which will be e ] } ``` + All workflow YAML files will be updated except `experimental-workflow.yaml`, which will be removed from the target repository if present. #### Example 4: Multiple update and exclude rules @@ -321,6 +324,7 @@ All workflow YAML files will be updated except `experimental-workflow.yaml`, whi ] } ``` + This configuration updates all JSON files from `shared/config` and all PowerShell scripts from `.github/scripts`, but excludes `legacy-config.json` from being updated or created. These examples demonstrate how you can fine-tune which files are propagated from your template repository and which are excluded, giving you granular control over your AL-Go customization process. From 90d4809c4e20ef33a284b6875cf6b0cab82cf9bc Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 20 Nov 2025 14:03:42 +0100 Subject: [PATCH 71/92] Enhance schema and documentation for file update and exclusion configurations to include detailed descriptions for sourceFolder, filter, and destinationFolder properties. --- Actions/.Modules/settings.schema.json | 22 ++++++++++++++-------- Scenarios/CustomizingALGoForGitHub.md | 8 ++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index 40c617bab..49c4e08cc 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -703,17 +703,21 @@ "items": { "type": "object", "properties": { - "destinationFolder": { - "type": "string" - }, "sourceFolder": { - "type": "string" + "type": "string", + "description": "The source folder from which to update files, relative to the template repository root." }, "filter": { - "type": "string" + "type": "string", + "description": "A filter string to select which files to update. It can contain '*' and '?' wildcards." + }, + "destinationFolder": { + "type": "string", + "description": "The destination folder where the files should be updated, relative to the repository root. If not specified, defaults to the same as the source file folder." }, "perProject": { - "type": "boolean" + "type": "boolean", + "description": "Indicates whether the file update should be applied per project. In that case, the destinationFolder is considered relative to each project folder." } } } @@ -724,10 +728,12 @@ "type": "object", "properties": { "sourceFolder": { - "type": "string" + "type": "string", + "description": "The source folder from which to exclude files, relative to the template repository root." }, "filter": { - "type": "string" + "type": "string", + "description": "A filter string to select which files to exclude. It can contain '*' and '?' wildcards." } } } diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index a7df85c8c..bb99d4c06 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -209,9 +209,9 @@ In order to instruct AL-Go which files to look for at the template repository, y `filesToUpdate`, as the name suggests, is an array of file configurations that will instruct AL-Go which files to update. Every item in the array may contain the following properties: -- `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. -- `filter`: A string to use for filtering in the specified source path. _Example_: `*.ps1`. -- `destinationFolder`: A path to a folder, relative to repository that is being updated, where the files should be placed. _Example_: `src/templateScripts`. +- `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. `*` characters are not supported. _Example_: `src/scripts`. +- `filter`: A string to use for filtering in the specified source path. It can contain `*` and `?` wildcards. _Example_: `*.ps1` or `fileToUpdate.ps1`. +- `destinationFolder`: A path to a folder, relative to repository that is being updated, where the files should be placed. If not specified, defaults to the same as the source file folder. _Example_: `src/templateScripts`. - `perProject`: A boolean that indicates whether the matched files should be propagated for all available AL-Go projects. In that case, `destinationFolder` is relative to the project folder. _Example_: `.AL-Go/scripts`. > [!NOTE] @@ -220,7 +220,7 @@ In order to instruct AL-Go which files to look for at the template repository, y `filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (remove) from `filesToUpdate`. Every item in the array may contain the following properties: - `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. -- `filter`: A string to use for filtering in the specified source path. _Example_: `notRelevantScript.ps1`. +- `filter`: A string to use for filtering in the specified source path. It can contain `*` and `?` wildcards. _Example_: `notRelevantScript.ps1` or `*-internal.ps1` > [!NOTE] `filesToExclude` is an array of file configurations already included in `filesToUpdate`. These files are specifically marked to be excluded from the update process. > This mechanism allows for fine-grained control over which files are propagated to the end repository and which should be explicitly removed, ensuring that unwanted files are not carried forward during updates. From 2895c48b531902813d2e62145cbf788b68cc55c5 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 20 Nov 2025 18:10:21 +0100 Subject: [PATCH 72/92] Add duplicate file checks in ResolveFilePaths function and update related tests --- .../CheckForUpdates.HelperFunctions.ps1 | 10 ++- Tests/CheckForUpdates.Action.Test.ps1 | 68 ++++++++++--------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 1da8566b6..ae6eea70a 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -874,6 +874,10 @@ function ResolveFilePaths { $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $file.destinationFolder $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $destinationName + if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullProjectFilePath.destinationFullPath) { + Write-Host "Skipping duplicate per-project file for project '$project': destinationFullPath '$($fullProjectFilePath.destinationFullPath)' already exists" + continue + } Write-Host "Adding per-project file for project '$project': sourceFullPath '$($fullProjectFilePath.sourceFullPath)', originalSourceFullPath '$($fullProjectFilePath.originalSourceFullPath)', destinationFullPath '$($fullProjectFilePath.destinationFullPath)'" $fullFilePaths += $fullProjectFilePath } @@ -885,13 +889,15 @@ function ResolveFilePaths { $fullFilePath.destinationFullPath = Join-Path $destinationFolder $file.destinationFolder $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $destinationName + if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullFilePath.destinationFullPath) { + Write-Host "Skipping duplicate file: destinationFullPath '$($fullFilePath.destinationFullPath)' already exists" + continue + } Write-Host "Adding file: sourceFullPath '$($fullFilePath.sourceFullPath)', originalSourceFullPath '$($fullFilePath.originalSourceFullPath)', destinationFullPath '$($fullFilePath.destinationFullPath)'" $fullFilePaths += $fullFilePath } } } - # Remove duplicates - $fullFilePaths = $fullFilePaths | Sort-Object { $_.destinationFullPath } -Unique return $fullFilePaths } diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index eba45cbda..891e8c9d0 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -427,16 +427,15 @@ Describe "ResolveFilePaths" { $fullFilePaths[0].originalSourceFullPath | Should -Be $null $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + # Second file has is not present in original source folder, so originalSourceFullPath should be $null + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].originalSourceFullPath | Should -Be $null + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - # Second file has type not containing "template", so originalSourceFullPath should be populated - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") - $fullFilePaths[1].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") - - # Third file has is not present in original source folder, so originalSourceFullPath should be $null - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[2].originalSourceFullPath | Should -Be $null - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + # Third file has type not containing "template", so originalSourceFullPath should be populated + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + $fullFilePaths[2].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") } It 'ResolveFilePaths with a single project' { @@ -452,17 +451,17 @@ Describe "ResolveFilePaths" { $fullFilePaths | Should -Not -BeNullOrEmpty $fullFilePaths.Count | Should -Be 3 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") - $fullFilePaths[0].type | Should -Be "markdown" + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File1.txt") + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File3.txt") $fullFilePaths[1].type | Should -Be "text" - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File3.txt") - $fullFilePaths[2].type | Should -Be "text" + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[2].type | Should -Be "markdown" } It 'ResolveFilePaths with multiple projects' { @@ -478,28 +477,30 @@ Describe "ResolveFilePaths" { $fullFilePaths | Should -Not -BeNullOrEmpty $fullFilePaths.Count | Should -Be 5 - # Non-per-project file - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") - $fullFilePaths[0].type | Should -Be "markdown" + # ProjectA files: File1.txt + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" - # ProjectA files + # ProjectB files: File1.txt $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File1.txt") $fullFilePaths[1].type | Should -Be "text" + # ProjectA files: File3.txt $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") $fullFilePaths[2].type | Should -Be "text" - # ProjectB files - $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File1.txt") + # ProjectB files: File3.txt + $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File3.txt") $fullFilePaths[3].type | Should -Be "text" - $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File3.txt") - $fullFilePaths[4].type | Should -Be "text" + # Non-per-project file + $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[4].type | Should -Be "markdown" } } @@ -843,11 +844,12 @@ Describe 'GetFilesToUpdate (real template)' { $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToExclude[1].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToExclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude[1].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToExclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PushPowerPlatformChanges.yaml") + + $filesToExclude[2].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude[2].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToExclude[2].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") - $filesToExclude[2].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PushPowerPlatformChanges.yaml") } It 'Return the correct files when unusedALGoSystemFiles is specified' { From 0cd888178e5c38536cd662b1df57c71f66d450d7 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 20 Nov 2025 18:14:19 +0100 Subject: [PATCH 73/92] Update CustomizingALGoForGitHub.md to clarify default sync behavior for workflow files and remove unnecessary filesToUpdate configuration --- Scenarios/CustomizingALGoForGitHub.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index bb99d4c06..9f3ee3ce5 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -282,13 +282,7 @@ This will update all `.ps1` scripts except `DoNotPropagate.ps1`, which will be e ```json "customALGoFiles": { - "filesToUpdate": [ - { - "sourceFolder": ".github/workflows", - "filter": "*.yaml", - "destinationFolder": ".github/workflows" - } - ], + "filesToUpdate": [], "filesToExclude": [ { "sourceFolder": ".github/workflows", @@ -299,6 +293,7 @@ This will update all `.ps1` scripts except `DoNotPropagate.ps1`, which will be e ``` All workflow YAML files will be updated except `experimental-workflow.yaml`, which will be removed from the target repository if present. +Note that AL-Go for GitHub already syncs all workflow files under `.github/workflows` by default, so you don't need to specify `filesToUpdate`; however, any files matched by `filesToExclude` will be excluded from this default sync. #### Example 4: Multiple update and exclude rules From fa666c668d89788eef6f5e51eec89f377503937e Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Thu, 20 Nov 2025 18:38:46 +0100 Subject: [PATCH 74/92] Add source folder validation in ResolveFilePaths function and corresponding tests --- .../CheckForUpdates.HelperFunctions.ps1 | 6 +++++ Tests/CheckForUpdates.Action.Test.ps1 | 25 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index ae6eea70a..33c31e9c1 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -841,6 +841,12 @@ function ResolveFilePaths { 'destinationFullPath' = $null } + # Check if the source file is under the source folder + if ($srcFile -notlike "$sourceFolder*") { + Write-Host "Skipping source file '$($srcFile)' as it is not under the source folder '$($sourceFolder)'." + continue + } + Write-Host "Processing file '$($srcFile)'" # Try to find the same files in the original template folder if it is specified. Exclude custom template files diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 891e8c9d0..1859326d7 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -502,6 +502,31 @@ Describe "ResolveFilePaths" { $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") $fullFilePaths[4].type | Should -Be "markdown" } + + It 'ResolveFilePaths skips files outside the source folder' { + # Create an external file outside the source folder + $externalFolder = Join-Path $PSScriptRoot "external" + if (-not (Test-Path $externalFolder)) { New-Item -Path $externalFolder -ItemType Directory | Out-Null } + $externalFile = Join-Path $externalFolder "outside.txt" + Set-Content -Path $externalFile -Value "outside" + + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + + $files = @( + @{ "sourceFolder" = "../external"; "filter" = "*.txt" } + ) + + # Intentionally call ResolveFilePaths with the real sourceFolder (so external file should not be included) + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + # Ensure none of the returned sourceFullPath entries point to the external file + $fullFilePaths | ForEach-Object { $_.sourceFullPath | Should -Not -Be $externalFile } + + # Cleanup + if (Test-Path $externalFile) { Remove-Item -Path $externalFile -Force } + if (Test-Path $externalFolder) { Remove-Item -Path $externalFolder -Recurse -Force } + } } Describe "ReplaceOwnerRepoAndBranch" { From e24ec415ca10ec1735ba3efcb65b7f04e0bc6e2b Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 21 Nov 2025 00:28:05 +0100 Subject: [PATCH 75/92] Rename 'filesToUpdate' to 'filesToInclude' across multiple files, update documentation, and adjust tests accordingly. --- Actions/.Modules/ReadSettings.psm1 | 2 +- Actions/.Modules/settings.schema.json | 2 +- .../CheckForUpdates.HelperFunctions.ps1 | 38 ++-- Actions/CheckForUpdates/CheckForUpdates.ps1 | 12 +- Scenarios/CustomizingALGoForGitHub.md | 24 +-- Scenarios/settings.md | 2 +- Tests/CheckForUpdates.Action.Test.ps1 | 199 +++++++++--------- e2eTests/scenarios/CustomTemplate/runtest.ps1 | 2 +- 8 files changed, 141 insertions(+), 140 deletions(-) diff --git a/Actions/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1 index 886de78e4..3e483e429 100644 --- a/Actions/.Modules/ReadSettings.psm1 +++ b/Actions/.Modules/ReadSettings.psm1 @@ -250,7 +250,7 @@ function GetDefaultSettings "reportSuppressedDiagnostics" = $false "workflowDefaultInputs" = @() "customALGoFiles" = [ordered]@{ - "filesToUpdate" = @() + "filesToInclude" = @() "filesToExclude" = @() } } diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index 49c4e08cc..d1f9ebad6 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -698,7 +698,7 @@ "description": "An object containing the custom AL-Go files configuration. See https://aka.ms/ALGoSettings#customALGoFiles", "type": "object", "properties": { - "filesToUpdate": { + "filesToInclude": { "type": "array", "items": { "type": "object", diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 33c31e9c1..1b210c125 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -957,10 +957,10 @@ function GetDefaultFilesToExclude { <# .SYNOPSIS - Get the list of files from the template repository to update and exclude based on the provided settings. + Get the list of files from the template repository to include and exclude based on the provided settings. .DESCRIPTION - This function gets the list of files to update and exclude based on the provided settings. - The unusedALGoSystemFiles setting is also applied to exclude files from the update list and add them to the exclude list. + This function gets the list of files to include and exclude based on the provided settings. + The unusedALGoSystemFiles setting is also applied to exclude files from the include list and add them to the exclude list. .PARAMETER settings The settings object containing the customALGoFiles configuration. .PARAMETER baseFolder @@ -974,7 +974,7 @@ function GetDefaultFilesToExclude { The list of projects in the repository. The projects are used to resolve per-project files. .OUTPUTS - An array containing two elements: the list of files to update and the list of files to exclude. + An array containing two elements: the list of files to include and the list of files to exclude. Files are represented as hashtables with the following keys: - sourceFullPath: The full path to the source file in the template repository. - originalSourceFullPath: The full path to the original source file in the original template repository (if any). @@ -993,30 +993,30 @@ function GetFilesToUpdate { $projects = @() ) - $filesToUpdate = GetDefaultFilesToUpdate -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) - $filesToUpdate += $settings.customALGoFiles.filesToUpdate - $filesToUpdate = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToUpdate -projects $projects + $filesToInclude = GetDefaultFilesToUpdate -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) + $filesToInclude += $settings.customALGoFiles.filesToInclude + $filesToInclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToInclude -projects $projects $filesToExclude = GetDefaultFilesToExclude -settings $settings $filesToExclude += $settings.customALGoFiles.filesToExclude $filesToExclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExclude -projects $projects - # Exclude files from filesToExclude that are not in filesToUpdate + # Exclude files from filesToExclude that are not in filesToInclude $filesToExclude = $filesToExclude | Where-Object { $fileToExclude = $_ - $include = $filesToUpdate | Where-Object { $_.sourceFullPath -eq $fileToExclude.sourceFullPath } + $include = $filesToInclude | Where-Object { $_.sourceFullPath -eq $fileToExclude.sourceFullPath } if(-not $include) { - Write-Host "Excluding file $($fileToExclude.sourceFullPath) from exclude list as it is not in the update list" + Write-Host "Excluding file $($fileToExclude.sourceFullPath) from exclude list as it is not in the include list" } return $include } - # Exclude files from filesToUpdate that are in filesToExclude - $filesToUpdate = $filesToUpdate | Where-Object { - $fileToUpdate = $_ - $include = -not ($filesToExclude | Where-Object { $_.sourceFullPath -eq $fileToUpdate.sourceFullPath }) + # Exclude files from filesToInclude that are in filesToExclude + $filesToInclude = $filesToInclude | Where-Object { + $fileToInclude = $_ + $include = -not ($filesToExclude | Where-Object { $_.sourceFullPath -eq $fileToInclude.sourceFullPath }) if(-not $include) { - Write-Host "Excluding file $($fileToUpdate.sourceFullPath) from update as it is in the exclude list" + Write-Host "Excluding file $($fileToInclude.sourceFullPath) from include as it is in the exclude list" } return $include } @@ -1024,17 +1024,17 @@ function GetFilesToUpdate { # Apply unusedALGoSystemFiles logic $unusedALGoSystemFiles = $settings.unusedALGoSystemFiles - # Exclude unusedALGoSystemFiles from $filesToUpdate and add them to $filesToExclude - $unusedFilesToExclude = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -contains (Split-Path -Path $_.sourceFullPath -Leaf) } + # Exclude unusedALGoSystemFiles from $filesToInclude and add them to $filesToExclude + $unusedFilesToExclude = $filesToInclude | Where-Object { $unusedALGoSystemFiles -contains (Split-Path -Path $_.sourceFullPath -Leaf) } if ($unusedFilesToExclude) { Trace-DeprecationWarning "The 'unusedALGoSystemFiles' setting is deprecated and will be removed in future versions." -DeprecationTag "unusedALGoSystemFiles" Write-Host "The following files are marked as unused and will be removed if they exist:" $unusedFilesToExclude | ForEach-Object { Write-Host "- $($_.destinationFullPath)" } - $filesToUpdate = $filesToUpdate | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) } + $filesToInclude = $filesToInclude | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) } $filesToExclude += @($unusedFilesToExclude) } - return @($filesToUpdate), @($filesToExclude) + return @($filesToInclude), @($filesToExclude) } diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 99c27e223..f8c6ca77f 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -115,7 +115,7 @@ if (-not $isDirectALGo) { $baseFolder = $ENV:GITHUB_WORKSPACE $projects = @(GetProjectsFromRepository -baseFolder $baseFolder -projectsFromSettings $repoSettings.projects) -$filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder +$filesToInclude, $filesToExclude = GetFilesToInclude -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder # $updateFiles will hold an array of files, which needs to be updated $updateFiles = @() @@ -134,15 +134,15 @@ if ($projects.Count -gt 1) { } # Loop through all folders in CheckFiles and check if there are any files that needs to be updated -foreach($fileToUpdate in $filesToUpdate) { - $type = $fileToUpdate.type - $srcPath = $fileToUpdate.sourceFullPath - $originalSrcPath = $fileToUpdate.originalSourceFullPath +foreach($fileToInclude in $filesToInclude) { + $type = $fileToInclude.type + $srcPath = $fileToInclude.sourceFullPath + $originalSrcPath = $fileToInclude.originalSourceFullPath if(-not $originalSrcPath) { $originalSrcPath = $srcPath } - $dstPath = $fileToUpdate.destinationFullPath + $dstPath = $fileToInclude.destinationFullPath $dstFileExists = Test-Path -Path $dstPath -PathType Leaf diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index 9f3ee3ce5..646dcfab8 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -205,9 +205,9 @@ Repositories based on your custom template will notify you that changes are avai When updating AL-Go for GitHub, only specific system files from the template repository are synced to your end repository by default. Files such as `README.md`, `.gitignore`, and other documentation or non-system files are not updated by AL-Go for GitHub. By default, AL-Go syncs workflow files in `.github/workflows`, PowerShell scripts in `.github` and `.AL-Go`, and configuration files required for AL-Go operations. When using custom template repositories, you may need to add additional files related to AL-Go for GitHub, such as script overrides, complementary workflows, or centrally managed files not part of the official AL-Go templates. -In order to instruct AL-Go which files to look for at the template repository, you need to define the `customALGoFiles` setting. The setting is an object that can contain two properties: `filesToUpdate` and `filesToExclude`. +In order to instruct AL-Go which files to look for at the template repository, you need to define the `customALGoFiles` setting. The setting is an object that can contain two properties: `filesToInclude` and `filesToExclude`. -`filesToUpdate`, as the name suggests, is an array of file configurations that will instruct AL-Go which files to update. Every item in the array may contain the following properties: +`filesToInclude`, as the name suggests, is an array of file configurations that will instruct AL-Go which files to include (create/update). Every item in the array may contain the following properties: - `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. `*` characters are not supported. _Example_: `src/scripts`. - `filter`: A string to use for filtering in the specified source path. It can contain `*` and `?` wildcards. _Example_: `*.ps1` or `fileToUpdate.ps1`. @@ -215,19 +215,19 @@ In order to instruct AL-Go which files to look for at the template repository, y - `perProject`: A boolean that indicates whether the matched files should be propagated for all available AL-Go projects. In that case, `destinationFolder` is relative to the project folder. _Example_: `.AL-Go/scripts`. > [!NOTE] -> `filesToUpdate` is used to define all the template files that will be used by AL-Go for GitHub. If a template file is not matched, it will be ignored. Please pay attention, when changing the file configurations: there might be template files that were previously propagated to your repositories. In case these files are no longer matched via `filesToUpdate`, AL-Go for GitHub will ignore them and you might have to remove them manually. +> `filesToInclude` is used to define all the template files that will be used by AL-Go for GitHub. If a template file is not matched, it will be ignored. Please pay attention, when changing the file configurations: there might be template files that were previously propagated to your repositories. In case these files are no longer matched via `filesToInclude`, AL-Go for GitHub will ignore them and you might have to remove them manually. -`filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (remove) from `filesToUpdate`. Every item in the array may contain the following properties: +`filesToExclude` is an array of file configurations that will instruct AL-Go which files to exclude (remove) from `filesToInclude`. Every item in the array may contain the following properties: - `sourceFolder`: A path to a folder, relative to the template, where to look for files. If not specified the root folder is implied. _Example_: `src/scripts`. - `filter`: A string to use for filtering in the specified source path. It can contain `*` and `?` wildcards. _Example_: `notRelevantScript.ps1` or `*-internal.ps1` -> [!NOTE] `filesToExclude` is an array of file configurations already included in `filesToUpdate`. These files are specifically marked to be excluded from the update process. +> [!NOTE] `filesToExclude` is an array of file configurations already included in `filesToInclude`. These files are specifically marked to be excluded from the update process. > This mechanism allows for fine-grained control over which files are propagated to the end repository and which should be explicitly removed, ensuring that unwanted files are not carried forward during updates. The following table summarizes how AL-Go for GitHub manages file updates and exclusions when using custom template files. Say, there is a file (e.g. `file.ps1`) in the template repository. -| File is present in end repo | File is matched by `filesToUpdate` | File is matched by `filesToExclude` | Result | +| File is present in end repo | File is matched by `filesToInclude` | File is matched by `filesToExclude` | Result | |---|---|---|---| | Yes/No | Yes | No | The file is **updated/created** in the end repo | | Yes | Yes | Yes | The file is **removed** from the end repo, as it's matched for exclusion | @@ -236,13 +236,13 @@ The following table summarizes how AL-Go for GitHub manages file updates and exc ### Examples of using custom template files -Below are examples of how to use the `filesToUpdate` and `filesToExclude` settings in your AL-Go configuration. +Below are examples of how to use the `filesToInclude` and `filesToExclude` settings in your AL-Go configuration. #### Example 1: Updating specific scripts for all projects ```json "customALGoFiles": { - "filesToUpdate": [ + "filesToInclude": [ { "sourceFolder": ".github/customScripts", "filter": "*.ps1", @@ -259,7 +259,7 @@ This configuration will copy all PowerShell scripts from `.github/customScripts` ```json "customALGoFiles": { - "filesToUpdate": [ + "filesToInclude": [ { "sourceFolder": ".github/customScripts", "filter": "*.ps1", @@ -282,7 +282,7 @@ This will update all `.ps1` scripts except `DoNotPropagate.ps1`, which will be e ```json "customALGoFiles": { - "filesToUpdate": [], + "filesToInclude": [], "filesToExclude": [ { "sourceFolder": ".github/workflows", @@ -293,13 +293,13 @@ This will update all `.ps1` scripts except `DoNotPropagate.ps1`, which will be e ``` All workflow YAML files will be updated except `experimental-workflow.yaml`, which will be removed from the target repository if present. -Note that AL-Go for GitHub already syncs all workflow files under `.github/workflows` by default, so you don't need to specify `filesToUpdate`; however, any files matched by `filesToExclude` will be excluded from this default sync. +Note that AL-Go for GitHub already syncs all workflow files under `.github/workflows` by default, so you don't need to specify `filesToInclude`; however, any files matched by `filesToExclude` will be excluded from this default sync. #### Example 4: Multiple update and exclude rules ```json "customALGoFiles": { - "filesToUpdate": [ + "filesToInclude": [ { "sourceFolder": "shared/config", "filter": "*.json", diff --git a/Scenarios/settings.md b/Scenarios/settings.md index 4642d25e9..193e54878 100644 --- a/Scenarios/settings.md +++ b/Scenarios/settings.md @@ -241,7 +241,7 @@ Please read the release notes carefully when installing new versions of AL-Go fo | BcContainerHelperVersion | This setting can be set to a specific version (ex. 3.0.8) of BcContainerHelper to force AL-Go to use this version. **latest** means that AL-Go will use the latest released version. **preview** means that AL-Go will use the latest preview version. **dev** means that AL-Go will use the dev branch of containerhelper. | latest (or preview for AL-Go preview) | | unusedALGoSystemFiles (**deprecated**) | An array of AL-Go System Files, which won't be updated during Update AL-Go System Files. They will instead be removed.
Use this setting with care, as this can break the AL-Go for GitHub functionality and potentially leave your repo no longer functional. | [ ] | | reportSuppressedDiagnostics | If this setting is set to true, the AL compiler will report diagnostics which are suppressed in the code using the pragma `#pragma warning disable `. This can be useful if you want to ensure that no warnings are suppressed in your code. | false | -| customALGoFiles | An object to configure custom AL-Go files, that will be updated during "Update AL-Go System Files" workflow. The object can contain properties `filesToUpdate` and `filesToExclude`. Read more at [Customizing AL-Go](CustomizingALGoForGitHub.md#Using-custom-template-files). | `{ "filesToUpdate": [], "filesToExclude": [] }` +| customALGoFiles | An object to configure custom AL-Go files, that will be updated during "Update AL-Go System Files" workflow. The object can contain properties `filesToInclude` and `filesToExclude`. Read more at [Customizing AL-Go](CustomizingALGoForGitHub.md#Using-custom-template-files). | `{ "filesToInclude": [], "filesToExclude": [] }` ## Overwrite settings diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 1859326d7..48f8824db 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -614,17 +614,17 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.ps1" }) + filesToInclude = @(@{ filter = "*.ps1" }) filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') # No files to remove $filesToExclude | Should -BeNullOrEmpty @@ -633,18 +633,18 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt" }) + filesToInclude = @(@{ filter = "*.txt" }) filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 2 - $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') - $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') # No files to remove $filesToExclude | Should -BeNullOrEmpty @@ -655,19 +655,19 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) + filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 2 - $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') - $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test2.txt') + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test2.txt') # No files to remove $filesToExclude | Should -BeNullOrEmpty @@ -676,17 +676,17 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) + filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) filesToExclude = @(@{ filter = "test2.txt" }) } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') # One file to remove $filesToExclude | Should -Not -BeNullOrEmpty @@ -700,17 +700,17 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) + filesToInclude = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') # No files to remove $filesToExclude | Should -BeNullOrEmpty @@ -719,17 +719,17 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "test.ps1"; destinationFolder = 'dstPath'; destinationName = "renamed.txt" }) + filesToInclude = @(@{ filter = "test.ps1"; destinationFolder = 'dstPath'; destinationName = "renamed.txt" }) filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') } It 'Return the correct files with types' { @@ -737,18 +737,18 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.ps1"; type = "script" }) + filesToInclude = @(@{ filter = "*.ps1"; type = "script" }) filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testPSFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') - $filesToUpdate[0].type | Should -Be "script" + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToInclude[0].type | Should -Be "script" # No files to remove $filesToExclude | Should -BeNullOrEmpty @@ -757,18 +757,18 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*.txt"; type = "text" }) + filesToInclude = @(@{ filter = "*.txt"; type = "text" }) filesToExclude = @(@{ filter = "test.txt" }) } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 1 - $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile2 - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - $filesToUpdate[0].type | Should -Be "text" + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude[0].type | Should -Be "text" # One file to remove $filesToExclude | Should -Not -BeNullOrEmpty @@ -782,19 +782,19 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "nonPTE" unusedALGoSystemFiles = @("test.ps1") customALGoFiles = @{ - filesToUpdate = @(@{ filter = "*" }) + filesToInclude = @(@{ filter = "*" }) filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 2 - $filesToUpdate[0].sourceFullPath | Should -Be $testTxtFile - $filesToUpdate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') - $filesToUpdate[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToUpdate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') # One file to remove $filesToExclude | Should -Not -BeNullOrEmpty @@ -823,18 +823,19 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @() + filesToInclude = @() filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 24 - $filesToUpdate.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToUpdate.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToUpdate.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 24 + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") # No files to remove $filesToExclude | Should -BeNullOrEmpty @@ -846,17 +847,17 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @() + filesToInclude = @() filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 21 + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 21 - $filesToUpdate | ForEach-Object { + $filesToInclude | ForEach-Object { $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") @@ -883,21 +884,21 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @("Test Next Major.settings.json") customALGoFiles = @{ - filesToUpdate = @() + filesToInclude = @() filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 23 + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 23 - # Two files to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 1 - $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") - $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/Test Next Major.settings.json') + # Two files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/Test Next Major.settings.json') } It 'Return the correct files when unusedALGoSystemFiles is specified and no PP solution is present' { @@ -906,24 +907,24 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @("Test Next Major.settings.json", "_BuildPowerPlatformSolution.yaml") customALGoFiles = @{ - filesToUpdate = @() + filesToInclude = @() filesToExclude = @() } } - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $filesToUpdate | Should -Not -BeNullOrEmpty - $filesToUpdate.Count | Should -Be 20 + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 20 - # Four files to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 4 + # Four files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 4 - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") } It 'Returns the custom template settings files when there is a custom template' { @@ -932,19 +933,19 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToUpdate = @() + filesToInclude = @() filesToExclude = @() } } $customTemplateFolder = $realPTETemplateFolder $originalTemplateFolder = $realAppSourceAppTemplateFolder - $filesToUpdate, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template - $filesToUpdate | Should -Not -BeNullOrEmpty + $filesToInclude | Should -Not -BeNullOrEmpty # Check repo settings files - $repoSettingsFiles = $filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".github/AL-Go-Settings.json") } + $repoSettingsFiles = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".github/AL-Go-Settings.json") } $repoSettingsFiles | Should -Not -BeNullOrEmpty $repoSettingsFiles.Count | Should -Be 2 @@ -958,7 +959,7 @@ Describe 'GetFilesToUpdate (real template)' { $repoSettingsFiles[1].type | Should -Be '' # Check project settings files - $projectSettingsFilesFromCustomTemplate = @($filesToUpdate | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".AL-Go/settings.json") }) + $projectSettingsFilesFromCustomTemplate = @($filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".AL-Go/settings.json") }) $projectSettingsFilesFromCustomTemplate | Should -Not -BeNullOrEmpty $projectSettingsFilesFromCustomTemplate.Count | Should -Be 2 diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index f1f18ca94..5cdef971b 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -269,7 +269,7 @@ Get-ContentLF -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) | (Join-Path (Get-Location) $customFileName) | Should -Not -Exist # Custom file should not be copied by default # Add custom file to be copied via settings -$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "customALGoFiles" = @{ "filesToUpdate" = @( @{ "filter" = $customFileName } ) } } +$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "customALGoFiles" = @{ "filesToInclude" = @( @{ "filter" = $customFileName } ) } } # Push CommitAndPush -commitMessage 'Add custom file to be updated when updating AL-Go system files [skip ci]' From 1750f8b1682a6f21214fc84c25028f68926bf23a Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 21 Nov 2025 00:59:48 +0100 Subject: [PATCH 76/92] Add tests for ResolveFilePaths and GetFilesToUpdate to handle empty project scenarios and non-matching file filters --- Tests/CheckForUpdates.Action.Test.ps1 | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 48f8824db..bb657f668 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -527,6 +527,35 @@ Describe "ResolveFilePaths" { if (Test-Path $externalFile) { Remove-Item -Path $externalFile -Force } if (Test-Path $externalFolder) { Remove-Item -Path $externalFolder -Recurse -Force } } + + It 'ResolveFilePaths returns empty when no files match filter' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $rootFolder $destinationFolder + + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.doesnotexist"; "destinationFolder" = "newFolder" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + # No matching files should be returned + $fullFilePaths | Should -BeNullOrEmpty + } + + It 'ResolveFilePaths with perProject true and empty projects returns no per-project entries' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + ) + + # Intentionally pass an empty projects array + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @() + + # Behavior: when projects is empty, no per-project entries should be created + $fullFilePaths | Should -BeNullOrEmpty + } } Describe "ReplaceOwnerRepoAndBranch" { @@ -802,6 +831,47 @@ Describe "GetFilesToUpdate (general files to update logic)" { $filesToExclude[0].sourceFullPath | Should -Be $testPSFile $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') } + + It 'GetFilesToUpdate with perProject true and empty projects returns no per-project entries' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt"; type = "text"; perProject = $true }) + filesToExclude = @() + } + } + + # Pass empty projects array + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects @() + + # Behavior: when projects is empty, no per-project entries should be created + $filesToInclude | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty + } + + It 'GetFilesToUpdate ignores filesToExclude patterns that do not match any file' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) + filesToExclude = @(@{ filter = "no-match-*.none" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + # All txt files should be included, no files to exclude + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + + $filesToExclude | Should -BeNullOrEmpty + } } Describe 'GetFilesToUpdate (real template)' { From 02b9f7a92b8a035b9cdccd2b9f7db13cb7a14483 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 21 Nov 2025 01:10:21 +0100 Subject: [PATCH 77/92] Refactor ResolveFilePaths and GetFilesToUpdate functions to replace Write-Host with OutputDebug for improved logging --- .../CheckForUpdates.HelperFunctions.ps1 | 37 ++++++++++++------- Actions/CheckForUpdates/CheckForUpdates.ps1 | 4 +- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 1b210c125..2ebf57e88 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -818,18 +818,18 @@ function ResolveFilePaths { # If originalSourceFolder is not specified, it means there is no custom template, so skip custom template files if(!$originalSourceFolder -and $file.origin -eq 'custom template') { - Write-Host "Skipping custom template file(s) with source folder '$($file.sourceFolder)' as there is no original source folder specified" + OutputDebug "Skipping custom template file(s) with source folder '$($file.sourceFolder)' as there is no original source folder specified" continue; } # All files are relative to the template folder - Write-Host "Resolving files for source folder '$($file.sourceFolder)' and filter '$($file.filter)'" + OutputDebug "Resolving files for source folder '$($file.sourceFolder)' and filter '$($file.filter)'" $sourceFiles = @(Get-ChildItem -Path (Join-Path $sourceFolder $file.sourceFolder) -Filter $file.filter -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName) - Write-Host "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder', origin '$($file.origin)')" + OutputDebug "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder', origin '$($file.origin)')" if(-not $sourceFiles) { - Write-Debug "No files found for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder')" + OutputDebug "No files found for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder')" continue } @@ -843,11 +843,11 @@ function ResolveFilePaths { # Check if the source file is under the source folder if ($srcFile -notlike "$sourceFolder*") { - Write-Host "Skipping source file '$($srcFile)' as it is not under the source folder '$($sourceFolder)'." + OutputDebug "Skipping source file '$($srcFile)' as it is not under the source folder '$($sourceFolder)'." continue } - Write-Host "Processing file '$($srcFile)'" + OutputDebug "Processing file '$($srcFile)'" # Try to find the same files in the original template folder if it is specified. Exclude custom template files if ($originalSourceFolder -and ($file.origin -ne 'custom template')) { @@ -881,10 +881,10 @@ function ResolveFilePaths { $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $destinationName if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullProjectFilePath.destinationFullPath) { - Write-Host "Skipping duplicate per-project file for project '$project': destinationFullPath '$($fullProjectFilePath.destinationFullPath)' already exists" + OutputDebug "Skipping duplicate per-project file for project '$project': destinationFullPath '$($fullProjectFilePath.destinationFullPath)' already exists" continue } - Write-Host "Adding per-project file for project '$project': sourceFullPath '$($fullProjectFilePath.sourceFullPath)', originalSourceFullPath '$($fullProjectFilePath.originalSourceFullPath)', destinationFullPath '$($fullProjectFilePath.destinationFullPath)'" + OutputDebug "Adding per-project file for project '$project': sourceFullPath '$($fullProjectFilePath.sourceFullPath)', originalSourceFullPath '$($fullProjectFilePath.originalSourceFullPath)', destinationFullPath '$($fullProjectFilePath.destinationFullPath)'" $fullFilePaths += $fullProjectFilePath } } @@ -896,10 +896,10 @@ function ResolveFilePaths { $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $destinationName if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullFilePath.destinationFullPath) { - Write-Host "Skipping duplicate file: destinationFullPath '$($fullFilePath.destinationFullPath)' already exists" + OutputDebug "Skipping duplicate file: destinationFullPath '$($fullFilePath.destinationFullPath)' already exists" continue } - Write-Host "Adding file: sourceFullPath '$($fullFilePath.sourceFullPath)', originalSourceFullPath '$($fullFilePath.originalSourceFullPath)', destinationFullPath '$($fullFilePath.destinationFullPath)'" + OutputDebug "Adding file: sourceFullPath '$($fullFilePath.sourceFullPath)', originalSourceFullPath '$($fullFilePath.originalSourceFullPath)', destinationFullPath '$($fullFilePath.destinationFullPath)'" $fullFilePaths += $fullFilePath } } @@ -993,6 +993,8 @@ function GetFilesToUpdate { $projects = @() ) + OutputDebug "Getting files to update from template folder '$templateFolder', original template folder '$originalTemplateFolder' and base folder '$baseFolder'" + $filesToInclude = GetDefaultFilesToUpdate -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) $filesToInclude += $settings.customALGoFiles.filesToInclude $filesToInclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToInclude -projects $projects @@ -1006,7 +1008,7 @@ function GetFilesToUpdate { $fileToExclude = $_ $include = $filesToInclude | Where-Object { $_.sourceFullPath -eq $fileToExclude.sourceFullPath } if(-not $include) { - Write-Host "Excluding file $($fileToExclude.sourceFullPath) from exclude list as it is not in the include list" + OutputDebug "Excluding file $($fileToExclude.sourceFullPath) from exclude list as it is not in the include list" } return $include } @@ -1016,7 +1018,7 @@ function GetFilesToUpdate { $fileToInclude = $_ $include = -not ($filesToExclude | Where-Object { $_.sourceFullPath -eq $fileToInclude.sourceFullPath }) if(-not $include) { - Write-Host "Excluding file $($fileToInclude.sourceFullPath) from include as it is in the exclude list" + OutputDebug "Excluding file $($fileToInclude.sourceFullPath) from include as it is in the exclude list" } return $include } @@ -1029,12 +1031,19 @@ function GetFilesToUpdate { if ($unusedFilesToExclude) { Trace-DeprecationWarning "The 'unusedALGoSystemFiles' setting is deprecated and will be removed in future versions." -DeprecationTag "unusedALGoSystemFiles" - Write-Host "The following files are marked as unused and will be removed if they exist:" - $unusedFilesToExclude | ForEach-Object { Write-Host "- $($_.destinationFullPath)" } + OutputDebug "The following files are marked as unused and will be removed if they exist:" + $unusedFilesToExclude | ForEach-Object { OutputDebug "- $($_.destinationFullPath)" } $filesToInclude = $filesToInclude | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) } $filesToExclude += @($unusedFilesToExclude) } + # List all files to be included and excluded with their source and destination paths, type and original source path (if any) + OutputDebug "Files to include:" + $filesToInclude | ForEach-Object { OutputDebug " -Source: $($_.sourceFullPath), Destination: $($_.destinationFullPath), Type: $($_.type), Original Source: $($_.originalSourceFullPath)" } + + OutputDebug "Files to exclude:" + $filesToExclude | ForEach-Object { OutputDebug " -Source: $($_.sourceFullPath), Destination: $($_.destinationFullPath), Type: $($_.type), Original Source: $($_.originalSourceFullPath)" } + return @($filesToInclude), @($filesToExclude) } diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index f8c6ca77f..46a95da70 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -115,7 +115,7 @@ if (-not $isDirectALGo) { $baseFolder = $ENV:GITHUB_WORKSPACE $projects = @(GetProjectsFromRepository -baseFolder $baseFolder -projectsFromSettings $repoSettings.projects) -$filesToInclude, $filesToExclude = GetFilesToInclude -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder +$filesToInclude, $filesToExclude = GetFilesToUpdate -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder # $updateFiles will hold an array of files, which needs to be updated $updateFiles = @() @@ -146,6 +146,8 @@ foreach($fileToInclude in $filesToInclude) { $dstFileExists = Test-Path -Path $dstPath -PathType Leaf + Write-Host "Processing file: $srcPath -> $dstPath (type: $type)" + switch ($type) { "workflow" { # For workflow files, we might need to modify the file based on the settings From 5b626e7098081bb2fe04eeb23cac2d43f0b9e861 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 21 Nov 2025 01:22:50 +0100 Subject: [PATCH 78/92] Update ResolveFilePaths to return an array and enhance tests for null/empty file parameters --- .../CheckForUpdates.HelperFunctions.ps1 | 2 +- Tests/CheckForUpdates.Action.Test.ps1 | 154 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 2ebf57e88..001715061 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -905,7 +905,7 @@ function ResolveFilePaths { } } - return $fullFilePaths + return @($fullFilePaths) } function GetDefaultFilesToUpdate { diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index bb657f668..5b39d86b1 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -556,6 +556,59 @@ Describe "ResolveFilePaths" { # Behavior: when projects is empty, no per-project entries should be created $fullFilePaths | Should -BeNullOrEmpty } + + It 'ResolveFilePaths returns empty array when files parameter is null or empty' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + + # Explicitly pass $null and @() to verify both code paths behave the same + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $null -destinationFolder $destinationFolder + $fullFilePaths | Should -BeNullOrEmpty + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files @() -destinationFolder $destinationFolder + $fullFilePaths | Should -BeNullOrEmpty + } + + It 'ResolveFilePaths defaults destination folder when none is provided' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") + } + + It 'ResolveFilePaths avoids duplicate destination entries' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationFolder" = "folder" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") + } + + It 'ResolveFilePaths treats dot project as repository root for per-project files' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true; "destinationFolder" = "custom" } + ) + + $projects = @('.', 'ProjectAlpha') + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects $projects + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "custom/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectAlpha/custom/File1.txt") + } } Describe "ReplaceOwnerRepoAndBranch" { @@ -872,6 +925,107 @@ Describe "GetFilesToUpdate (general files to update logic)" { $filesToExclude | Should -BeNullOrEmpty } + + It 'GetFilesToUpdate duplicates per-project includes for each project including the repository root' { + $perProjectFile = Join-Path $templateFolder "perProjectFile.algo" + Set-Content -Path $perProjectFile -Value "per project" + + try { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "perProjectFile.algo"; perProject = $true; destinationFolder = 'custom' }) + filesToExclude = @() + } + } + + $projects = @('.', 'ProjectOne') + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects $projects + + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + + $rootDestination = Join-Path 'baseFolder' 'custom/perProjectFile.algo' + $projectDestination = Join-Path 'baseFolder' 'ProjectOne/custom/perProjectFile.algo' + + $filesToInclude.destinationFullPath | Should -Contain $rootDestination + $filesToInclude.destinationFullPath | Should -Contain $projectDestination + + $filesToExclude | Should -BeNullOrEmpty + } + finally { + if (Test-Path $perProjectFile) { + Remove-Item -Path $perProjectFile -Force + } + } + } + + It 'GetFilesToUpdate adds custom template settings only when original template folder is provided' { + $customTemplateFolder = Join-Path $PSScriptRoot "customTemplateFolder" + $originalTemplateFolder = Join-Path $PSScriptRoot "originalTemplateFolder" + + New-Item -ItemType Directory -Path $customTemplateFolder -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.github') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.AL-Go') -Force | Out-Null + Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{}' -Encoding UTF8 + Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{}' -Encoding UTF8 + + New-Item -ItemType Directory -Path $originalTemplateFolder -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.github') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.AL-Go') -Force | Out-Null + Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 + Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 + + try { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } + + $baseFolder = 'baseFolder' + $projects = @('ProjectA') + + $filesWithoutOriginal, $excludesWithoutOriginal = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $customTemplateFolder -projects $projects + + $filesWithoutOriginal | Should -Not -BeNullOrEmpty + + $repoSettingsDestination = Join-Path $baseFolder (Join-Path '.github' $RepoSettingsFileName) + $projectSettingsRelative = Join-Path 'ProjectA' '.AL-Go' + $projectSettingsRelative = Join-Path $projectSettingsRelative $ALGoSettingsFileName + $projectSettingsDestination = Join-Path $baseFolder $projectSettingsRelative + + $filesWithoutOriginal.destinationFullPath | Should -Contain $repoSettingsDestination + $filesWithoutOriginal.destinationFullPath | Should -Contain $projectSettingsDestination + $filesWithoutOriginal.destinationFullPath | Should -Not -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateRepoSettingsFileName)) + $filesWithoutOriginal.destinationFullPath | Should -Not -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateProjectSettingsFileName)) + + $filesWithOriginal, $excludesWithOriginal = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder -projects $projects + + $filesWithOriginal | Should -Not -BeNullOrEmpty + + $filesWithOriginal.destinationFullPath | Should -Contain $repoSettingsDestination + $filesWithOriginal.destinationFullPath | Should -Contain $projectSettingsDestination + $filesWithOriginal.destinationFullPath | Should -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateRepoSettingsFileName)) + $filesWithOriginal.destinationFullPath | Should -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateProjectSettingsFileName)) + + $excludesWithoutOriginal | Should -BeNullOrEmpty + $excludesWithOriginal | Should -BeNullOrEmpty + } + finally { + if (Test-Path $customTemplateFolder) { + Remove-Item -Path $customTemplateFolder -Recurse -Force + } + if (Test-Path $originalTemplateFolder) { + Remove-Item -Path $originalTemplateFolder -Recurse -Force + } + } + } } Describe 'GetFilesToUpdate (real template)' { From a0b57045a59efde0ed95327a9d0bf0f348d28a95 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 21 Nov 2025 08:53:45 +0100 Subject: [PATCH 79/92] Rename GetDefaultFilesToUpdate to GetDefaultFilesToInclude for clarity and update references accordingly --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 001715061..9884ac78a 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -908,7 +908,7 @@ function ResolveFilePaths { return @($fullFilePaths) } -function GetDefaultFilesToUpdate { +function GetDefaultFilesToInclude { Param( [switch] $includeCustomTemplateFiles ) @@ -995,7 +995,7 @@ function GetFilesToUpdate { OutputDebug "Getting files to update from template folder '$templateFolder', original template folder '$originalTemplateFolder' and base folder '$baseFolder'" - $filesToInclude = GetDefaultFilesToUpdate -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) + $filesToInclude = GetDefaultFilesToInclude -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) $filesToInclude += $settings.customALGoFiles.filesToInclude $filesToInclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToInclude -projects $projects From 85ee21356a101f1a5129a0d1245d75b8d9b00d1f Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Fri, 21 Nov 2025 09:03:03 +0100 Subject: [PATCH 80/92] Add tests for ResolveFilePaths and GetFilesToUpdate to handle various scenarios including trailing slashes, deeply nested folders, mixed perProject flags, and special characters in filenames --- Tests/CheckForUpdates.Action.Test.ps1 | 453 +++++++++++++++++++++++++- 1 file changed, 440 insertions(+), 13 deletions(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 5b39d86b1..befa2990b 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -609,6 +609,257 @@ Describe "ResolveFilePaths" { $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "custom/File1.txt") $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectAlpha/custom/File1.txt") } + + It 'ResolveFilePaths handles sourceFolder with trailing slashes' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder/"; "filter" = "File1.txt" } + @{ "sourceFolder" = "folder\"; "filter" = "File2.log" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + } + + It 'ResolveFilePaths handles deeply nested folder structures' { + # Create a deeply nested folder structure + $deepFolder = Join-Path $sourceFolder "level1/level2/level3" + New-Item -Path $deepFolder -ItemType Directory -Force | Out-Null + $deepFile = Join-Path $deepFolder "deep.txt" + Set-Content -Path $deepFile -Value "deep file" + + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "level1/level2/level3"; "filter" = "deep.txt"; "destinationFolder" = "output" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $deepFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "output/deep.txt") + } + finally { + if (Test-Path $deepFile) { Remove-Item -Path $deepFile -Force } + if (Test-Path (Join-Path $sourceFolder "level1")) { Remove-Item -Path (Join-Path $sourceFolder "level1") -Recurse -Force } + } + } + + It 'ResolveFilePaths handles mixed perProject true and false in same call' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "File2.log"; perProject = $false } + @{ "sourceFolder" = "folder"; "filter" = "File3.txt"; perProject = $true } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA") + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + + # File1.txt is per-project + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + + # File2.log is not per-project + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File2.log") + + # File3.txt is per-project + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") + } + + It 'ResolveFilePaths handles files with no extension' { + $noExtFile = Join-Path $sourceFolder "folder/README" + Set-Content -Path $noExtFile -Value "readme content" + + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "README" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $noExtFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/README") + } + finally { + if (Test-Path $noExtFile) { Remove-Item -Path $noExtFile -Force } + } + } + + It 'ResolveFilePaths handles special characters in filenames' { + $specialFile = Join-Path $sourceFolder "folder/File-With-Dashes_And_Underscores.txt" + Set-Content -Path $specialFile -Value "special chars" + + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File-With-Dashes_And_Underscores.txt" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $specialFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File-With-Dashes_And_Underscores.txt") + } + finally { + if (Test-Path $specialFile) { Remove-Item -Path $specialFile -Force } + } + } + + It 'ResolveFilePaths handles wildcard filters correctly' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File*.txt" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + } + + It 'ResolveFilePaths with perProject skips duplicate files across projects' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true; "destinationFolder" = "folder" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA")) + + # Should only have one entry per project since both resolve to the same destination + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + } + + It 'ResolveFilePaths handles empty sourceFolder value' { + # Create a file in the root of the sourceFolder + $rootFile = Join-Path $sourceFolder "RootFile.txt" + Set-Content -Path $rootFile -Value "root file content" + + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = ""; "filter" = "RootFile.txt" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $rootFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "RootFile.txt") + } + finally { + if (Test-Path $rootFile) { Remove-Item -Path $rootFile -Force } + } + } + + It 'ResolveFilePaths handles multiple filters matching same file' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File1.*" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + # File1.txt should only appear once even though both filters match it + $fullFilePaths | Should -Not -BeNullOrEmpty + $file1Matches = @($fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File1.txt") }) + $file1Matches.Count | Should -Be 1 + } + + It 'ResolveFilePaths correctly resolves originalSourceFullPath only when file exists in original folder' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + + # Create an additional file only in the source folder (not in original) + $onlyInSourceFile = Join-Path $sourceFolder "folder/OnlyInSource.txt" + Set-Content -Path $onlyInSourceFile -Value "only in source" + + try { + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt" } + @{ "sourceFolder" = "folder"; "filter" = "*.log" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder + + # Files that exist in original source should have originalSourceFullPath set + $file1 = $fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File1.txt") } + $file1.originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File1.txt") + + $file2 = $fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File2.log") } + $file2.originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") + + # Files that don't exist in original source should have originalSourceFullPath as $null + $fileOnlyInSource = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $onlyInSourceFile } + $fileOnlyInSource | Should -Not -BeNullOrEmpty + $fileOnlyInSource.originalSourceFullPath | Should -Be $null + } + finally { + if (Test-Path $onlyInSourceFile) { Remove-Item -Path $onlyInSourceFile -Force } + } + } + + It 'ResolveFilePaths with origin custom template and no originalSourceFolder skips files' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "origin" = "custom template" } + @{ "sourceFolder" = "folder"; "filter" = "File2.log" } + ) + + # Don't pass originalSourceFolder - custom template files should be skipped + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + # Only File2.log should be included + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + } + + It 'ResolveFilePaths handles case-insensitive filter matching on Windows' { + # Create files with different case + $upperFile = Join-Path $sourceFolder "folder/UPPER.TXT" + $lowerFile = Join-Path $sourceFolder "folder/lower.txt" + Set-Content -Path $upperFile -Value "upper" + Set-Content -Path $lowerFile -Value "lower" + + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + # On Windows, both should match due to case-insensitive file system + $fullFilePaths | Should -Not -BeNullOrEmpty + $upperMatch = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $upperFile } + $lowerMatch = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $lowerFile } + $upperMatch | Should -Not -BeNullOrEmpty + $lowerMatch | Should -Not -BeNullOrEmpty + } + finally { + if (Test-Path $upperFile) { Remove-Item -Path $upperFile -Force } + if (Test-Path $lowerFile) { Remove-Item -Path $lowerFile -Force } + } + } } Describe "ReplaceOwnerRepoAndBranch" { @@ -965,15 +1216,15 @@ Describe "GetFilesToUpdate (general files to update logic)" { $customTemplateFolder = Join-Path $PSScriptRoot "customTemplateFolder" $originalTemplateFolder = Join-Path $PSScriptRoot "originalTemplateFolder" - New-Item -ItemType Directory -Path $customTemplateFolder -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.github') -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.AL-Go') -Force | Out-Null + New-Item -ItemType Directory -Path $customTemplateFolder -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.github') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.AL-Go') -Force | Out-Null Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{}' -Encoding UTF8 Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{}' -Encoding UTF8 - New-Item -ItemType Directory -Path $originalTemplateFolder -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.github') -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.AL-Go') -Force | Out-Null + New-Item -ItemType Directory -Path $originalTemplateFolder -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.github') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.AL-Go') -Force | Out-Null Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 @@ -1026,6 +1277,71 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } } + + It 'GetFilesToUpdate excludes files that match both include and exclude patterns' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) + filesToExclude = @(@{ filter = "test.txt" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + # test.txt should not be in filesToInclude + $includedTestTxt = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } + $includedTestTxt | Should -BeNullOrEmpty + + # test.txt should be in filesToExclude + $excludedTestTxt = $filesToExclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } + $excludedTestTxt | Should -Not -BeNullOrEmpty + } + + It 'GetFilesToUpdate ignores exclude patterns that do not match any included file' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) + filesToExclude = @(@{ filter = "nonexistent.xyz" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + # All txt files should be included + $filesToInclude | Should -Not -BeNullOrEmpty + $txtFiles = $filesToInclude | Where-Object { $_.sourceFullPath -like "*.txt" } + $txtFiles.Count | Should -BeGreaterThan 0 + + # Exclude list should not contain the non-matching pattern + $excludedNonExistent = $filesToExclude | Where-Object { $_.sourceFullPath -like "*.xyz" } + $excludedNonExistent | Should -BeNullOrEmpty + } + + It 'GetFilesToUpdate handles overlapping include patterns with different destinations' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @( + @{ filter = "test.txt"; destinationFolder = "folder1" } + @{ filter = "test.txt"; destinationFolder = "folder2" } + ) + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + + # Should have two entries for test.txt with different destinations + $testTxtFiles = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } + $testTxtFiles.Count | Should -Be 2 + $testTxtFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'folder1/test.txt') + $testTxtFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'folder2/test.txt') + } } Describe 'GetFilesToUpdate (real template)' { @@ -1052,14 +1368,13 @@ Describe 'GetFilesToUpdate (real template)' { } } + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 24 - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 24 + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") # No files to remove $filesToExclude | Should -BeNullOrEmpty @@ -1199,6 +1514,118 @@ Describe 'GetFilesToUpdate (real template)' { # No files to exclude $filesToExclude | Should -BeNullOrEmpty } + + It 'GetFilesToUpdate handles AppSource template type correctly' { + $settings = @{ + type = "AppSource App" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realAppSourceAppTemplateFolder + + # PowerPlatform files should be excluded for AppSource App too (same as PTE) + $filesToInclude | Should -Not -BeNullOrEmpty + + $ppFiles = $filesToInclude | Where-Object { $_.sourceFullPath -like "*BuildPowerPlatformSolution*" } + $ppFiles | Should -BeNullOrEmpty + + # No files to remove that match PP files as they are not in the template + $ppExcludes = $filesToExclude | Where-Object { $_.sourceFullPath -like "*BuildPowerPlatformSolution*" } + $ppExcludes | Should -BeNullOrEmpty + } + + It 'GetFilesToUpdate with empty unusedALGoSystemFiles array does not exclude any files' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + + # No additional files should be excluded due to unusedALGoSystemFiles + $ppExcludes = $filesToExclude | Where-Object { $_.sourceFullPath -like "*_BuildPowerPlatformSolution.yaml" -or $_.sourceFullPath -like "*PullPowerPlatformChanges.yaml" -or $_.sourceFullPath -like "*PushPowerPlatformChanges.yaml" } + $ppExcludes.Count | Should -Be 3 # Only PP files should be excluded by default + } + + It 'GetFilesToUpdate marks settings files with correct type' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects @('Project1') + + # Check that settings files have type = 'settings' + $repoSettingsFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -like "*$RepoSettingsFileName" -and $_.destinationFullPath -like "*.github*$RepoSettingsFileName" }) + $repoSettingsFiles | Should -Not -BeNullOrEmpty + $repoSettingsFiles[0].type | Should -Be 'settings' + + $projectSettingsFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -like "*$ALGoSettingsFileName" -and $_.destinationFullPath -like "*Project1*.AL-Go*" }) + $projectSettingsFiles | Should -Not -BeNullOrEmpty + $projectSettingsFiles[0].type | Should -Be 'settings' + } + + It 'GetFilesToUpdate handles multiple projects correctly' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } + + $projects = @('ProjectA', 'ProjectB', 'ProjectC') + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects $projects + + # Each project should have its own settings file + $projectASettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectA*.AL-Go*" } + $projectBSettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectB*.AL-Go*" } + $projectCSettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectC*.AL-Go*" } + + $projectASettings | Should -Not -BeNullOrEmpty + $projectBSettings | Should -Not -BeNullOrEmpty + $projectCSettings | Should -Not -BeNullOrEmpty + } + + It 'GetFilesToUpdate excludes files correctly when in both unusedALGoSystemFiles and filesToExclude' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @("Test Next Major.settings.json") + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @(@{ filter = "Test Next Major.settings.json" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + + # Test Next Major.settings.json should be excluded + $testNextMajor = $filesToInclude | Where-Object { $_.sourceFullPath -like "*Test Next Major.settings.json" } + $testNextMajor | Should -BeNullOrEmpty + + # Should be in exclude list + $excludedTestNextMajor = @($filesToExclude | Where-Object { $_.sourceFullPath -like "*Test Next Major.settings.json" }) + $excludedTestNextMajor | Should -Not -BeNullOrEmpty + $excludedTestNextMajor.Count | Should -Be 1 + } } Describe "CheckForUpdates Action Tests" { From 6fe050c93ab3a0e5586cfc8a06751306605efce3 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 12:05:44 +0100 Subject: [PATCH 81/92] Remove duplicate tests --- Tests/CheckForUpdates.Action.Test.ps1 | 3812 ++++++++++++------------- 1 file changed, 1773 insertions(+), 2039 deletions(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index befa2990b..8c099aedd 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -268,2466 +268,2200 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { $modifiedContent."srcSetting" | Should -Be "value1" $modifiedContent."`$schema" | Should -Be "someSchema" } -} -Describe "ResolveFilePaths" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + It 'ApplyWorkflowDefaultInputs applies default values to workflow inputs' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $rootFolder = $PSScriptRoot + # Create a test workflow YAML with workflow_dispatch inputs + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " directCommit:", + " description: Direct Commit?", + " type: boolean", + " default: false", + " useGhTokenWorkflow:", + " description: Use GhTokenWorkflow?", + " type: boolean", + " default: false", + " updateVersionNumber:", + " description: Version number", + " required: false", + " default: ''", + "jobs:", + " test:", + " runs-on: ubuntu-latest", + " steps:", + " - run: echo test" + ) - $sourceFolder = Join-Path $rootFolder "sourceFolder" - if (-not (Test-Path $sourceFolder)) { - New-Item -Path $sourceFolder -ItemType Directory | Out-Null - } - # Create a source folder structure - New-Item -Path (Join-Path $sourceFolder "folder/File1.txt") -ItemType File -Force | Out-Null - New-Item -Path (Join-Path $sourceFolder "folder/File2.log") -ItemType File -Force | Out-Null - New-Item -Path (Join-Path $sourceFolder "folder/File3.txt") -ItemType File -Force | Out-Null - New-Item -Path (Join-Path $sourceFolder "folder/File4.md") -ItemType File -Force | Out-Null + $yaml = [Yaml]::new($yamlContent) - $originalSourceFolder = Join-Path $rootFolder "originalSourceFolder" - if (-not (Test-Path $originalSourceFolder)) { - New-Item -Path $originalSourceFolder -ItemType Directory | Out-Null + # Create settings with workflow input defaults + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "directCommit"; "value" = $true }, + @{ "name" = "useGhTokenWorkflow"; "value" = $true }, + @{ "name" = "updateVersionNumber"; "value" = "+0.1" } + ) } - New-Item -Path (Join-Path $originalSourceFolder "folder/File1.txt") -ItemType File -Force | Out-Null - New-Item -Path (Join-Path $originalSourceFolder "folder/File2.log") -ItemType File -Force | Out-Null - - # File tree: - # sourceFolder - # └── folder - # ├── File1.txt - # ├── File2.log - # ├── File3.txt - # └── File4.md - } - AfterAll { - # Clean up - if (Test-Path $sourceFolder) { - Remove-Item -Path $sourceFolder -Recurse -Force - } + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - if (Test-Path $originalSourceFolder) { - Remove-Item -Path $originalSourceFolder -Recurse -Force - } + # Verify the defaults were applied + $yaml.Get('on:/workflow_dispatch:/inputs:/directCommit:/default:').content -join '' | Should -Be 'default: true' + $yaml.Get('on:/workflow_dispatch:/inputs:/useGhTokenWorkflow:/default:').content -join '' | Should -Be 'default: true' + $yaml.Get('on:/workflow_dispatch:/inputs:/updateVersionNumber:/default:').content -join '' | Should -Be "default: '+0.1'" } - It 'ResolveFilePaths with specific files extensions' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $rootFolder $destinationFolder - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = 'newFolder'; "destinationName" = '' } - @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = 'newFolder'; "destinationName" = '' } - ) - - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 3 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") - $fullFilePaths[0].type | Should -Be '' - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - $fullFilePaths[1].type | Should -Be '' - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") - $fullFilePaths[2].type | Should -Be '' - } + It 'ApplyWorkflowDefaultInputs handles empty workflowDefaultInputs array' { + . (Join-Path $scriptRoot "yamlclass.ps1") - It 'ResolveFilePaths with specific destination names' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $rootFolder $destinationFolder - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationFolder" = 'newFolder'; "destinationName" = "CustomFile1.txt" } - @{ "sourceFolder" = "folder"; "filter" = "File2.log"; "destinationFolder" = 'newFolder'; "destinationName" = "CustomFile2.log" } + # Create a test workflow YAML + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " myInput:", + " type: boolean", + " default: false", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 2 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile1.txt") - $fullFilePaths[0].type | Should -Be '' - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile2.log") - $fullFilePaths[1].type | Should -Be '' - } + $yaml = [Yaml]::new($yamlContent) + $originalContent = $yaml.content -join "`n" - It 'ResolveFilePaths with type' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $PSScriptRoot $destinationFolder - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "folder"; "destinationName" = ''; type = "text" } - @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = "folder"; "destinationName" = ''; type = "markdown" } - ) - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + # Create settings with empty workflowDefaultInputs array + $repoSettings = @{ + "workflowDefaultInputs" = @() + } - # Verify destinationFullPath is not filled - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 3 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") - $fullFilePaths[0].type | Should -Be "text" - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File3.txt") - $fullFilePaths[1].type | Should -Be "text" - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") - $fullFilePaths[2].type | Should -Be "markdown" + # Apply the defaults - should not throw and should not modify workflow + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + $yaml.content -join "`n" | Should -Be $originalContent + $yaml.Get('on:/workflow_dispatch:/inputs:/myInput:/default:').content -join '' | Should -Be 'default: false' } - It 'ResolveFilePaths with original source folder' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $PSScriptRoot $destinationFolder - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "newFolder"; "destinationName" = ''; type = "text" } - @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = "newFolder"; "destinationName" = ''; type = "markdown" } - ) + It 'ApplyWorkflowDefaultInputs handles workflows without workflow_dispatch' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder + # Create a test workflow YAML without workflow_dispatch + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " push:", + " branches: [ main ]", + "jobs:", + " test:", + " runs-on: ubuntu-latest", + " steps:", + " - run: echo test" + ) - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 3 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") - $fullFilePaths[0].type | Should -Be "text" + $yaml = [Yaml]::new($yamlContent) + $originalContent = $yaml.content -join "`n" - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") # File3.txt doesn't exist in original source folder, so it should still point to the source folder - $fullFilePaths[1].originalSourceFullPath | Should -Be $null - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") - $fullFilePaths[1].type | Should -Be "text" + # Create settings with workflow input defaults + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "directCommit"; "value" = $true } + ) + } - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") # File4.md doesn't exist in original source folder, so it should still point to the source folder - $fullFilePaths[2].originalSourceFullPath | Should -Be $null - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") - $fullFilePaths[2].type | Should -Be "markdown" + # Apply the defaults - should not throw or modify YAML + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + $yaml.content -join "`n" | Should -Be $originalContent } - It 'ResolveFilePaths populates the originalSourceFullPath property only if the origin is not a custom template' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $PSScriptRoot $destinationFolder - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "newFolder"; "destinationName" = ''; "origin" = "custom template"; } - @{ "sourceFolder" = "folder"; "filter" = "*.log"; "destinationFolder" = "newFolder"; "destinationName" = ''; } - ) - - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder + It 'ApplyWorkflowDefaultInputs handles workflow_dispatch without inputs section' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 3 + # Create a test workflow YAML with workflow_dispatch but no inputs + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - # First file has type containing "template", so originalSourceFullPath should be $null - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].originalSourceFullPath | Should -Be $null - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + $yaml = [Yaml]::new($yamlContent) + $originalContent = $yaml.content -join "`n" - # Second file has is not present in original source folder, so originalSourceFullPath should be $null - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[1].originalSourceFullPath | Should -Be $null - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + # Create settings with workflow input defaults + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "someInput"; "value" = $true } + ) + } - # Third file has type not containing "template", so originalSourceFullPath should be populated - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") - $fullFilePaths[2].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") + # Apply the defaults - should not throw or modify YAML + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + $yaml.content -join "`n" | Should -Be $originalContent } - It 'ResolveFilePaths with a single project' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $PSScriptRoot $destinationFolder - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } - @{ "sourceFolder" = "folder"; "filter" = "*.md"; type = "markdown"; } - ) + It 'ApplyWorkflowDefaultInputs applies multiple defaults to same workflow' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("SomeProject") + # Create a test workflow YAML with multiple inputs + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " input1:", + " type: boolean", + " default: false", + " input2:", + " type: number", + " default: 0", + " input3:", + " type: string", + " default: ''", + " input4:", + " type: choice", + " options:", + " - optionA", + " - optionB", + " default: optionA", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 3 + $yaml = [Yaml]::new($yamlContent) - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File1.txt") - $fullFilePaths[0].type | Should -Be "text" + # Create settings with multiple defaults + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "input1"; "value" = $true }, + @{ "name" = "input2"; "value" = 5 }, + @{ "name" = "input3"; "value" = "test-value" }, + @{ "name" = "input4"; "value" = "optionB" } + ) + } - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File3.txt") - $fullFilePaths[1].type | Should -Be "text" + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") - $fullFilePaths[2].type | Should -Be "markdown" + # Verify all defaults were applied + $yaml.Get('on:/workflow_dispatch:/inputs:/input1:/default:').content -join '' | Should -Be 'default: true' + $yaml.Get('on:/workflow_dispatch:/inputs:/input2:/default:').content -join '' | Should -Be 'default: 5' + $yaml.Get('on:/workflow_dispatch:/inputs:/input3:/default:').content -join '' | Should -Be "default: 'test-value'" + $yaml.Get('on:/workflow_dispatch:/inputs:/input4:/default:').content -join '' | Should -Be "default: 'optionB'" } - It 'ResolveFilePaths with multiple projects' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $PSScriptRoot $destinationFolder - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } - @{ "sourceFolder" = "folder"; "filter" = "*.md"; type = "markdown"; } - ) - - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA", "ProjectB") - - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 5 + It 'ApplyWorkflowDefaultInputs inserts default line when missing' { + . (Join-Path $scriptRoot "yamlclass.ps1") - # ProjectA files: File1.txt - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") - $fullFilePaths[0].type | Should -Be "text" + # Create a test workflow YAML with input without default line (only description) + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " myInput:", + " description: 'My input without default'", + " type: string", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - # ProjectB files: File1.txt - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File1.txt") - $fullFilePaths[1].type | Should -Be "text" + $yaml = [Yaml]::new($yamlContent) - # ProjectA files: File3.txt - $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") - $fullFilePaths[2].type | Should -Be "text" + # Create settings with default value + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "myInput"; "value" = "new-default" } + ) + } - # ProjectB files: File3.txt - $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") - $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File3.txt") - $fullFilePaths[3].type | Should -Be "text" + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - # Non-per-project file - $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") - $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") - $fullFilePaths[4].type | Should -Be "markdown" + # Verify default line was inserted + $defaultLine = $yaml.Get('on:/workflow_dispatch:/inputs:/myInput:/default:') + $defaultLine | Should -Not -BeNullOrEmpty + $defaultLine.content -join '' | Should -Be "default: 'new-default'" } - It 'ResolveFilePaths skips files outside the source folder' { - # Create an external file outside the source folder - $externalFolder = Join-Path $PSScriptRoot "external" - if (-not (Test-Path $externalFolder)) { New-Item -Path $externalFolder -ItemType Directory | Out-Null } - $externalFile = Join-Path $externalFolder "outside.txt" - Set-Content -Path $externalFile -Value "outside" - - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + It 'ApplyWorkflowDefaultInputs is case-insensitive for input names' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $files = @( - @{ "sourceFolder" = "../external"; "filter" = "*.txt" } + # Create a test workflow YAML with specific casing + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " MyInput:", + " type: boolean", + " default: false", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - # Intentionally call ResolveFilePaths with the real sourceFolder (so external file should not be included) - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + $yaml = [Yaml]::new($yamlContent) - # Ensure none of the returned sourceFullPath entries point to the external file - $fullFilePaths | ForEach-Object { $_.sourceFullPath | Should -Not -Be $externalFile } + # Create settings with different casing + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "myInput"; "value" = $true } + ) + } - # Cleanup - if (Test-Path $externalFile) { Remove-Item -Path $externalFile -Force } - if (Test-Path $externalFolder) { Remove-Item -Path $externalFolder -Recurse -Force } + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + + # Verify default WAS applied despite case difference (case-insensitive matching) + $yaml.Get('on:/workflow_dispatch:/inputs:/MyInput:/default:').content -join '' | Should -Be 'default: true' } - It 'ResolveFilePaths returns empty when no files match filter' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $rootFolder $destinationFolder + It 'ApplyWorkflowDefaultInputs ignores defaults for non-existent inputs' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.doesnotexist"; "destinationFolder" = "newFolder" } + # Create a test workflow YAML + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " existingInput:", + " type: boolean", + " default: false", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + $yaml = [Yaml]::new($yamlContent) + $originalContent = $yaml.content -join "`n" - # No matching files should be returned - $fullFilePaths | Should -BeNullOrEmpty + # Create settings with only non-existent input names + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "nonExistentInput"; "value" = "ignored" }, + @{ "name" = "anotherMissingInput"; "value" = 42 } + ) + } + + # Apply defaults for non-existent inputs - should not throw or modify YAML + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + $yaml.content -join "`n" | Should -Be $originalContent } - It 'ResolveFilePaths with perProject true and empty projects returns no per-project entries' { - $destinationFolder = "destinationFolder" - $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + It 'ApplyWorkflowDefaultInputs applies only existing inputs when mixed with non-existent inputs' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + # Create a test workflow YAML + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " existingInput:", + " type: boolean", + " default: false", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - # Intentionally pass an empty projects array - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @() - - # Behavior: when projects is empty, no per-project entries should be created - $fullFilePaths | Should -BeNullOrEmpty - } + $yaml = [Yaml]::new($yamlContent) - It 'ResolveFilePaths returns empty array when files parameter is null or empty' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + # Create settings with both existing and non-existent input names + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "existingInput"; "value" = $true }, + @{ "name" = "nonExistentInput"; "value" = "ignored" }, + @{ "name" = "anotherMissingInput"; "value" = 42 } + ) + } - # Explicitly pass $null and @() to verify both code paths behave the same - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $null -destinationFolder $destinationFolder - $fullFilePaths | Should -BeNullOrEmpty + # Apply the defaults - should not throw + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files @() -destinationFolder $destinationFolder - $fullFilePaths | Should -BeNullOrEmpty + # Verify only the existing input was modified + $yaml.Get('on:/workflow_dispatch:/inputs:/existingInput:/default:').content -join '' | Should -Be 'default: true' } - It 'ResolveFilePaths defaults destination folder when none is provided' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File1.txt" } - ) - - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + It 'ApplyWorkflowDefaultInputs handles special YAML characters in string values' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") - } - - It 'ResolveFilePaths avoids duplicate destination entries' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File1.txt" } - @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationFolder" = "folder" } + # Create a test workflow YAML + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " input1:", + " type: string", + " default: ''", + " input2:", + " type: string", + " default: ''", + " input3:", + " type: string", + " default: ''", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") - } + $yaml = [Yaml]::new($yamlContent) - It 'ResolveFilePaths treats dot project as repository root for per-project files' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true; "destinationFolder" = "custom" } - ) + # Create settings with special YAML characters + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "input1"; "value" = "value: with colon" }, + @{ "name" = "input2"; "value" = "value # with comment" }, + @{ "name" = "input3"; "value" = "value with 'quotes' inside" } + ) + } - $projects = @('.', 'ProjectAlpha') - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects $projects + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 2 - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "custom/File1.txt") - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectAlpha/custom/File1.txt") + # Verify values are properly quoted and escaped + $yaml.Get('on:/workflow_dispatch:/inputs:/input1:/default:').content -join '' | Should -Be "default: 'value: with colon'" + $yaml.Get('on:/workflow_dispatch:/inputs:/input2:/default:').content -join '' | Should -Be "default: 'value # with comment'" + $yaml.Get('on:/workflow_dispatch:/inputs:/input3:/default:').content -join '' | Should -Be "default: 'value with ''quotes'' inside'" } - It 'ResolveFilePaths handles sourceFolder with trailing slashes' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder/"; "filter" = "File1.txt" } - @{ "sourceFolder" = "folder\"; "filter" = "File2.log" } - ) - - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + It 'ApplyWorkflowDefaultInputs handles environment input type' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 2 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") - } + # Create a test workflow YAML with environment type + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " environmentName:", + " description: Environment to deploy to", + " type: environment", + " default: ''", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - It 'ResolveFilePaths handles deeply nested folder structures' { - # Create a deeply nested folder structure - $deepFolder = Join-Path $sourceFolder "level1/level2/level3" - New-Item -Path $deepFolder -ItemType Directory -Force | Out-Null - $deepFile = Join-Path $deepFolder "deep.txt" - Set-Content -Path $deepFile -Value "deep file" + $yaml = [Yaml]::new($yamlContent) - try { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "level1/level2/level3"; "filter" = "deep.txt"; "destinationFolder" = "output" } + # Create settings with environment value (should be treated as string) + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "environmentName"; "value" = "production" } ) + } - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].sourceFullPath | Should -Be $deepFile - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "output/deep.txt") - } - finally { - if (Test-Path $deepFile) { Remove-Item -Path $deepFile -Force } - if (Test-Path (Join-Path $sourceFolder "level1")) { Remove-Item -Path (Join-Path $sourceFolder "level1") -Recurse -Force } - } + # Verify environment value is set as string + $yaml.Get('on:/workflow_dispatch:/inputs:/environmentName:/default:').content -join '' | Should -Be "default: 'production'" } - It 'ResolveFilePaths handles mixed perProject true and false in same call' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true } - @{ "sourceFolder" = "folder"; "filter" = "File2.log"; perProject = $false } - @{ "sourceFolder" = "folder"; "filter" = "File3.txt"; perProject = $true } + It 'ApplyWorkflowDefaultInputs validates invalid choice value not in options' { + . (Join-Path $scriptRoot "yamlclass.ps1") + + # Create a test workflow YAML with choice input + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " deploymentType:", + " type: choice", + " options:", + " - Development", + " - Staging", + " - Production", + " default: Development", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA") + $yaml = [Yaml]::new($yamlContent) - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 3 + # Create settings with invalid choice value + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "deploymentType"; "value" = "Testing" } + ) + } - # File1.txt is per-project - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + # Apply the defaults - should throw validation error + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | + Should -Throw "*not a valid choice*" + } - # File2.log is not per-project - $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File2.log") + It 'ApplyWorkflowDefaultInputs handles inputs without existing default' { + . (Join-Path $scriptRoot "yamlclass.ps1") - # File3.txt is per-project - $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") - } + # Create a test workflow YAML with input without default + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " myInput:", + " description: My Input", + " required: false", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - It 'ResolveFilePaths handles files with no extension' { - $noExtFile = Join-Path $sourceFolder "folder/README" - Set-Content -Path $noExtFile -Value "readme content" + $yaml = [Yaml]::new($yamlContent) - try { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "README" } + # Create settings with workflow input defaults + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "myInput"; "value" = "test-value" } ) + } - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].sourceFullPath | Should -Be $noExtFile - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/README") - } - finally { - if (Test-Path $noExtFile) { Remove-Item -Path $noExtFile -Force } - } + # Verify the default was added + $defaultLine = $yaml.Get('on:/workflow_dispatch:/inputs:/myInput:/default:') + $defaultLine | Should -Not -BeNullOrEmpty + $defaultLine.content -join '' | Should -Be "default: 'test-value'" } - It 'ResolveFilePaths handles special characters in filenames' { - $specialFile = Join-Path $sourceFolder "folder/File-With-Dashes_And_Underscores.txt" - Set-Content -Path $specialFile -Value "special chars" + It 'ApplyWorkflowDefaultInputs handles different value types' { + . (Join-Path $scriptRoot "yamlclass.ps1") - try { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File-With-Dashes_And_Underscores.txt" } - ) + # Create a test workflow YAML + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " boolInput:", + " type: boolean", + " default: false", + " stringInput:", + " type: string", + " default: ''", + " numberInput:", + " type: number", + " default: 0", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + $yaml = [Yaml]::new($yamlContent) - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].sourceFullPath | Should -Be $specialFile - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File-With-Dashes_And_Underscores.txt") - } - finally { - if (Test-Path $specialFile) { Remove-Item -Path $specialFile -Force } + # Create settings with different value types + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "boolInput"; "value" = $true }, + @{ "name" = "stringInput"; "value" = "test" }, + @{ "name" = "numberInput"; "value" = 42 } + ) } - } - It 'ResolveFilePaths handles wildcard filters correctly' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File*.txt" } - ) - - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 2 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") - $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + # Verify the defaults were applied with correct types + $yaml.Get('on:/workflow_dispatch:/inputs:/boolInput:/default:').content -join '' | Should -Be 'default: true' + $yaml.Get('on:/workflow_dispatch:/inputs:/stringInput:/default:').content -join '' | Should -Be "default: 'test'" + $yaml.Get('on:/workflow_dispatch:/inputs:/numberInput:/default:').content -join '' | Should -Be 'default: 42' } - It 'ResolveFilePaths with perProject skips duplicate files across projects' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true } - @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true; "destinationFolder" = "folder" } + It 'ApplyWorkflowDefaultInputs validates boolean type mismatch' { + . (Join-Path $scriptRoot "yamlclass.ps1") + + # Create a test workflow YAML with boolean input + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " boolInput:", + " type: boolean", + " default: false", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA")) + $yaml = [Yaml]::new($yamlContent) - # Should only have one entry per project since both resolve to the same destination - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + # Create settings with wrong type (string instead of boolean) + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "boolInput"; "value" = "not a boolean" } + ) + } + + # Apply the defaults - should throw validation error + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | + Should -Throw "*Expected boolean value*" } - It 'ResolveFilePaths handles empty sourceFolder value' { - # Create a file in the root of the sourceFolder - $rootFile = Join-Path $sourceFolder "RootFile.txt" - Set-Content -Path $rootFile -Value "root file content" + It 'ApplyWorkflowDefaultInputs validates number type mismatch' { + . (Join-Path $scriptRoot "yamlclass.ps1") - try { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = ""; "filter" = "RootFile.txt" } - ) + # Create a test workflow YAML with number input + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " numberInput:", + " type: number", + " default: 0", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + $yaml = [Yaml]::new($yamlContent) - $fullFilePaths | Should -Not -BeNullOrEmpty - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].sourceFullPath | Should -Be $rootFile - $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "RootFile.txt") - } - finally { - if (Test-Path $rootFile) { Remove-Item -Path $rootFile -Force } + # Create settings with wrong type (string instead of number) + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "numberInput"; "value" = "not a number" } + ) } + + # Apply the defaults - should throw validation error + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | + Should -Throw "*Expected number value*" } - It 'ResolveFilePaths handles multiple filters matching same file' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt" } - @{ "sourceFolder" = "folder"; "filter" = "File1.*" } + It 'ApplyWorkflowDefaultInputs validates string type mismatch' { + . (Join-Path $scriptRoot "yamlclass.ps1") + + # Create a test workflow YAML with string input + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " stringInput:", + " type: string", + " default: ''", + "jobs:", + " test:", + " runs-on: ubuntu-latest" ) - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + $yaml = [Yaml]::new($yamlContent) - # File1.txt should only appear once even though both filters match it - $fullFilePaths | Should -Not -BeNullOrEmpty - $file1Matches = @($fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File1.txt") }) - $file1Matches.Count | Should -Be 1 + # Create settings with wrong type (boolean instead of string) + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "stringInput"; "value" = $true } + ) + } + + # Apply the defaults - should throw validation error + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | + Should -Throw "*Expected string value*" } - It 'ResolveFilePaths correctly resolves originalSourceFullPath only when file exists in original folder' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + It 'ApplyWorkflowDefaultInputs validates choice type' { + . (Join-Path $scriptRoot "yamlclass.ps1") - # Create an additional file only in the source folder (not in original) - $onlyInSourceFile = Join-Path $sourceFolder "folder/OnlyInSource.txt" - Set-Content -Path $onlyInSourceFile -Value "only in source" + # Create a test workflow YAML with choice input + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " choiceInput:", + " type: choice", + " options:", + " - option1", + " - option2", + " default: option1", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - try { - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt" } - @{ "sourceFolder" = "folder"; "filter" = "*.log" } + $yaml = [Yaml]::new($yamlContent) + + # Create settings with correct type (string for choice) + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "choiceInput"; "value" = "option2" } ) + } - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder + # Apply the defaults - should succeed + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + $yaml.Get('on:/workflow_dispatch:/inputs:/choiceInput:/default:').content -join '' | Should -Be "default: 'option2'" + } - # Files that exist in original source should have originalSourceFullPath set - $file1 = $fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File1.txt") } - $file1.originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File1.txt") + It 'ApplyWorkflowDefaultInputs validates choice value is in available options' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $file2 = $fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File2.log") } - $file2.originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") + # Create a test workflow YAML with choice input + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " choiceInput:", + " type: choice", + " options:", + " - option1", + " - option2", + " - option3", + " default: option1", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - # Files that don't exist in original source should have originalSourceFullPath as $null - $fileOnlyInSource = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $onlyInSourceFile } - $fileOnlyInSource | Should -Not -BeNullOrEmpty - $fileOnlyInSource.originalSourceFullPath | Should -Be $null - } - finally { - if (Test-Path $onlyInSourceFile) { Remove-Item -Path $onlyInSourceFile -Force } + $yaml = [Yaml]::new($yamlContent) + + # Create settings with invalid choice value + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "choiceInput"; "value" = "invalidOption" } + ) } - } - It 'ResolveFilePaths with origin custom template and no originalSourceFolder skips files' { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "origin" = "custom template" } - @{ "sourceFolder" = "folder"; "filter" = "File2.log" } - ) + # Apply the defaults - should throw validation error + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | + Should -Throw "*not a valid choice*" + } - # Don't pass originalSourceFolder - custom template files should be skipped - $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + It 'ApplyWorkflowDefaultInputs validates choice value with case-sensitive matching' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $fullFilePaths | Should -Not -BeNullOrEmpty - # Only File2.log should be included - $fullFilePaths.Count | Should -Be 1 - $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") - } + # Create a test workflow YAML with choice input using mixed case options + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " releaseTypeInput:", + " type: choice", + " options:", + " - Release", + " - Prerelease", + " - Draft", + " default: Release", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - It 'ResolveFilePaths handles case-insensitive filter matching on Windows' { - # Create files with different case - $upperFile = Join-Path $sourceFolder "folder/UPPER.TXT" - $lowerFile = Join-Path $sourceFolder "folder/lower.txt" - Set-Content -Path $upperFile -Value "upper" - Set-Content -Path $lowerFile -Value "lower" + $yaml = [Yaml]::new($yamlContent) - try { - $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $files = @( - @{ "sourceFolder" = "folder"; "filter" = "*.txt" } + # Test 1: Exact case match should succeed + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "releaseTypeInput"; "value" = "Prerelease" } ) + } - $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + $yaml.Get('on:/workflow_dispatch:/inputs:/releaseTypeInput:/default:').content -join '' | Should -Be "default: 'Prerelease'" - # On Windows, both should match due to case-insensitive file system - $fullFilePaths | Should -Not -BeNullOrEmpty - $upperMatch = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $upperFile } - $lowerMatch = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $lowerFile } - $upperMatch | Should -Not -BeNullOrEmpty - $lowerMatch | Should -Not -BeNullOrEmpty - } - finally { - if (Test-Path $upperFile) { Remove-Item -Path $upperFile -Force } - if (Test-Path $lowerFile) { Remove-Item -Path $lowerFile -Force } + # Test 2: Wrong case should fail with case-sensitive error message + $yaml2 = [Yaml]::new($yamlContent) + $repoSettings2 = @{ + "workflowDefaultInputs" = @( + @{ "name" = "releaseTypeInput"; "value" = "prerelease" } + ) } - } -} -Describe "ReplaceOwnerRepoAndBranch" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - } + { ApplyWorkflowDefaultInputs -yaml $yaml2 -repoSettings $repoSettings2 -workflowName "Test Workflow" } | + Should -Throw "*case-sensitive match required*" - It "Replaces owner, repo, and branch in workflow content" { - $srcContent = [ref]@" -jobs: - build: - uses: microsoft/AL-Go-Actions@main -"@ - $templateOwner = "contoso" - $templateBranch = "dev" - ReplaceOwnerRepoAndBranch -srcContent $srcContent -templateOwner $templateOwner -templateBranch $templateBranch - $srcContent.Value | Should -Be @" -jobs: - build: - uses: contoso/AL-Go/Actions@dev -"@ - } -} + # Test 3: Uppercase version should also fail + $yaml3 = [Yaml]::new($yamlContent) + $repoSettings3 = @{ + "workflowDefaultInputs" = @( + @{ "name" = "releaseTypeInput"; "value" = "PRERELEASE" } + ) + } -Describe "IsDirectALGo" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - } - It "Returns true for direct AL-Go repo URL" { - IsDirectALGo -templateUrl "https://github.com/contoso/AL-Go@main" | Should -Be True - } - It "Returns false for non-direct AL-Go repo URL" { - IsDirectALGo -templateUrl "https://github.com/contoso/OtherRepo@main" | Should -Be False + { ApplyWorkflowDefaultInputs -yaml $yaml3 -repoSettings $repoSettings3 -workflowName "Test Workflow" } | + Should -Throw "*case-sensitive match required*" } -} - -Describe "GetFilesToUpdate (general files to update logic)" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - # Create template folder with test files - $templateFolder = Join-Path $PSScriptRoot "template" - New-Item -ItemType Directory -Path $templateFolder -Force | Out-Null + It 'ApplyWorkflowDefaultInputs handles inputs without type specification' { + . (Join-Path $scriptRoot "yamlclass.ps1") - New-Item -ItemType Directory -Path (Join-Path $templateFolder "subfolder") -Force | Out-Null + # Create a test workflow YAML without type (defaults to string) + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " noTypeInput:", + " description: Input without type", + " default: ''", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - $testPSFile = Join-Path $templateFolder "test.ps1" - Set-Content -Path $testPSFile -Value "# test ps file" + $yaml = [Yaml]::new($yamlContent) - $testTxtFile = Join-Path $templateFolder "test.txt" - Set-Content -Path $testTxtFile -Value "test txt file" + # Create settings with string value (should work without warning) + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "noTypeInput"; "value" = "string value" } + ) + } - $testTxtFile2 = Join-Path $templateFolder "test2.txt" - Set-Content -Path $testTxtFile2 -Value "test txt file 2" + # Apply the defaults - should succeed + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + $yaml.Get('on:/workflow_dispatch:/inputs:/noTypeInput:/default:').content -join '' | Should -Be "default: 'string value'" + } - $testSubfolderFile = Join-Path $templateFolder "subfolder/testsub.txt" - Set-Content -Path $testSubfolderFile -Value "test subfolder txt file" + It 'ApplyWorkflowDefaultInputs escapes single quotes in string values' { + . (Join-Path $scriptRoot "yamlclass.ps1") - $testSubfolderFile2 = Join-Path $templateFolder "subfolder/testsub2.txt" - Set-Content -Path $testSubfolderFile2 -Value "test subfolder txt file 2" + # Create a test workflow YAML with string input + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " nameInput:", + " type: string", + " default: ''", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - # Display the created files structure for template folder - # . - # ├── test.ps1 - # ├── test.txt - # └── test2.txt - # └── subfolder - # └── testsub.txt - } + $yaml = [Yaml]::new($yamlContent) - AfterAll { - if (Test-Path $templateFolder) { - Remove-Item -Path $templateFolder -Recurse -Force + # Create settings with string value containing single quote + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "nameInput"; "value" = "O'Brien" } + ) } - } - It "Returns the correct files to update with filters" { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.ps1" }) - filesToExclude = @() - } - } + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + # Verify single quote is escaped per YAML spec (doubled) + $yaml.Get('on:/workflow_dispatch:/inputs:/nameInput:/default:').content -join '' | Should -Be "default: 'O''Brien'" + } - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 1 - $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + It 'ApplyWorkflowDefaultInputs applies last value when multiple entries have same input name' { + . (Join-Path $scriptRoot "yamlclass.ps1") - # No files to remove - $filesToExclude | Should -BeNullOrEmpty + # Create a test workflow YAML + $yamlContent = @( + "name: 'Test Workflow'", + "on:", + " workflow_dispatch:", + " inputs:", + " input1:", + " type: string", + " default: ''", + " input2:", + " type: boolean", + " default: false", + "jobs:", + " test:", + " runs-on: ubuntu-latest" + ) - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) - filesToExclude = @() - } + $yaml = [Yaml]::new($yamlContent) + + # Create settings with duplicate entries for input1 - simulating merged conditional settings + # This can happen when multiple conditionalSettings blocks both match and both define the same input + $repoSettings = @{ + "workflowDefaultInputs" = @( + @{ "name" = "input1"; "value" = "first-value" }, + @{ "name" = "input2"; "value" = $false }, + @{ "name" = "input1"; "value" = "second-value" }, # Duplicate input1 + @{ "name" = "input1"; "value" = "final-value" } # Another duplicate input1 + ) } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 2 - $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') - $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + # Apply the defaults + ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - # No files to remove - $filesToExclude | Should -BeNullOrEmpty + # Verify "last wins" - the final value for input1 should be applied + $yaml.Get('on:/workflow_dispatch:/inputs:/input1:/default:').content -join '' | Should -Be "default: 'final-value'" + $yaml.Get('on:/workflow_dispatch:/inputs:/input2:/default:').content -join '' | Should -Be 'default: false' } +} - It 'Returns the correct files with destinationFolder' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) - filesToExclude = @() - } - } +Describe "ResolveFilePaths" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $rootFolder = $PSScriptRoot - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 2 - $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') - $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test2.txt') + $sourceFolder = Join-Path $rootFolder "sourceFolder" + if (-not (Test-Path $sourceFolder)) { + New-Item -Path $sourceFolder -ItemType Directory | Out-Null + } + # Create a source folder structure + New-Item -Path (Join-Path $sourceFolder "folder/File1.txt") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $sourceFolder "folder/File2.log") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $sourceFolder "folder/File3.txt") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $sourceFolder "folder/File4.md") -ItemType File -Force | Out-Null - # No files to remove - $filesToExclude | Should -BeNullOrEmpty + $originalSourceFolder = Join-Path $rootFolder "originalSourceFolder" + if (-not (Test-Path $originalSourceFolder)) { + New-Item -Path $originalSourceFolder -ItemType Directory | Out-Null + } + New-Item -Path (Join-Path $originalSourceFolder "folder/File1.txt") -ItemType File -Force | Out-Null + New-Item -Path (Join-Path $originalSourceFolder "folder/File2.log") -ItemType File -Force | Out-Null - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) - filesToExclude = @(@{ filter = "test2.txt" }) - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 1 - $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') - - # One file to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 1 - $filesToExclude[0].sourceFullPath | Should -Be $testTxtFile2 - $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - } - - It 'Returns the correct files with destinationName' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 1 - $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') - - # No files to remove - $filesToExclude | Should -BeNullOrEmpty - - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "test.ps1"; destinationFolder = 'dstPath'; destinationName = "renamed.txt" }) - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 1 - $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') - } - - It 'Return the correct files with types' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.ps1"; type = "script" }) - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 1 - $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') - $filesToInclude[0].type | Should -Be "script" - - # No files to remove - $filesToExclude | Should -BeNullOrEmpty - - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; type = "text" }) - filesToExclude = @(@{ filter = "test.txt" }) - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 1 - $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - $filesToInclude[0].type | Should -Be "text" - - # One file to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 1 - $filesToExclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') - } - - It 'Return the correct files when unusedALGoSystemFiles is specified' { - $settings = @{ - type = "nonPTE" - unusedALGoSystemFiles = @("test.ps1") - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*" }) - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 2 - $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') - $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - - # One file to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 1 - $filesToExclude[0].sourceFullPath | Should -Be $testPSFile - $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') - } - - It 'GetFilesToUpdate with perProject true and empty projects returns no per-project entries' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; type = "text"; perProject = $true }) - filesToExclude = @() - } - } - - # Pass empty projects array - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects @() - - # Behavior: when projects is empty, no per-project entries should be created - $filesToInclude | Should -BeNullOrEmpty - $filesToExclude | Should -BeNullOrEmpty - } - - It 'GetFilesToUpdate ignores filesToExclude patterns that do not match any file' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) - filesToExclude = @(@{ filter = "no-match-*.none" }) - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - # All txt files should be included, no files to exclude - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 2 - $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') - $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - - $filesToExclude | Should -BeNullOrEmpty - } - - It 'GetFilesToUpdate duplicates per-project includes for each project including the repository root' { - $perProjectFile = Join-Path $templateFolder "perProjectFile.algo" - Set-Content -Path $perProjectFile -Value "per project" - - try { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "perProjectFile.algo"; perProject = $true; destinationFolder = 'custom' }) - filesToExclude = @() - } - } - - $projects = @('.', 'ProjectOne') - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects $projects - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 2 - - $rootDestination = Join-Path 'baseFolder' 'custom/perProjectFile.algo' - $projectDestination = Join-Path 'baseFolder' 'ProjectOne/custom/perProjectFile.algo' - - $filesToInclude.destinationFullPath | Should -Contain $rootDestination - $filesToInclude.destinationFullPath | Should -Contain $projectDestination - - $filesToExclude | Should -BeNullOrEmpty - } - finally { - if (Test-Path $perProjectFile) { - Remove-Item -Path $perProjectFile -Force - } - } - } - - It 'GetFilesToUpdate adds custom template settings only when original template folder is provided' { - $customTemplateFolder = Join-Path $PSScriptRoot "customTemplateFolder" - $originalTemplateFolder = Join-Path $PSScriptRoot "originalTemplateFolder" - - New-Item -ItemType Directory -Path $customTemplateFolder -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.github') -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.AL-Go') -Force | Out-Null - Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{}' -Encoding UTF8 - Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{}' -Encoding UTF8 - - New-Item -ItemType Directory -Path $originalTemplateFolder -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.github') -Force | Out-Null - New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.AL-Go') -Force | Out-Null - Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 - Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 - - try { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } - - $baseFolder = 'baseFolder' - $projects = @('ProjectA') - - $filesWithoutOriginal, $excludesWithoutOriginal = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $customTemplateFolder -projects $projects - - $filesWithoutOriginal | Should -Not -BeNullOrEmpty - - $repoSettingsDestination = Join-Path $baseFolder (Join-Path '.github' $RepoSettingsFileName) - $projectSettingsRelative = Join-Path 'ProjectA' '.AL-Go' - $projectSettingsRelative = Join-Path $projectSettingsRelative $ALGoSettingsFileName - $projectSettingsDestination = Join-Path $baseFolder $projectSettingsRelative - - $filesWithoutOriginal.destinationFullPath | Should -Contain $repoSettingsDestination - $filesWithoutOriginal.destinationFullPath | Should -Contain $projectSettingsDestination - $filesWithoutOriginal.destinationFullPath | Should -Not -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateRepoSettingsFileName)) - $filesWithoutOriginal.destinationFullPath | Should -Not -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateProjectSettingsFileName)) - - $filesWithOriginal, $excludesWithOriginal = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder -projects $projects - - $filesWithOriginal | Should -Not -BeNullOrEmpty - - $filesWithOriginal.destinationFullPath | Should -Contain $repoSettingsDestination - $filesWithOriginal.destinationFullPath | Should -Contain $projectSettingsDestination - $filesWithOriginal.destinationFullPath | Should -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateRepoSettingsFileName)) - $filesWithOriginal.destinationFullPath | Should -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateProjectSettingsFileName)) - - $excludesWithoutOriginal | Should -BeNullOrEmpty - $excludesWithOriginal | Should -BeNullOrEmpty - } - finally { - if (Test-Path $customTemplateFolder) { - Remove-Item -Path $customTemplateFolder -Recurse -Force - } - if (Test-Path $originalTemplateFolder) { - Remove-Item -Path $originalTemplateFolder -Recurse -Force - } - } - } - - It 'GetFilesToUpdate excludes files that match both include and exclude patterns' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) - filesToExclude = @(@{ filter = "test.txt" }) - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - # test.txt should not be in filesToInclude - $includedTestTxt = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } - $includedTestTxt | Should -BeNullOrEmpty - - # test.txt should be in filesToExclude - $excludedTestTxt = $filesToExclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } - $excludedTestTxt | Should -Not -BeNullOrEmpty - } - - It 'GetFilesToUpdate ignores exclude patterns that do not match any included file' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) - filesToExclude = @(@{ filter = "nonexistent.xyz" }) - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - # All txt files should be included - $filesToInclude | Should -Not -BeNullOrEmpty - $txtFiles = $filesToInclude | Where-Object { $_.sourceFullPath -like "*.txt" } - $txtFiles.Count | Should -BeGreaterThan 0 - - # Exclude list should not contain the non-matching pattern - $excludedNonExistent = $filesToExclude | Where-Object { $_.sourceFullPath -like "*.xyz" } - $excludedNonExistent | Should -BeNullOrEmpty - } - - It 'GetFilesToUpdate handles overlapping include patterns with different destinations' { - $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @( - @{ filter = "test.txt"; destinationFolder = "folder1" } - @{ filter = "test.txt"; destinationFolder = "folder2" } - ) - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - - # Should have two entries for test.txt with different destinations - $testTxtFiles = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } - $testTxtFiles.Count | Should -Be 2 - $testTxtFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'folder1/test.txt') - $testTxtFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'folder2/test.txt') - } -} - -Describe 'GetFilesToUpdate (real template)' { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realPTETemplateFolder', Justification = 'False positive.')] - $realPTETemplateFolder = Join-Path $PSScriptRoot "../Templates/Per Tenant Extension" -Resolve - - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realAppSourceAppTemplateFolder', Justification = 'False positive.')] - $realAppSourceAppTemplateFolder = Join-Path $PSScriptRoot "../Templates/AppSource App" -Resolve - } - - It 'Return the correct files to exclude when type is PTE and powerPlatformSolutionFolder is not empty' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = "PowerPlatformSolution" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 24 - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") - - # No files to remove - $filesToExclude | Should -BeNullOrEmpty - } - - It 'Return PP files in filesToExclude when type is PTE but powerPlatformSolutionFolder is empty' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 21 - - $filesToInclude | ForEach-Object { - $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") - } - - # All PP files to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 3 - - $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/_BuildPowerPlatformSolution.yaml") - - $filesToExclude[1].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") - $filesToExclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PushPowerPlatformChanges.yaml") - - $filesToExclude[2].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToExclude[2].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PullPowerPlatformChanges.yaml") - - } - - It 'Return the correct files when unusedALGoSystemFiles is specified' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = "PowerPlatformSolution" - unusedALGoSystemFiles = @("Test Next Major.settings.json") - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 23 - - # Two files to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 1 - $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") - $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/Test Next Major.settings.json') - } - - It 'Return the correct files when unusedALGoSystemFiles is specified and no PP solution is present' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @("Test Next Major.settings.json", "_BuildPowerPlatformSolution.yaml") - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } - - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 20 - - # Four files to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 4 - - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") - } - - It 'Returns the custom template settings files when there is a custom template' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = "PowerPlatformSolution" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } - - $customTemplateFolder = $realPTETemplateFolder - $originalTemplateFolder = $realAppSourceAppTemplateFolder - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template + # File tree: + # sourceFolder + # └── folder + # ├── File1.txt + # ├── File2.log + # ├── File3.txt + # └── File4.md + } - $filesToInclude | Should -Not -BeNullOrEmpty + AfterAll { + # Clean up + if (Test-Path $sourceFolder) { + Remove-Item -Path $sourceFolder -Recurse -Force + } - # Check repo settings files - $repoSettingsFiles = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".github/AL-Go-Settings.json") } + if (Test-Path $originalSourceFolder) { + Remove-Item -Path $originalSourceFolder -Recurse -Force + } + } - $repoSettingsFiles | Should -Not -BeNullOrEmpty - $repoSettingsFiles.Count | Should -Be 2 + It 'ResolveFilePaths with specific files extensions' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $rootFolder $destinationFolder + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = 'newFolder'; "destinationName" = '' } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = 'newFolder'; "destinationName" = '' } + ) - $repoSettingsFiles[0].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".github/AL-Go-Settings.json") - $repoSettingsFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-Settings.json') - $repoSettingsFiles[0].type | Should -Be 'settings' + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - $repoSettingsFiles[1].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null - $repoSettingsFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') - $repoSettingsFiles[1].type | Should -Be '' + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + $fullFilePaths[0].type | Should -Be '' + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + $fullFilePaths[1].type | Should -Be '' + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") + $fullFilePaths[2].type | Should -Be '' + } - # Check project settings files - $projectSettingsFilesFromCustomTemplate = @($filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".AL-Go/settings.json") }) + It 'ResolveFilePaths with specific destination names' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $rootFolder $destinationFolder + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationFolder" = 'newFolder'; "destinationName" = "CustomFile1.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File2.log"; "destinationFolder" = 'newFolder'; "destinationName" = "CustomFile2.log" } + ) - $projectSettingsFilesFromCustomTemplate | Should -Not -BeNullOrEmpty - $projectSettingsFilesFromCustomTemplate.Count | Should -Be 2 + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - $projectSettingsFilesFromCustomTemplate[0].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".AL-Go/settings.json") - $projectSettingsFilesFromCustomTemplate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.AL-Go/settings.json') - $projectSettingsFilesFromCustomTemplate[0].type | Should -Be 'settings' + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile1.txt") + $fullFilePaths[0].type | Should -Be '' + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/CustomFile2.log") + $fullFilePaths[1].type | Should -Be '' + } - $projectSettingsFilesFromCustomTemplate[1].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null - $projectSettingsFilesFromCustomTemplate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateProjectSettings.doNotEdit.json') - $projectSettingsFilesFromCustomTemplate[1].type | Should -Be '' + It 'ResolveFilePaths with type' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "folder"; "destinationName" = ''; type = "text" } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = "folder"; "destinationName" = ''; type = "markdown" } + ) + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - # No files to exclude - $filesToExclude | Should -BeNullOrEmpty + # Verify destinationFullPath is not filled + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File3.txt") + $fullFilePaths[1].type | Should -Be "text" + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[2].type | Should -Be "markdown" } - It 'GetFilesToUpdate handles AppSource template type correctly' { - $settings = @{ - type = "AppSource App" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } + It 'ResolveFilePaths with original source folder' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "newFolder"; "destinationName" = ''; type = "text" } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; "destinationFolder" = "newFolder"; "destinationName" = ''; type = "markdown" } + ) - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realAppSourceAppTemplateFolder + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder - # PowerPlatform files should be excluded for AppSource App too (same as PTE) - $filesToInclude | Should -Not -BeNullOrEmpty + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" - $ppFiles = $filesToInclude | Where-Object { $_.sourceFullPath -like "*BuildPowerPlatformSolution*" } - $ppFiles | Should -BeNullOrEmpty + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") # File3.txt doesn't exist in original source folder, so it should still point to the source folder + $fullFilePaths[1].originalSourceFullPath | Should -Be $null + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + $fullFilePaths[1].type | Should -Be "text" - # No files to remove that match PP files as they are not in the template - $ppExcludes = $filesToExclude | Where-Object { $_.sourceFullPath -like "*BuildPowerPlatformSolution*" } - $ppExcludes | Should -BeNullOrEmpty + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") # File4.md doesn't exist in original source folder, so it should still point to the source folder + $fullFilePaths[2].originalSourceFullPath | Should -Be $null + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File4.md") + $fullFilePaths[2].type | Should -Be "markdown" } - It 'GetFilesToUpdate with empty unusedALGoSystemFiles array does not exclude any files' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } + It 'ResolveFilePaths populates the originalSourceFullPath property only if the origin is not a custom template' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; "destinationFolder" = "newFolder"; "destinationName" = ''; "origin" = "custom template"; } + @{ "sourceFolder" = "folder"; "filter" = "*.log"; "destinationFolder" = "newFolder"; "destinationName" = ''; } + ) - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder - # No additional files should be excluded due to unusedALGoSystemFiles - $ppExcludes = $filesToExclude | Where-Object { $_.sourceFullPath -like "*_BuildPowerPlatformSolution.yaml" -or $_.sourceFullPath -like "*PullPowerPlatformChanges.yaml" -or $_.sourceFullPath -like "*PushPowerPlatformChanges.yaml" } - $ppExcludes.Count | Should -Be 3 # Only PP files should be excluded by default + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 + + # First file has type containing "template", so originalSourceFullPath should be $null + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].originalSourceFullPath | Should -Be $null + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File1.txt") + + # Second file has is not present in original source folder, so originalSourceFullPath should be $null + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].originalSourceFullPath | Should -Be $null + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File3.txt") + + # Third file has type not containing "template", so originalSourceFullPath should be populated + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + $fullFilePaths[2].originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "newFolder/File2.log") } - It 'GetFilesToUpdate marks settings files with correct type' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } + It 'ResolveFilePaths with a single project' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; type = "markdown"; } + ) - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects @('Project1') + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("SomeProject") - # Check that settings files have type = 'settings' - $repoSettingsFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -like "*$RepoSettingsFileName" -and $_.destinationFullPath -like "*.github*$RepoSettingsFileName" }) - $repoSettingsFiles | Should -Not -BeNullOrEmpty - $repoSettingsFiles[0].type | Should -Be 'settings' + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 - $projectSettingsFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -like "*$ALGoSettingsFileName" -and $_.destinationFullPath -like "*Project1*.AL-Go*" }) - $projectSettingsFiles | Should -Not -BeNullOrEmpty - $projectSettingsFiles[0].type | Should -Be 'settings' + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" + + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "SomeProject/folder/File3.txt") + $fullFilePaths[1].type | Should -Be "text" + + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[2].type | Should -Be "markdown" } - It 'GetFilesToUpdate handles multiple projects correctly' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @() - } - } + It 'ResolveFilePaths with multiple projects' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "*.md"; type = "markdown"; } + ) - $projects = @('ProjectA', 'ProjectB', 'ProjectC') - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects $projects + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA", "ProjectB") - # Each project should have its own settings file - $projectASettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectA*.AL-Go*" } - $projectBSettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectB*.AL-Go*" } - $projectCSettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectC*.AL-Go*" } + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 5 - $projectASettings | Should -Not -BeNullOrEmpty - $projectBSettings | Should -Not -BeNullOrEmpty - $projectCSettings | Should -Not -BeNullOrEmpty - } + # ProjectA files: File1.txt + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") + $fullFilePaths[0].type | Should -Be "text" - It 'GetFilesToUpdate excludes files correctly when in both unusedALGoSystemFiles and filesToExclude' { - $settings = @{ - type = "PTE" - powerPlatformSolutionFolder = '' - unusedALGoSystemFiles = @("Test Next Major.settings.json") - customALGoFiles = @{ - filesToInclude = @() - filesToExclude = @(@{ filter = "Test Next Major.settings.json" }) - } - } + # ProjectB files: File1.txt + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File1.txt") + $fullFilePaths[1].type | Should -Be "text" - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + # ProjectA files: File3.txt + $fullFilePaths[2].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") + $fullFilePaths[2].type | Should -Be "text" - # Test Next Major.settings.json should be excluded - $testNextMajor = $filesToInclude | Where-Object { $_.sourceFullPath -like "*Test Next Major.settings.json" } - $testNextMajor | Should -BeNullOrEmpty + # ProjectB files: File3.txt + $fullFilePaths[3].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") + $fullFilePaths[3].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectB/folder/File3.txt") + $fullFilePaths[3].type | Should -Be "text" - # Should be in exclude list - $excludedTestNextMajor = @($filesToExclude | Where-Object { $_.sourceFullPath -like "*Test Next Major.settings.json" }) - $excludedTestNextMajor | Should -Not -BeNullOrEmpty - $excludedTestNextMajor.Count | Should -Be 1 + # Non-per-project file + $fullFilePaths[4].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[4].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + $fullFilePaths[4].type | Should -Be "markdown" } -} -Describe "CheckForUpdates Action Tests" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] - $actionScript = GetActionScript -scriptRoot $scriptRoot -scriptName "$actionName.ps1" - } + It 'ResolveFilePaths skips files outside the source folder' { + # Create an external file outside the source folder + $externalFolder = Join-Path $PSScriptRoot "external" + if (-not (Test-Path $externalFolder)) { New-Item -Path $externalFolder -ItemType Directory | Out-Null } + $externalFile = Join-Path $externalFolder "outside.txt" + Set-Content -Path $externalFile -Value "outside" - It 'Compile Action' { - Invoke-Expression $actionScript - } + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder - It 'Test action.yaml matches script' { - $outputs = [ordered]@{ - } - YamlTest -scriptRoot $scriptRoot -actionName $actionName -actionScript $actionScript -outputs $outputs - } + $files = @( + @{ "sourceFolder" = "../external"; "filter" = "*.txt" } + ) - It 'Test that Update AL-Go System Files uses fixes runs-on' { - . (Join-Path $scriptRoot "yamlclass.ps1") + # Intentionally call ResolveFilePaths with the real sourceFolder (so external file should not be included) + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - $updateYamlFile = Join-Path $scriptRoot "..\..\Templates\Per Tenant Extension\.github\workflows\UpdateGitHubGoSystemFiles.yaml" - $updateYaml = [Yaml]::Load($updateYamlFile) - $updateYaml.content | Where-Object { $_ -like '*runs-on:*' } | ForEach-Object { - $_.Trim() | Should -Be 'runs-on: windows-latest' -Because "Expected 'runs-on: windows-latest', in order to hardcode runner to windows-latest, but got $_" - } + # Ensure none of the returned sourceFullPath entries point to the external file + $fullFilePaths | ForEach-Object { $_.sourceFullPath | Should -Not -Be $externalFile } + + # Cleanup + if (Test-Path $externalFile) { Remove-Item -Path $externalFile -Force } + if (Test-Path $externalFolder) { Remove-Item -Path $externalFolder -Recurse -Force } } -} -Describe('YamlClass Tests') { - BeforeAll { - $actionName = "CheckForUpdates" - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'scriptRoot', Justification = 'False positive.')] - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + It 'ResolveFilePaths returns empty when no files match filter' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $rootFolder $destinationFolder - Mock Trace-Information {} + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.doesnotexist"; "destinationFolder" = "newFolder" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + # No matching files should be returned + $fullFilePaths | Should -BeNullOrEmpty } - It 'Test YamlClass' { - . (Join-Path $scriptRoot "yamlclass.ps1") - $yaml = [Yaml]::load((Join-Path $PSScriptRoot 'YamlSnippet.txt')) + It 'ResolveFilePaths with perProject true and empty projects returns no per-project entries' { + $destinationFolder = "destinationFolder" + $destinationFolder = Join-Path $PSScriptRoot $destinationFolder - # Yaml file should have 77 entries - $yaml.content.Count | Should -be 74 + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt"; type = "text"; perProject = $true } + ) - $start = 0; $count = 0 - # Locate lines for permissions section (including permissions: line) - $yaml.Find('permissions:', [ref] $start, [ref] $count) | Should -be $true - $start | Should -be 17 - $count | Should -be 5 + # Intentionally pass an empty projects array + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @() - # Locate lines for permissions section (excluding permissions: line) - $yaml.Find('permissions:/', [ref] $start, [ref] $count) | Should -be $true - $start | Should -be 18 - $count | Should -be 4 + # Behavior: when projects is empty, no per-project entries should be created + $fullFilePaths | Should -BeNullOrEmpty + } - # Get Yaml class for permissions section (excluding permissions: line) - $yaml.Get('permissions:/').content | ForEach-Object { $_ | Should -not -belike ' *' } + It 'ResolveFilePaths returns empty array when files parameter is null or empty' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - # Locate section called permissionos (should return false) - $yaml.Find('permissionos:', [ref] $start, [ref] $count) | Should -Not -be $true + # Explicitly pass $null and @() to verify both code paths behave the same + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $null -destinationFolder $destinationFolder + $fullFilePaths | Should -BeNullOrEmpty - # Check checkout step - ($yaml.Get('jobs:/Initialization:/steps:/- name: Checkout').content -join '') | Should -be "- name: Checkout uses: actions/checkout@v4 with: lfs: true" + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files @() -destinationFolder $destinationFolder + $fullFilePaths | Should -BeNullOrEmpty + } - # Get Shell line in read Settings step - ($yaml.Get('jobs:/Initialization:/steps:/- name: Read settings/with:/shell:').content -join '') | Should -be "shell: powershell" + It 'ResolveFilePaths defaults destination folder when none is provided' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt" } + ) - # Get Jobs section (without the jobs: line) - $jobsYaml = $yaml.Get('jobs:/') + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - # Locate CheckForUpdates - $jobsYaml.Find('CheckForUpdates:', [ref] $start, [ref] $count) | Should -be $true - $start | Should -be 24 - $count | Should -be 19 + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") + } - # Replace all occurances of 'shell: powershell' with 'shell: pwsh' - $yaml.ReplaceAll('shell: powershell','shell: pwsh') - $yaml.content[46].Trim() | Should -be 'shell: pwsh' + It 'ResolveFilePaths avoids duplicate destination entries' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationFolder" = "folder" } + ) - # Replace Permissions - $yaml.Replace('Permissions:/',@('contents: write','actions: read')) - $yaml.content[44].Trim() | Should -be 'shell: pwsh' - $yaml.content.Count | Should -be 72 + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - # Get Jobs section (without the jobs: line) - $jobsYaml = $yaml.Get('jobs:/') - ($jobsYaml.Get('Initialization:/steps:/- name: Read settings/with:/shell:').content -join '') | Should -be "shell: pwsh" + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File1.txt") } - It 'Test YamlClass Remove' { - . (Join-Path $scriptRoot "yamlclass.ps1") - - $yamlSnippet = @( - "permissions:", - " contents: read", - " actions: read", - " pull-requests: write", - " checks: write" + It 'ResolveFilePaths treats dot project as repository root for per-project files' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true; "destinationFolder" = "custom" } ) - $permissionsYaml = [Yaml]::new($yamlSnippet) + $projects = @('.', 'ProjectAlpha') + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects $projects - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(1, 0) # Remove nothing - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.content[0].Trim() | Should -be 'contents: read' - $permissionsContent.content[1].Trim() | Should -be 'actions: read' - $permissionsContent.content[2].Trim() | Should -be 'pull-requests: write' - $permissionsContent.content[3].Trim() | Should -be 'checks: write' + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "custom/File1.txt") + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectAlpha/custom/File1.txt") + } - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(0, 3) # Remove first 3 lines - $permissionsContent.content.Count | Should -be 1 - $permissionsContent.content[0].Trim() | Should -be 'checks: write' + It 'ResolveFilePaths handles sourceFolder with trailing slashes' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder/"; "filter" = "File1.txt" } + @{ "sourceFolder" = "folder\"; "filter" = "File2.log" } + ) - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(2, 1) # Remove only the 3rd line - $permissionsContent.content.Count | Should -be 3 - $permissionsContent.content[0].Trim() | Should -be 'contents: read' - $permissionsContent.content[1].Trim() | Should -be 'actions: read' - $permissionsContent.content[2].Trim() | Should -be 'checks: write' + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - $permissionsContent = $permissionsYaml.Get('permissions:/') - $permissionsContent.content.Count | Should -be 4 - $permissionsContent.Remove(2, 4) # Remove more than the number of lines - $permissionsContent.content.Count | Should -be 2 # Only the first two lines should remain - $permissionsContent.content[0].Trim() | Should -be 'contents: read' - $permissionsContent.content[1].Trim() | Should -be 'actions: read' + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") } - It 'Test YamlClass GetCustomJobsFromYaml' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'ResolveFilePaths handles deeply nested folder structures' { + # Create a deeply nested folder structure + $deepFolder = Join-Path $sourceFolder "level1/level2/level3" + New-Item -Path $deepFolder -ItemType Directory -Force | Out-Null + $deepFile = Join-Path $deepFolder "deep.txt" + Set-Content -Path $deepFile -Value "deep file" - $customizedYaml = [Yaml]::load((Join-Path $PSScriptRoot 'CustomizedYamlSnippet-All.txt')) - $nonCustomizedYaml = [Yaml]::load((Join-Path $PSScriptRoot 'YamlSnippet.txt')) + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "level1/level2/level3"; "filter" = "deep.txt"; "destinationFolder" = "output" } + ) - # Get Custom jobs from yaml - $customJobs = $customizedYaml.GetCustomJobsFromYaml('CustomJob*') - $customJobs | Should -Not -BeNullOrEmpty - $customJobs.Count | Should -be 2 + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - $customJobs[0].Name | Should -Be 'CustomJob-MyFinalJob' - $customJobs[0].Origin | Should -Be 'FinalRepository' + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $deepFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "output/deep.txt") + } + finally { + if (Test-Path $deepFile) { Remove-Item -Path $deepFile -Force } + if (Test-Path (Join-Path $sourceFolder "level1")) { Remove-Item -Path (Join-Path $sourceFolder "level1") -Recurse -Force } + } + } - $customJobs[1].Name | Should -Be 'CustomJob-MyCustomTemplateJob' - $customJobs[1].Origin | Should -Be 'TemplateRepository' + It 'ResolveFilePaths handles mixed perProject true and false in same call' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "File2.log"; perProject = $false } + @{ "sourceFolder" = "folder"; "filter" = "File3.txt"; perProject = $true } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA") - $emptyCustomJobs = $nonCustomizedYaml.GetCustomJobsFromYaml('CustomJob*') - $emptyCustomJobs | Should -BeNullOrEmpty - } + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 3 - It 'Test YamlClass AddCustomJobsToYaml' { - . (Join-Path $scriptRoot "yamlclass.ps1") + # File1.txt is per-project + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") - $customTemplateYaml = [Yaml]::load((Join-Path $PSScriptRoot 'CustomizedYamlSnippet-TemplateRepository.txt')) - $finalRepositoryYaml = [Yaml]::load((Join-Path $PSScriptRoot 'CustomizedYamlSnippet-FinalRepository.txt')) - $nonCustomizedYaml = [Yaml]::load((Join-Path $PSScriptRoot 'YamlSnippet.txt')) + # File2.log is not per-project + $fullFilePaths[1].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File2.log") - $customTemplateJobs = $customTemplateYaml.GetCustomJobsFromYaml('CustomJob*') - $customTemplateJobs | Should -Not -BeNullOrEmpty - $customTemplateJobs.Count | Should -be 1 - $customTemplateJobs[0].Name | Should -Be 'CustomJob-MyCustomTemplateJob' - $customTemplateJobs[0].Origin | Should -Be 'FinalRepository' # Custom template job has FinalRepository as origin when in the template itself + # File3.txt is per-project + $fullFilePaths[2].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File3.txt") + } - # Add the custom job to the non-customized yaml - $nonCustomizedYaml.AddCustomJobsToYaml($customTemplateJobs, [CustomizationOrigin]::TemplateRepository) + It 'ResolveFilePaths handles files with no extension' { + $noExtFile = Join-Path $sourceFolder "folder/README" + Set-Content -Path $noExtFile -Value "readme content" - $nonCustomizedYaml.content -join "`r`n" | Should -Be ($finalRepositoryYaml.content -join "`r`n") + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "README" } + ) - # Adding the jobs again doesn't have an effect - $nonCustomizedYaml.AddCustomJobsToYaml($customTemplateJobs, [CustomizationOrigin]::TemplateRepository) + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - $nonCustomizedYaml.content -join "`r`n" | Should -Be ($finalRepositoryYaml.content -join "`r`n") + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $noExtFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/README") + } + finally { + if (Test-Path $noExtFile) { Remove-Item -Path $noExtFile -Force } + } } - It('Test YamlClass ApplyTemplateCustomizations') { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'ResolveFilePaths handles special characters in filenames' { + $specialFile = Join-Path $sourceFolder "folder/File-With-Dashes_And_Underscores.txt" + Set-Content -Path $specialFile -Value "special chars" - $srcContent = Get-Content (Join-Path $PSScriptRoot 'YamlSnippet.txt') - $resultContent = Get-Content (Join-Path $PSScriptRoot 'CustomizedYamlSnippet-FinalRepository.txt') + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File-With-Dashes_And_Underscores.txt" } + ) - [Yaml]::ApplyTemplateCustomizations([ref] $srcContent, (Join-Path $PSScriptRoot 'CustomizedYamlSnippet-TemplateRepository.txt')) + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - $srcContent | Should -Be ($resultContent -join "`n") + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $specialFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File-With-Dashes_And_Underscores.txt") + } + finally { + if (Test-Path $specialFile) { Remove-Item -Path $specialFile -Force } + } } - It('Test YamlClass ApplyFinalCustomizations') { - . (Join-Path $scriptRoot "yamlclass.ps1") - - $srcContent = Get-Content (Join-Path $PSScriptRoot 'YamlSnippet.txt') - $resultContent = Get-Content (Join-Path $PSScriptRoot 'CustomizedYamlSnippet-TemplateRepository.txt') + It 'ResolveFilePaths handles wildcard filters correctly' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File*.txt" } + ) - [Yaml]::ApplyFinalCustomizations([ref] $srcContent, (Join-Path $PSScriptRoot 'CustomizedYamlSnippet-TemplateRepository.txt')) # Threat the template repo as a final repo + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder - $srcContent | Should -Be ($resultContent -join "`n") + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 2 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File1.txt") + $fullFilePaths[1].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File3.txt") } -} -Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { - BeforeAll { - $actionName = "CheckForUpdates" - $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve - Import-Module (Join-Path $scriptRoot "..\Github-Helper.psm1") -DisableNameChecking -Force - . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] - $tmpSrcFile = Join-Path $PSScriptRoot "tempSrcFile.json" - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] - $tmpDstFile = Join-Path $PSScriptRoot "tempDestFile.json" + It 'ResolveFilePaths with perProject skips duplicate files across projects' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true } + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; perProject = $true; "destinationFolder" = "folder" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("ProjectA")) + + # Should only have one entry per project since both resolve to the same destination + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "ProjectA/folder/File1.txt") } - AfterEach { - # Clean up temporary files - if (Test-Path $tmpSrcFile) { - Remove-Item -Path $tmpSrcFile -Force + It 'ResolveFilePaths handles empty sourceFolder value' { + # Create a file in the root of the sourceFolder + $rootFile = Join-Path $sourceFolder "RootFile.txt" + Set-Content -Path $rootFile -Value "root file content" + + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = ""; "filter" = "RootFile.txt" } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -Not -BeNullOrEmpty + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be $rootFile + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "RootFile.txt") } - if (Test-Path $tmpDstFile) { - Remove-Item -Path $tmpDstFile -Force + finally { + if (Test-Path $rootFile) { Remove-Item -Path $rootFile -Force } } } - It 'GetModifiedSettingsContent returns correct content when destination file is not empty' { - # Create settings files with the content - @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force - @{ "setting1" = "value2" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpDstFile -Force + It 'ResolveFilePaths handles multiple filters matching same file' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File1.*" } + ) - $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - $modifiedContent = $modifiedContentJson | ConvertFrom-Json - $modifiedContent | Should -Not -BeNullOrEmpty - $modifiedContent.PSObject.Properties.Name.Count | Should -Be 2 # setting1 and $schema - $modifiedContent."setting1" | Should -Be "value2" - $modifiedContent."`$schema" | Should -Be "someSchema" + # File1.txt should only appear once even though both filters match it + $fullFilePaths | Should -Not -BeNullOrEmpty + $file1Matches = @($fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File1.txt") }) + $file1Matches.Count | Should -Be 1 } - It 'GetModifiedSettingsContent returns correct content when destination file is empty' { - # Create only the source file - @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force - '' | Out-File -FilePath $tmpDstFile -Force - $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile + It 'ResolveFilePaths correctly resolves originalSourceFullPath only when file exists in original folder' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" - $modifiedContent = $modifiedContentJson | ConvertFrom-Json - $modifiedContent | Should -Not -BeNullOrEmpty - @($modifiedContent.PSObject.Properties.Name).Count | Should -Be 2 # srcSetting and $schema - $modifiedContent."`$schema" | Should -Be "someSchema" - $modifiedContent."srcSetting" | Should -Be "value1" - } + # Create an additional file only in the source folder (not in original) + $onlyInSourceFile = Join-Path $sourceFolder "folder/OnlyInSource.txt" + Set-Content -Path $onlyInSourceFile -Value "only in source" - It 'GetModifiedSettingsContent returns correct content when destination file does not exist' { - # Create only the source file - @{ "`$schema" = "someSchema"; "srcSetting" = "value1" } | ConvertTo-Json -Depth 10 | Out-File -FilePath $tmpSrcFile -Force + try { + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt" } + @{ "sourceFolder" = "folder"; "filter" = "*.log" } + ) - Test-Path $tmpDstFile | Should -Be $false - $modifiedContentJson = GetModifiedSettingsContent -srcSettingsFile $tmpSrcFile -dstSettingsFile $tmpDstFile + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -originalSourceFolder $originalSourceFolder - $modifiedContent = $modifiedContentJson | ConvertFrom-Json - $modifiedContent | Should -Not -BeNullOrEmpty - $modifiedContent.PSObject.Properties.Name.Count | Should -Be 2 # srcSetting and $schema - $modifiedContent."srcSetting" | Should -Be "value1" - $modifiedContent."`$schema" | Should -Be "someSchema" - } + # Files that exist in original source should have originalSourceFullPath set + $file1 = $fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File1.txt") } + $file1.originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File1.txt") - It 'ApplyWorkflowDefaultInputs applies default values to workflow inputs' { - . (Join-Path $scriptRoot "yamlclass.ps1") + $file2 = $fullFilePaths | Where-Object { $_.sourceFullPath -eq (Join-Path $sourceFolder "folder/File2.log") } + $file2.originalSourceFullPath | Should -Be (Join-Path $originalSourceFolder "folder/File2.log") - # Create a test workflow YAML with workflow_dispatch inputs - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " directCommit:", - " description: Direct Commit?", - " type: boolean", - " default: false", - " useGhTokenWorkflow:", - " description: Use GhTokenWorkflow?", - " type: boolean", - " default: false", - " updateVersionNumber:", - " description: Version number", - " required: false", - " default: ''", - "jobs:", - " test:", - " runs-on: ubuntu-latest", - " steps:", - " - run: echo test" + # Files that don't exist in original source should have originalSourceFullPath as $null + $fileOnlyInSource = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $onlyInSourceFile } + $fileOnlyInSource | Should -Not -BeNullOrEmpty + $fileOnlyInSource.originalSourceFullPath | Should -Be $null + } + finally { + if (Test-Path $onlyInSourceFile) { Remove-Item -Path $onlyInSourceFile -Force } + } + } + + It 'ResolveFilePaths with origin custom template and no originalSourceFolder skips files' { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "origin" = "custom template" } + @{ "sourceFolder" = "folder"; "filter" = "File2.log" } ) - $yaml = [Yaml]::new($yamlContent) + # Don't pass originalSourceFolder - custom template files should be skipped + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) - # Create settings with workflow input defaults - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "directCommit"; "value" = $true }, - @{ "name" = "useGhTokenWorkflow"; "value" = $true }, - @{ "name" = "updateVersionNumber"; "value" = "+0.1" } + $fullFilePaths | Should -Not -BeNullOrEmpty + # Only File2.log should be included + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File2.log") + } + + It 'ResolveFilePaths handles case-insensitive filter matching on Windows' { + # Create files with different case + $upperFile = Join-Path $sourceFolder "folder/UPPER.TXT" + $lowerFile = Join-Path $sourceFolder "folder/lower.txt" + Set-Content -Path $upperFile -Value "upper" + Set-Content -Path $lowerFile -Value "lower" + + try { + $destinationFolder = Join-Path $PSScriptRoot "destinationFolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "*.txt" } ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + # On Windows, both should match due to case-insensitive file system + $fullFilePaths | Should -Not -BeNullOrEmpty + $upperMatch = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $upperFile } + $lowerMatch = $fullFilePaths | Where-Object { $_.sourceFullPath -eq $lowerFile } + $upperMatch | Should -Not -BeNullOrEmpty + $lowerMatch | Should -Not -BeNullOrEmpty + } + finally { + if (Test-Path $upperFile) { Remove-Item -Path $upperFile -Force } + if (Test-Path $lowerFile) { Remove-Item -Path $lowerFile -Force } } + } +} - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" +Describe "ReplaceOwnerRepoAndBranch" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + } - # Verify the defaults were applied - $yaml.Get('on:/workflow_dispatch:/inputs:/directCommit:/default:').content -join '' | Should -Be 'default: true' - $yaml.Get('on:/workflow_dispatch:/inputs:/useGhTokenWorkflow:/default:').content -join '' | Should -Be 'default: true' - $yaml.Get('on:/workflow_dispatch:/inputs:/updateVersionNumber:/default:').content -join '' | Should -Be "default: '+0.1'" + It "Replaces owner, repo, and branch in workflow content" { + $srcContent = [ref]@" +jobs: + build: + uses: microsoft/AL-Go-Actions@main +"@ + $templateOwner = "contoso" + $templateBranch = "dev" + ReplaceOwnerRepoAndBranch -srcContent $srcContent -templateOwner $templateOwner -templateBranch $templateBranch + $srcContent.Value | Should -Be @" +jobs: + build: + uses: contoso/AL-Go/Actions@dev +"@ } +} - It 'ApplyWorkflowDefaultInputs handles empty workflowDefaultInputs array' { - . (Join-Path $scriptRoot "yamlclass.ps1") +Describe "IsDirectALGo" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + } + It "Returns true for direct AL-Go repo URL" { + IsDirectALGo -templateUrl "https://github.com/contoso/AL-Go@main" | Should -Be True + } + It "Returns false for non-direct AL-Go repo URL" { + IsDirectALGo -templateUrl "https://github.com/contoso/OtherRepo@main" | Should -Be False + } +} - # Create a test workflow YAML - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " myInput:", - " type: boolean", - " default: false", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) +Describe "GetFilesToUpdate (general files to update logic)" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - $yaml = [Yaml]::new($yamlContent) - $originalContent = $yaml.content -join "`n" + # Create template folder with test files + $templateFolder = Join-Path $PSScriptRoot "template" + New-Item -ItemType Directory -Path $templateFolder -Force | Out-Null - # Create settings with empty workflowDefaultInputs array - $repoSettings = @{ - "workflowDefaultInputs" = @() - } + New-Item -ItemType Directory -Path (Join-Path $templateFolder "subfolder") -Force | Out-Null - # Apply the defaults - should not throw and should not modify workflow - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $yaml.content -join "`n" | Should -Be $originalContent - $yaml.Get('on:/workflow_dispatch:/inputs:/myInput:/default:').content -join '' | Should -Be 'default: false' - } + $testPSFile = Join-Path $templateFolder "test.ps1" + Set-Content -Path $testPSFile -Value "# test ps file" - It 'ApplyWorkflowDefaultInputs handles workflows without workflow_dispatch' { - . (Join-Path $scriptRoot "yamlclass.ps1") + $testTxtFile = Join-Path $templateFolder "test.txt" + Set-Content -Path $testTxtFile -Value "test txt file" - # Create a test workflow YAML without workflow_dispatch - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " push:", - " branches: [ main ]", - "jobs:", - " test:", - " runs-on: ubuntu-latest", - " steps:", - " - run: echo test" - ) + $testTxtFile2 = Join-Path $templateFolder "test2.txt" + Set-Content -Path $testTxtFile2 -Value "test txt file 2" - $yaml = [Yaml]::new($yamlContent) - $originalContent = $yaml.content -join "`n" + $testSubfolderFile = Join-Path $templateFolder "subfolder/testsub.txt" + Set-Content -Path $testSubfolderFile -Value "test subfolder txt file" - # Create settings with workflow input defaults - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "directCommit"; "value" = $true } - ) - } + $testSubfolderFile2 = Join-Path $templateFolder "subfolder/testsub2.txt" + Set-Content -Path $testSubfolderFile2 -Value "test subfolder txt file 2" - # Apply the defaults - should not throw or modify YAML - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $yaml.content -join "`n" | Should -Be $originalContent + # Display the created files structure for template folder + # . + # ├── test.ps1 + # ├── test.txt + # └── test2.txt + # └── subfolder + # └── testsub.txt } - It 'ApplyWorkflowDefaultInputs handles workflow_dispatch without inputs section' { - . (Join-Path $scriptRoot "yamlclass.ps1") + AfterAll { + if (Test-Path $templateFolder) { + Remove-Item -Path $templateFolder -Recurse -Force + } + } - # Create a test workflow YAML with workflow_dispatch but no inputs - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + It "Returns the correct files to update with filters" { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.ps1" }) + filesToExclude = @() + } + } - $yaml = [Yaml]::new($yamlContent) - $originalContent = $yaml.content -join "`n" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # Create settings with workflow input defaults - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "someInput"; "value" = $true } - ) + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + + # No files to remove + $filesToExclude | Should -BeNullOrEmpty + + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) + filesToExclude = @() + } } - # Apply the defaults - should not throw or modify YAML - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $yaml.content -join "`n" | Should -Be $originalContent + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + + # No files to remove + $filesToExclude | Should -BeNullOrEmpty } - It 'ApplyWorkflowDefaultInputs applies multiple defaults to same workflow' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'Returns the correct files with destinationFolder' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) + filesToExclude = @() + } + } - # Create a test workflow YAML with multiple inputs - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " input1:", - " type: boolean", - " default: false", - " input2:", - " type: number", - " default: 0", - " input3:", - " type: string", - " default: ''", - " input4:", - " type: choice", - " options:", - " - optionA", - " - optionB", - " default: optionA", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $yaml = [Yaml]::new($yamlContent) + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test2.txt') - # Create settings with multiple defaults - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "input1"; "value" = $true }, - @{ "name" = "input2"; "value" = 5 }, - @{ "name" = "input3"; "value" = "test-value" }, - @{ "name" = "input4"; "value" = "optionB" } - ) + # No files to remove + $filesToExclude | Should -BeNullOrEmpty + + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) + filesToExclude = @(@{ filter = "test2.txt" }) + } } - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # Verify all defaults were applied - $yaml.Get('on:/workflow_dispatch:/inputs:/input1:/default:').content -join '' | Should -Be 'default: true' - $yaml.Get('on:/workflow_dispatch:/inputs:/input2:/default:').content -join '' | Should -Be 'default: 5' - $yaml.Get('on:/workflow_dispatch:/inputs:/input3:/default:').content -join '' | Should -Be "default: 'test-value'" - $yaml.Get('on:/workflow_dispatch:/inputs:/input4:/default:').content -join '' | Should -Be "default: 'optionB'" + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + + # One file to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be $testTxtFile2 + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') } - It 'ApplyWorkflowDefaultInputs inserts default line when missing' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'Returns the correct files with destinationName' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) + filesToExclude = @() + } + } - # Create a test workflow YAML with input without default line (only description) - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " myInput:", - " description: 'My input without default'", - " type: string", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $yaml = [Yaml]::new($yamlContent) + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') - # Create settings with default value - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "myInput"; "value" = "new-default" } - ) + # No files to remove + $filesToExclude | Should -BeNullOrEmpty + + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.ps1"; destinationFolder = 'dstPath'; destinationName = "renamed.txt" }) + filesToExclude = @() + } } - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # Verify default line was inserted - $defaultLine = $yaml.Get('on:/workflow_dispatch:/inputs:/myInput:/default:') - $defaultLine | Should -Not -BeNullOrEmpty - $defaultLine.content -join '' | Should -Be "default: 'new-default'" + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') } - It 'ApplyWorkflowDefaultInputs is case-insensitive for input names' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'Return the correct files with types' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.ps1"; type = "script" }) + filesToExclude = @() + } + } - # Create a test workflow YAML with specific casing - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " MyInput:", - " type: boolean", - " default: false", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $yaml = [Yaml]::new($yamlContent) + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testPSFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToInclude[0].type | Should -Be "script" - # Create settings with different casing - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "myInput"; "value" = $true } - ) + # No files to remove + $filesToExclude | Should -BeNullOrEmpty + + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt"; type = "text" }) + filesToExclude = @(@{ filter = "test.txt" }) + } } - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # Verify default WAS applied despite case difference (case-insensitive matching) - $yaml.Get('on:/workflow_dispatch:/inputs:/MyInput:/default:').content -join '' | Should -Be 'default: true' + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 1 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude[0].type | Should -Be "text" + + # One file to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') } - It 'ApplyWorkflowDefaultInputs ignores defaults for non-existent inputs' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'Return the correct files when unusedALGoSystemFiles is specified' { + $settings = @{ + type = "nonPTE" + unusedALGoSystemFiles = @("test.ps1") + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*" }) + filesToExclude = @() + } + } - # Create a test workflow YAML - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " existingInput:", - " type: boolean", - " default: false", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $yaml = [Yaml]::new($yamlContent) - $originalContent = $yaml.content -join "`n" + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - # Create settings with only non-existent input names - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "nonExistentInput"; "value" = "ignored" }, - @{ "name" = "anotherMissingInput"; "value" = 42 } - ) + # One file to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be $testPSFile + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + } + + It 'GetFilesToUpdate with perProject true and empty projects returns no per-project entries' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt"; type = "text"; perProject = $true }) + filesToExclude = @() + } } - # Apply defaults for non-existent inputs - should not throw or modify YAML - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $yaml.content -join "`n" | Should -Be $originalContent + # Pass empty projects array + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects @() + + # Behavior: when projects is empty, no per-project entries should be created + $filesToInclude | Should -BeNullOrEmpty + $filesToExclude | Should -BeNullOrEmpty } - It 'ApplyWorkflowDefaultInputs applies only existing inputs when mixed with non-existent inputs' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'GetFilesToUpdate ignores filesToExclude patterns that do not match any file' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) + filesToExclude = @(@{ filter = "no-match-*.none" }) + } + } - # Create a test workflow YAML - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " existingInput:", - " type: boolean", - " default: false", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $yaml = [Yaml]::new($yamlContent) + # All txt files should be included, no files to exclude + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') - # Create settings with both existing and non-existent input names - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "existingInput"; "value" = $true }, - @{ "name" = "nonExistentInput"; "value" = "ignored" }, - @{ "name" = "anotherMissingInput"; "value" = 42 } - ) - } + $filesToExclude | Should -BeNullOrEmpty + } - # Apply the defaults - should not throw - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw + It 'GetFilesToUpdate duplicates per-project includes for each project including the repository root' { + $perProjectFile = Join-Path $templateFolder "perProjectFile.algo" + Set-Content -Path $perProjectFile -Value "per project" + + try { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "perProjectFile.algo"; perProject = $true; destinationFolder = 'custom' }) + filesToExclude = @() + } + } - # Verify only the existing input was modified - $yaml.Get('on:/workflow_dispatch:/inputs:/existingInput:/default:').content -join '' | Should -Be 'default: true' - } + $projects = @('.', 'ProjectOne') + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects $projects - It 'ApplyWorkflowDefaultInputs handles special YAML characters in string values' { - . (Join-Path $scriptRoot "yamlclass.ps1") + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 - # Create a test workflow YAML - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " input1:", - " type: string", - " default: ''", - " input2:", - " type: string", - " default: ''", - " input3:", - " type: string", - " default: ''", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $rootDestination = Join-Path 'baseFolder' 'custom/perProjectFile.algo' + $projectDestination = Join-Path 'baseFolder' 'ProjectOne/custom/perProjectFile.algo' - $yaml = [Yaml]::new($yamlContent) + $filesToInclude.destinationFullPath | Should -Contain $rootDestination + $filesToInclude.destinationFullPath | Should -Contain $projectDestination - # Create settings with special YAML characters - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "input1"; "value" = "value: with colon" }, - @{ "name" = "input2"; "value" = "value # with comment" }, - @{ "name" = "input3"; "value" = "value with 'quotes' inside" } - ) + $filesToExclude | Should -BeNullOrEmpty + } + finally { + if (Test-Path $perProjectFile) { + Remove-Item -Path $perProjectFile -Force + } } - - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" - - # Verify values are properly quoted and escaped - $yaml.Get('on:/workflow_dispatch:/inputs:/input1:/default:').content -join '' | Should -Be "default: 'value: with colon'" - $yaml.Get('on:/workflow_dispatch:/inputs:/input2:/default:').content -join '' | Should -Be "default: 'value # with comment'" - $yaml.Get('on:/workflow_dispatch:/inputs:/input3:/default:').content -join '' | Should -Be "default: 'value with ''quotes'' inside'" } - It 'ApplyWorkflowDefaultInputs handles environment input type' { - . (Join-Path $scriptRoot "yamlclass.ps1") - - # Create a test workflow YAML with environment type - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " environmentName:", - " description: Environment to deploy to", - " type: environment", - " default: ''", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + It 'GetFilesToUpdate adds custom template settings only when original template folder is provided' { + $customTemplateFolder = Join-Path $PSScriptRoot "customTemplateFolder" + $originalTemplateFolder = Join-Path $PSScriptRoot "originalTemplateFolder" - $yaml = [Yaml]::new($yamlContent) + New-Item -ItemType Directory -Path $customTemplateFolder -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.github') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $customTemplateFolder '.AL-Go') -Force | Out-Null + Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{}' -Encoding UTF8 + Set-Content -Path (Join-Path $customTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{}' -Encoding UTF8 - # Create settings with environment value (should be treated as string) - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "environmentName"; "value" = "production" } - ) - } + New-Item -ItemType Directory -Path $originalTemplateFolder -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.github') -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $originalTemplateFolder '.AL-Go') -Force | Out-Null + Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.github' $RepoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 + Set-Content -Path (Join-Path $originalTemplateFolder (Join-Path '.AL-Go' $ALGoSettingsFileName)) -Value '{"original":true}' -Encoding UTF8 - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + try { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } - # Verify environment value is set as string - $yaml.Get('on:/workflow_dispatch:/inputs:/environmentName:/default:').content -join '' | Should -Be "default: 'production'" - } + $baseFolder = 'baseFolder' + $projects = @('ProjectA') - It 'ApplyWorkflowDefaultInputs validates invalid choice value not in options' { - . (Join-Path $scriptRoot "yamlclass.ps1") + $filesWithoutOriginal, $excludesWithoutOriginal = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $customTemplateFolder -projects $projects - # Create a test workflow YAML with choice input - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " deploymentType:", - " type: choice", - " options:", - " - Development", - " - Staging", - " - Production", - " default: Development", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesWithoutOriginal | Should -Not -BeNullOrEmpty - $yaml = [Yaml]::new($yamlContent) + $repoSettingsDestination = Join-Path $baseFolder (Join-Path '.github' $RepoSettingsFileName) + $projectSettingsRelative = Join-Path 'ProjectA' '.AL-Go' + $projectSettingsRelative = Join-Path $projectSettingsRelative $ALGoSettingsFileName + $projectSettingsDestination = Join-Path $baseFolder $projectSettingsRelative - # Create settings with invalid choice value - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "deploymentType"; "value" = "Testing" } - ) - } + $filesWithoutOriginal.destinationFullPath | Should -Contain $repoSettingsDestination + $filesWithoutOriginal.destinationFullPath | Should -Contain $projectSettingsDestination + $filesWithoutOriginal.destinationFullPath | Should -Not -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateRepoSettingsFileName)) + $filesWithoutOriginal.destinationFullPath | Should -Not -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateProjectSettingsFileName)) - # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*not a valid choice*" - } + $filesWithOriginal, $excludesWithOriginal = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder -projects $projects - It 'ApplyWorkflowDefaultInputs handles inputs without existing default' { - . (Join-Path $scriptRoot "yamlclass.ps1") + $filesWithOriginal | Should -Not -BeNullOrEmpty - # Create a test workflow YAML with input without default - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " myInput:", - " description: My Input", - " required: false", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesWithOriginal.destinationFullPath | Should -Contain $repoSettingsDestination + $filesWithOriginal.destinationFullPath | Should -Contain $projectSettingsDestination + $filesWithOriginal.destinationFullPath | Should -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateRepoSettingsFileName)) + $filesWithOriginal.destinationFullPath | Should -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateProjectSettingsFileName)) - $yaml = [Yaml]::new($yamlContent) + $excludesWithoutOriginal | Should -BeNullOrEmpty + $excludesWithOriginal | Should -BeNullOrEmpty + } + finally { + if (Test-Path $customTemplateFolder) { + Remove-Item -Path $customTemplateFolder -Recurse -Force + } + if (Test-Path $originalTemplateFolder) { + Remove-Item -Path $originalTemplateFolder -Recurse -Force + } + } + } - # Create settings with workflow input defaults - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "myInput"; "value" = "test-value" } - ) + It 'GetFilesToUpdate excludes files that match both include and exclude patterns' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) + filesToExclude = @(@{ filter = "test.txt" }) + } } - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # Verify the default was added - $defaultLine = $yaml.Get('on:/workflow_dispatch:/inputs:/myInput:/default:') - $defaultLine | Should -Not -BeNullOrEmpty - $defaultLine.content -join '' | Should -Be "default: 'test-value'" - } + # test.txt should not be in filesToInclude + $includedTestTxt = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } + $includedTestTxt | Should -BeNullOrEmpty - It 'ApplyWorkflowDefaultInputs handles different value types' { - . (Join-Path $scriptRoot "yamlclass.ps1") + # test.txt should be in filesToExclude + $excludedTestTxt = $filesToExclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } + $excludedTestTxt | Should -Not -BeNullOrEmpty + } - # Create a test workflow YAML - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " boolInput:", - " type: boolean", - " default: false", - " stringInput:", - " type: string", - " default: ''", - " numberInput:", - " type: number", - " default: 0", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + It 'GetFilesToUpdate ignores exclude patterns that do not match any included file' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) + filesToExclude = @(@{ filter = "nonexistent.xyz" }) + } + } - $yaml = [Yaml]::new($yamlContent) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # Create settings with different value types - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "boolInput"; "value" = $true }, - @{ "name" = "stringInput"; "value" = "test" }, - @{ "name" = "numberInput"; "value" = 42 } - ) + # All txt files should be included + $filesToInclude | Should -Not -BeNullOrEmpty + $txtFiles = $filesToInclude | Where-Object { $_.sourceFullPath -like "*.txt" } + $txtFiles.Count | Should -BeGreaterThan 0 + + # Exclude list should not contain the non-matching pattern + $excludedNonExistent = $filesToExclude | Where-Object { $_.sourceFullPath -like "*.xyz" } + $excludedNonExistent | Should -BeNullOrEmpty + } + + It 'GetFilesToUpdate handles overlapping include patterns with different destinations' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @( + @{ filter = "test.txt"; destinationFolder = "folder1" } + @{ filter = "test.txt"; destinationFolder = "folder2" } + ) + filesToExclude = @() + } } - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - # Verify the defaults were applied with correct types - $yaml.Get('on:/workflow_dispatch:/inputs:/boolInput:/default:').content -join '' | Should -Be 'default: true' - $yaml.Get('on:/workflow_dispatch:/inputs:/stringInput:/default:').content -join '' | Should -Be "default: 'test'" - $yaml.Get('on:/workflow_dispatch:/inputs:/numberInput:/default:').content -join '' | Should -Be 'default: 42' + # Should have two entries for test.txt with different destinations + $testTxtFiles = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } + $testTxtFiles.Count | Should -Be 2 + $testTxtFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'folder1/test.txt') + $testTxtFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'folder2/test.txt') } +} - It 'ApplyWorkflowDefaultInputs validates boolean type mismatch' { - . (Join-Path $scriptRoot "yamlclass.ps1") +Describe 'GetFilesToUpdate (real template)' { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - # Create a test workflow YAML with boolean input - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " boolInput:", - " type: boolean", - " default: false", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realPTETemplateFolder', Justification = 'False positive.')] + $realPTETemplateFolder = Join-Path $PSScriptRoot "../Templates/Per Tenant Extension" -Resolve - $yaml = [Yaml]::new($yamlContent) + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realAppSourceAppTemplateFolder', Justification = 'False positive.')] + $realAppSourceAppTemplateFolder = Join-Path $PSScriptRoot "../Templates/AppSource App" -Resolve + } - # Create settings with wrong type (string instead of boolean) - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "boolInput"; "value" = "not a boolean" } - ) + It 'Return the correct files to exclude when type is PTE and powerPlatformSolutionFolder is not empty' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } } - # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*Expected boolean value*" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 24 + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + + # No files to remove + $filesToExclude | Should -BeNullOrEmpty } - It 'ApplyWorkflowDefaultInputs validates number type mismatch' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'Return PP files in filesToExclude when type is PTE but powerPlatformSolutionFolder is empty' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } - # Create a test workflow YAML with number input - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " numberInput:", - " type: number", - " default: 0", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $yaml = [Yaml]::new($yamlContent) + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 21 - # Create settings with wrong type (string instead of number) - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "numberInput"; "value" = "not a number" } - ) + $filesToInclude | ForEach-Object { + $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $_.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") } - # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*Expected number value*" - } - - It 'ApplyWorkflowDefaultInputs validates string type mismatch' { - . (Join-Path $scriptRoot "yamlclass.ps1") + # All PP files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 3 - # Create a test workflow YAML with string input - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " stringInput:", - " type: string", - " default: ''", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/_BuildPowerPlatformSolution.yaml") - $yaml = [Yaml]::new($yamlContent) + $filesToExclude[1].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToExclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PushPowerPlatformChanges.yaml") - # Create settings with wrong type (boolean instead of string) - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "stringInput"; "value" = $true } - ) - } + $filesToExclude[2].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude[2].destinationFullPath | Should -Be (Join-Path 'baseFolder' ".github/workflows/PullPowerPlatformChanges.yaml") - # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*Expected string value*" } - It 'ApplyWorkflowDefaultInputs validates choice type' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'Return the correct files when unusedALGoSystemFiles is specified' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @("Test Next Major.settings.json") + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } - # Create a test workflow YAML with choice input - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " choiceInput:", - " type: choice", - " options:", - " - option1", - " - option2", - " default: option1", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $yaml = [Yaml]::new($yamlContent) + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 23 - # Create settings with correct type (string for choice) - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "choiceInput"; "value" = "option2" } - ) + # Two files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/Test Next Major.settings.json') + } + + It 'Return the correct files when unusedALGoSystemFiles is specified and no PP solution is present' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @("Test Next Major.settings.json", "_BuildPowerPlatformSolution.yaml") + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } } - # Apply the defaults - should succeed - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $yaml.Get('on:/workflow_dispatch:/inputs:/choiceInput:/default:').content -join '' | Should -Be "default: 'option2'" - } + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - It 'ApplyWorkflowDefaultInputs validates choice value is in available options' { - . (Join-Path $scriptRoot "yamlclass.ps1") + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 20 - # Create a test workflow YAML with choice input - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " choiceInput:", - " type: choice", - " options:", - " - option1", - " - option2", - " - option3", - " default: option1", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + # Four files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 4 - $yaml = [Yaml]::new($yamlContent) + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + } - # Create settings with invalid choice value - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "choiceInput"; "value" = "invalidOption" } - ) + It 'Returns the custom template settings files when there is a custom template' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } } - # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*not a valid choice*" - } + $customTemplateFolder = $realPTETemplateFolder + $originalTemplateFolder = $realAppSourceAppTemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template - It 'ApplyWorkflowDefaultInputs validates choice value with case-sensitive matching' { - . (Join-Path $scriptRoot "yamlclass.ps1") + $filesToInclude | Should -Not -BeNullOrEmpty - # Create a test workflow YAML with choice input using mixed case options - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " releaseTypeInput:", - " type: choice", - " options:", - " - Release", - " - Prerelease", - " - Draft", - " default: Release", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + # Check repo settings files + $repoSettingsFiles = $filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".github/AL-Go-Settings.json") } - $yaml = [Yaml]::new($yamlContent) + $repoSettingsFiles | Should -Not -BeNullOrEmpty + $repoSettingsFiles.Count | Should -Be 2 - # Test 1: Exact case match should succeed - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "releaseTypeInput"; "value" = "Prerelease" } - ) - } + $repoSettingsFiles[0].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".github/AL-Go-Settings.json") + $repoSettingsFiles[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-Settings.json') + $repoSettingsFiles[0].type | Should -Be 'settings' - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $yaml.Get('on:/workflow_dispatch:/inputs:/releaseTypeInput:/default:').content -join '' | Should -Be "default: 'Prerelease'" + $repoSettingsFiles[1].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null + $repoSettingsFiles[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') + $repoSettingsFiles[1].type | Should -Be '' - # Test 2: Wrong case should fail with case-sensitive error message - $yaml2 = [Yaml]::new($yamlContent) - $repoSettings2 = @{ - "workflowDefaultInputs" = @( - @{ "name" = "releaseTypeInput"; "value" = "prerelease" } - ) - } + # Check project settings files + $projectSettingsFilesFromCustomTemplate = @($filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $customTemplateFolder ".AL-Go/settings.json") }) - { ApplyWorkflowDefaultInputs -yaml $yaml2 -repoSettings $repoSettings2 -workflowName "Test Workflow" } | - Should -Throw "*case-sensitive match required*" + $projectSettingsFilesFromCustomTemplate | Should -Not -BeNullOrEmpty + $projectSettingsFilesFromCustomTemplate.Count | Should -Be 2 - # Test 3: Uppercase version should also fail - $yaml3 = [Yaml]::new($yamlContent) - $repoSettings3 = @{ - "workflowDefaultInputs" = @( - @{ "name" = "releaseTypeInput"; "value" = "PRERELEASE" } - ) - } + $projectSettingsFilesFromCustomTemplate[0].originalSourceFullPath | Should -Be (Join-Path $originalTemplateFolder ".AL-Go/settings.json") + $projectSettingsFilesFromCustomTemplate[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.AL-Go/settings.json') + $projectSettingsFilesFromCustomTemplate[0].type | Should -Be 'settings' - { ApplyWorkflowDefaultInputs -yaml $yaml3 -repoSettings $repoSettings3 -workflowName "Test Workflow" } | - Should -Throw "*case-sensitive match required*" + $projectSettingsFilesFromCustomTemplate[1].originalSourceFullPath | Should -Be $null # Because origin is 'custom template', originalSourceFullPath should be $null + $projectSettingsFilesFromCustomTemplate[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/AL-Go-TemplateProjectSettings.doNotEdit.json') + $projectSettingsFilesFromCustomTemplate[1].type | Should -Be '' + + # No files to exclude + $filesToExclude | Should -BeNullOrEmpty } - It 'ApplyWorkflowDefaultInputs handles inputs without type specification' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'GetFilesToUpdate handles AppSource template type correctly' { + $settings = @{ + type = "AppSource App" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } - # Create a test workflow YAML without type (defaults to string) - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " noTypeInput:", - " description: Input without type", - " default: ''", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realAppSourceAppTemplateFolder - $yaml = [Yaml]::new($yamlContent) + # PowerPlatform files should be excluded for AppSource App too (same as PTE) + $filesToInclude | Should -Not -BeNullOrEmpty - # Create settings with string value (should work without warning) - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "noTypeInput"; "value" = "string value" } - ) - } + $ppFiles = $filesToInclude | Where-Object { $_.sourceFullPath -like "*BuildPowerPlatformSolution*" } + $ppFiles | Should -BeNullOrEmpty - # Apply the defaults - should succeed - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Not -Throw - $yaml.Get('on:/workflow_dispatch:/inputs:/noTypeInput:/default:').content -join '' | Should -Be "default: 'string value'" + # No files to remove that match PP files as they are not in the template + $ppExcludes = $filesToExclude | Where-Object { $_.sourceFullPath -like "*BuildPowerPlatformSolution*" } + $ppExcludes | Should -BeNullOrEmpty } - It 'ApplyWorkflowDefaultInputs escapes single quotes in string values' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'GetFilesToUpdate with empty unusedALGoSystemFiles array does not exclude any files' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } - # Create a test workflow YAML with string input - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " nameInput:", - " type: string", - " default: ''", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $yaml = [Yaml]::new($yamlContent) + # No additional files should be excluded due to unusedALGoSystemFiles + $ppExcludes = $filesToExclude | Where-Object { $_.sourceFullPath -like "*_BuildPowerPlatformSolution.yaml" -or $_.sourceFullPath -like "*PullPowerPlatformChanges.yaml" -or $_.sourceFullPath -like "*PushPowerPlatformChanges.yaml" } + $ppExcludes.Count | Should -Be 3 # Only PP files should be excluded by default + } - # Create settings with string value containing single quote - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "nameInput"; "value" = "O'Brien" } - ) + It 'GetFilesToUpdate marks settings files with correct type' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } } - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects @('Project1') - # Verify single quote is escaped per YAML spec (doubled) - $yaml.Get('on:/workflow_dispatch:/inputs:/nameInput:/default:').content -join '' | Should -Be "default: 'O''Brien'" + # Check that settings files have type = 'settings' + $repoSettingsFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -like "*$RepoSettingsFileName" -and $_.destinationFullPath -like "*.github*$RepoSettingsFileName" }) + $repoSettingsFiles | Should -Not -BeNullOrEmpty + $repoSettingsFiles[0].type | Should -Be 'settings' + + $projectSettingsFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -like "*$ALGoSettingsFileName" -and $_.destinationFullPath -like "*Project1*.AL-Go*" }) + $projectSettingsFiles | Should -Not -BeNullOrEmpty + $projectSettingsFiles[0].type | Should -Be 'settings' } - It 'ApplyWorkflowDefaultInputs applies last value when multiple entries have same input name' { - . (Join-Path $scriptRoot "yamlclass.ps1") + It 'GetFilesToUpdate handles multiple projects correctly' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } - # Create a test workflow YAML - $yamlContent = @( - "name: 'Test Workflow'", - "on:", - " workflow_dispatch:", - " inputs:", - " input1:", - " type: string", - " default: ''", - " input2:", - " type: boolean", - " default: false", - "jobs:", - " test:", - " runs-on: ubuntu-latest" - ) + $projects = @('ProjectA', 'ProjectB', 'ProjectC') + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects $projects - $yaml = [Yaml]::new($yamlContent) + # Each project should have its own settings file + $projectASettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectA*.AL-Go*" } + $projectBSettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectB*.AL-Go*" } + $projectCSettings = $filesToInclude | Where-Object { $_.destinationFullPath -like "*ProjectC*.AL-Go*" } - # Create settings with duplicate entries for input1 - simulating merged conditional settings - # This can happen when multiple conditionalSettings blocks both match and both define the same input - $repoSettings = @{ - "workflowDefaultInputs" = @( - @{ "name" = "input1"; "value" = "first-value" }, - @{ "name" = "input2"; "value" = $false }, - @{ "name" = "input1"; "value" = "second-value" }, # Duplicate input1 - @{ "name" = "input1"; "value" = "final-value" } # Another duplicate input1 - ) + $projectASettings | Should -Not -BeNullOrEmpty + $projectBSettings | Should -Not -BeNullOrEmpty + $projectCSettings | Should -Not -BeNullOrEmpty + } + + It 'GetFilesToUpdate excludes files correctly when in both unusedALGoSystemFiles and filesToExclude' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @("Test Next Major.settings.json") + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @(@{ filter = "Test Next Major.settings.json" }) + } } - # Apply the defaults - ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - # Verify "last wins" - the final value for input1 should be applied - $yaml.Get('on:/workflow_dispatch:/inputs:/input1:/default:').content -join '' | Should -Be "default: 'final-value'" - $yaml.Get('on:/workflow_dispatch:/inputs:/input2:/default:').content -join '' | Should -Be 'default: false' + # Test Next Major.settings.json should be excluded + $testNextMajor = $filesToInclude | Where-Object { $_.sourceFullPath -like "*Test Next Major.settings.json" } + $testNextMajor | Should -BeNullOrEmpty + + # Should be in exclude list + $excludedTestNextMajor = @($filesToExclude | Where-Object { $_.sourceFullPath -like "*Test Next Major.settings.json" }) + $excludedTestNextMajor | Should -Not -BeNullOrEmpty + $excludedTestNextMajor.Count | Should -Be 1 } } From 27b45d589ce6f91f37492f89fb0e88cd3e705a36 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 12:11:25 +0100 Subject: [PATCH 82/92] Rename GetDefaultFilesToUpdate to GetDefaultFilesToInclude for clarity and update references accordingly --- Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 9884ac78a..e94683834 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -913,7 +913,7 @@ function GetDefaultFilesToInclude { [switch] $includeCustomTemplateFiles ) - $filesToUpdate = @( + $filesToInclude = @( [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = '*.yaml'; 'type' = 'workflow' } [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = '*.yml'; 'type' = 'workflow' } [ordered]@{ 'sourceFolder' = '.github'; 'filter' = '*.copy.md' } @@ -927,13 +927,13 @@ function GetDefaultFilesToInclude { if($includeCustomTemplateFiles) { # If there is an original template folder, we also need to include custom template files (RepoSettings and ProjectSettings) - $filesToUpdate += @( + $filesToInclude += @( [ordered]@{ 'sourceFolder' = ".github"; 'filter' = "$RepoSettingsFileName"; 'destinationName' = "$CustomTemplateRepoSettingsFileName"; 'origin' = 'custom template' } [ordered]@{ 'sourceFolder' = ".AL-Go"; 'filter' = "$ALGoSettingsFileName"; 'destinationFolder' = '.github'; 'destinationName' = "$CustomTemplateProjectSettingsFileName"; 'origin' = 'custom template' } ) } - return $filesToUpdate + return $filesToInclude } function GetDefaultFilesToExclude { From 2f24197b9823275664a5268462cd318b31760eb3 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 14:52:28 +0100 Subject: [PATCH 83/92] Add OutputArray function for formatted array output and deprecate OutputMessageAndArray --- Actions/.Modules/DebugLogHelper.psm1 | 52 ++++++++++++++++++- Actions/AL-Go-Helper.ps1 | 6 +++ .../CheckForUpdates.HelperFunctions.ps1 | 25 +++++---- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/Actions/.Modules/DebugLogHelper.psm1 b/Actions/.Modules/DebugLogHelper.psm1 index 335ac82d0..c3f562d2d 100644 --- a/Actions/.Modules/DebugLogHelper.psm1 +++ b/Actions/.Modules/DebugLogHelper.psm1 @@ -214,5 +214,55 @@ function OutputDebug { } } -Export-ModuleMember -Function OutputColor, OutputDebugFunctionCall, OutputGroupStart, OutputGroupEnd, OutputError, OutputWarning, OutputNotice, MaskValueInLog, OutputDebug +<# + .SYNOPSIS + Outputs each item in an array to the console with optional formatting. + .DESCRIPTION + Outputs each item in an array to the console. An optional formatter script block can be provided to customize the output format of each item. + If a message is provided, it is output before the array items. If the array is empty or null, it outputs "- None". + .PARAMETER Message + An optional message to output before the array items. + .PARAMETER Array + The array of items to output. + .PARAMETER Formatter + An optional script block to format each item in the array. + .PARAMETER Debug + A switch indicating whether to output messages as debug messages. +#> +function OutputArray { + Param( + [string] $Message, + [object[]] $Array, + [scriptblock] $Formatter = { "- $_" }, + [switch] $Debug + ) + + function OutputMessage { + Param( + [string] $Message, + [switch] $Debug + ) + + if ($Debug) { + OutputDebug $Message + } + else { + Write-Host $Message + } + } + + if($Message) { + OutputMessage $Message -Debug:$Debug + } + if (!$Array) { + OutputMessage "- None" -Debug:$Debug + } + else { + $Array | ForEach-Object { + OutputMessage "$(& $Formatter $_)" -Debug:$Debug + } + } +} + +Export-ModuleMember -Function OutputColor, OutputDebugFunctionCall, OutputGroupStart, OutputGroupEnd, OutputError, OutputWarning, OutputNotice, MaskValueInLog, OutputDebug, OutputArray Export-ModuleMember -Variable debugLoggingEnabled diff --git a/Actions/AL-Go-Helper.ps1 b/Actions/AL-Go-Helper.ps1 index 9bd124e56..efd769a75 100644 --- a/Actions/AL-Go-Helper.ps1 +++ b/Actions/AL-Go-Helper.ps1 @@ -2184,6 +2184,12 @@ function ConnectAz { } } +<# + .SYNOPSIS + Output a message and an array of strings in a formatted way. + + Deprecated: Use OutputArray function from DebugLogHelper module. +#> function OutputMessageAndArray { Param( [string] $message, diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index e94683834..e00e90091 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -1,4 +1,5 @@ Import-Module (Join-Path $PSScriptRoot '../.Modules/ReadSettings.psm1') -DisableNameChecking +Import-Module (Join-Path $PSScriptRoot '../.Modules/DebugLogHelper.psm1') -DisableNameChecking <# .SYNOPSIS @@ -993,35 +994,35 @@ function GetFilesToUpdate { $projects = @() ) - OutputDebug "Getting files to update from template folder '$templateFolder', original template folder '$originalTemplateFolder' and base folder '$baseFolder'" + Write-Host "Getting files to update from template folder '$templateFolder', original template folder '$originalTemplateFolder' and base folder '$baseFolder'" $filesToInclude = GetDefaultFilesToInclude -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) $filesToInclude += $settings.customALGoFiles.filesToInclude - $filesToInclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToInclude -projects $projects + $filesToInclude = @(ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToInclude -projects $projects) $filesToExclude = GetDefaultFilesToExclude -settings $settings $filesToExclude += $settings.customALGoFiles.filesToExclude - $filesToExclude = ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExclude -projects $projects + $filesToExclude = @(ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExclude -projects $projects) # Exclude files from filesToExclude that are not in filesToInclude - $filesToExclude = $filesToExclude | Where-Object { + $filesToExclude = @($filesToExclude | Where-Object { $fileToExclude = $_ $include = $filesToInclude | Where-Object { $_.sourceFullPath -eq $fileToExclude.sourceFullPath } if(-not $include) { OutputDebug "Excluding file $($fileToExclude.sourceFullPath) from exclude list as it is not in the include list" } return $include - } + }) # Exclude files from filesToInclude that are in filesToExclude - $filesToInclude = $filesToInclude | Where-Object { + $filesToInclude = @($filesToInclude | Where-Object { $fileToInclude = $_ $include = -not ($filesToExclude | Where-Object { $_.sourceFullPath -eq $fileToInclude.sourceFullPath }) if(-not $include) { OutputDebug "Excluding file $($fileToInclude.sourceFullPath) from include as it is in the exclude list" } return $include - } + }) # Apply unusedALGoSystemFiles logic $unusedALGoSystemFiles = $settings.unusedALGoSystemFiles @@ -1034,16 +1035,14 @@ function GetFilesToUpdate { OutputDebug "The following files are marked as unused and will be removed if they exist:" $unusedFilesToExclude | ForEach-Object { OutputDebug "- $($_.destinationFullPath)" } - $filesToInclude = $filesToInclude | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) } + $filesToInclude = @($filesToInclude | Where-Object { $unusedALGoSystemFiles -notcontains (Split-Path -Path $_.sourceFullPath -Leaf) }) $filesToExclude += @($unusedFilesToExclude) } # List all files to be included and excluded with their source and destination paths, type and original source path (if any) - OutputDebug "Files to include:" - $filesToInclude | ForEach-Object { OutputDebug " -Source: $($_.sourceFullPath), Destination: $($_.destinationFullPath), Type: $($_.type), Original Source: $($_.originalSourceFullPath)" } - - OutputDebug "Files to exclude:" - $filesToExclude | ForEach-Object { OutputDebug " -Source: $($_.sourceFullPath), Destination: $($_.destinationFullPath), Type: $($_.type), Original Source: $($_.originalSourceFullPath)" } + $fileFormatter = { param($file) " -Source: $($file.sourceFullPath), Destination: $($file.destinationFullPath), Type: $($file.type), Original Source: $($file.originalSourceFullPath)"} + OutputArray -Message "Files to include: $($filesToInclude.Count)" -Array $filesToInclude -Formatter $fileFormatter + OutputArray -Message "Files to exclude: $($filesToExclude.Count)" -Array $filesToExclude -Formatter $fileFormatter return @($filesToInclude), @($filesToExclude) } From bc0f3c09a1f21747c7f2160a6d27b75477a991c2 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 14:57:05 +0100 Subject: [PATCH 84/92] Fix indentation for filesToInclude property in settings schema --- Actions/.Modules/settings.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index d1f9ebad6..bd7180fe0 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -698,7 +698,7 @@ "description": "An object containing the custom AL-Go files configuration. See https://aka.ms/ALGoSettings#customALGoFiles", "type": "object", "properties": { - "filesToInclude": { + "filesToInclude": { "type": "array", "items": { "type": "object", From 6a289b02666766ed12498eb8aaeee81183331bd4 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 14:58:57 +0100 Subject: [PATCH 85/92] Fix import path formatting in CheckForUpdates.Action.Test.ps1 --- Tests/CheckForUpdates.Action.Test.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 8c099aedd..38b42c69f 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -1,7 +1,7 @@ Get-Module TestActionsHelper | Remove-Module -Force Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') -Import-Module (Join-Path $PSScriptRoot "..\Actions\TelemetryHelper.psm1") -Import-Module (Join-Path $PSScriptRoot '..\Actions\.Modules\ReadSettings.psm1') +Import-Module (Join-Path $PSScriptRoot "../Actions/TelemetryHelper.psm1") +Import-Module (Join-Path $PSScriptRoot '../Actions/.Modules/ReadSettings.psm1') $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 Describe "CheckForUpdates Action Tests" { From 53b4f029b87de35a3cc6535829330d066294ccf2 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 15:04:10 +0100 Subject: [PATCH 86/92] Add deprecation notice for unusedALGoSystemFiles in RELEASENOTES.md --- RELEASENOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 57375b2de..aebc59c7a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -55,6 +55,10 @@ Read more at [workflowDefaultInputs](https://aka.ms/algosettings#workflowDefault - Issue 1960 Deploy Reference Documentation fails - Discussion 1952 Set default values on workflow_dispatch input +### Deprecations + +- `unusedALGoSystemFiles` will be removed after October 1st 2026. Please use [`customALGoFiles.filesToExclude`](http://aka.ms/algosettings#customALGoFiles) instead. + ## v8.0 ### Mechanism to overwrite complex settings type From da18daf30cf5583a0b48840bafc8d08dfd678c8a Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 15:12:32 +0100 Subject: [PATCH 87/92] Fix URL formatting for customALGoFiles in DEPRECATIONS.md and RELEASENOTES.md --- DEPRECATIONS.md | 2 +- RELEASENOTES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index 16ed42f2c..e20ecf987 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -16,7 +16,7 @@ When handling support requests, we will request that you to use the latest versi ### Setting `unusedALGoSystemFiles` will no longer be supported -The recommended approach is to use the [`customALGoFiles.filesToExclude`](http://aka.ms/algosettings#customALGoFiles) setting to specify files from the AL-Go template that should be excluded from your repositories. +The recommended approach is to use the [`customALGoFiles.filesToExclude`](https://aka.ms/algosettings#customALGoFiles) setting to specify files from the AL-Go template that should be excluded from your repositories. ## Changes in effect after October 1st 2025 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index aebc59c7a..2e6aa0f57 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -57,7 +57,7 @@ Read more at [workflowDefaultInputs](https://aka.ms/algosettings#workflowDefault ### Deprecations -- `unusedALGoSystemFiles` will be removed after October 1st 2026. Please use [`customALGoFiles.filesToExclude`](http://aka.ms/algosettings#customALGoFiles) instead. +- `unusedALGoSystemFiles` will be removed after October 1st 2026. Please use [`customALGoFiles.filesToExclude`](https://aka.ms/algosettings#customALGoFiles) instead. ## v8.0 From 886be0ba06906ac9aff01f0cf506cc8250afa11b Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 16:17:11 +0100 Subject: [PATCH 88/92] Fix formatting in test file --- Tests/CheckForUpdates.Action.Test.ps1 | 145 ++++++++++++-------------- 1 file changed, 69 insertions(+), 76 deletions(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 38b42c69f..03d1cbdd0 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -702,8 +702,7 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { } # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*not a valid choice*" + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Throw "*not a valid choice*" } It 'ApplyWorkflowDefaultInputs handles inputs without existing default' { @@ -811,8 +810,7 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { } # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*Expected boolean value*" + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Throw "*Expected boolean value*" } It 'ApplyWorkflowDefaultInputs validates number type mismatch' { @@ -842,8 +840,7 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { } # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*Expected number value*" + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Throw "*Expected number value*" } It 'ApplyWorkflowDefaultInputs validates string type mismatch' { @@ -873,8 +870,7 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { } # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*Expected string value*" + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Throw "*Expected string value*" } It 'ApplyWorkflowDefaultInputs validates choice type' { @@ -942,8 +938,7 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { } # Apply the defaults - should throw validation error - { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | - Should -Throw "*not a valid choice*" + { ApplyWorkflowDefaultInputs -yaml $yaml -repoSettings $repoSettings -workflowName "Test Workflow" } | Should -Throw "*not a valid choice*" } It 'ApplyWorkflowDefaultInputs validates choice value with case-sensitive matching' { @@ -987,8 +982,7 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { ) } - { ApplyWorkflowDefaultInputs -yaml $yaml2 -repoSettings $repoSettings2 -workflowName "Test Workflow" } | - Should -Throw "*case-sensitive match required*" + { ApplyWorkflowDefaultInputs -yaml $yaml2 -repoSettings $repoSettings2 -workflowName "Test Workflow" } | Should -Throw "*case-sensitive match required*" # Test 3: Uppercase version should also fail $yaml3 = [Yaml]::new($yamlContent) @@ -998,8 +992,7 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { ) } - { ApplyWorkflowDefaultInputs -yaml $yaml3 -repoSettings $repoSettings3 -workflowName "Test Workflow" } | - Should -Throw "*case-sensitive match required*" + { ApplyWorkflowDefaultInputs -yaml $yaml3 -repoSettings $repoSettings3 -workflowName "Test Workflow" } | Should -Throw "*case-sensitive match required*" } It 'ApplyWorkflowDefaultInputs handles inputs without type specification' { @@ -1785,7 +1778,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.ps1" }) + filesToInclude = @(@{ filter = "*.ps1" }) filesToExclude = @() } } @@ -1804,7 +1797,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) + filesToInclude = @(@{ filter = "*.txt" }) filesToExclude = @() } } @@ -1826,7 +1819,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) + filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) filesToExclude = @() } } @@ -1847,12 +1840,12 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) + filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" }) filesToExclude = @(@{ filter = "test2.txt" }) } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 @@ -1871,12 +1864,12 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) + filesToInclude = @(@{ filter = "test.ps1"; destinationName = "renamed.txt" }) filesToExclude = @() } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 @@ -1890,7 +1883,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "test.ps1"; destinationFolder = 'dstPath'; destinationName = "renamed.txt" }) + filesToInclude = @(@{ filter = "test.ps1"; destinationFolder = 'dstPath'; destinationName = "renamed.txt" }) filesToExclude = @() } } @@ -1908,7 +1901,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.ps1"; type = "script" }) + filesToInclude = @(@{ filter = "*.ps1"; type = "script" }) filesToExclude = @() } } @@ -1928,7 +1921,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; type = "text" }) + filesToInclude = @(@{ filter = "*.txt"; type = "text" }) filesToExclude = @(@{ filter = "test.txt" }) } } @@ -1953,19 +1946,19 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "nonPTE" unusedALGoSystemFiles = @("test.ps1") customALGoFiles = @{ - filesToInclude = @(@{ filter = "*" }) + filesToInclude = @(@{ filter = "*" }) filesToExclude = @() } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 2 - $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') - $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 2 + $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') # One file to remove $filesToExclude | Should -Not -BeNullOrEmpty @@ -1979,7 +1972,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt"; type = "text"; perProject = $true }) + filesToInclude = @(@{ filter = "*.txt"; type = "text"; perProject = $true }) filesToExclude = @() } } @@ -1997,7 +1990,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) + filesToInclude = @(@{ filter = "*.txt" }) filesToExclude = @(@{ filter = "no-match-*.none" }) } } @@ -2024,7 +2017,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { type = "NotPTE" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @(@{ filter = "perProjectFile.algo"; perProject = $true; destinationFolder = 'custom' }) + filesToInclude = @(@{ filter = "perProjectFile.algo"; perProject = $true; destinationFolder = 'custom' }) filesToExclude = @() } } @@ -2072,7 +2065,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2118,10 +2111,10 @@ Describe "GetFilesToUpdate (general files to update logic)" { It 'GetFilesToUpdate excludes files that match both include and exclude patterns' { $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) filesToExclude = @(@{ filter = "test.txt" }) } } @@ -2139,10 +2132,10 @@ Describe "GetFilesToUpdate (general files to update logic)" { It 'GetFilesToUpdate ignores exclude patterns that do not match any included file' { $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @(@{ filter = "*.txt" }) + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "*.txt" }) filesToExclude = @(@{ filter = "nonexistent.xyz" }) } } @@ -2161,10 +2154,10 @@ Describe "GetFilesToUpdate (general files to update logic)" { It 'GetFilesToUpdate handles overlapping include patterns with different destinations' { $settings = @{ - type = "NotPTE" - unusedALGoSystemFiles = @() - customALGoFiles = @{ - filesToInclude = @( + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @( @{ filter = "test.txt"; destinationFolder = "folder1" } @{ filter = "test.txt"; destinationFolder = "folder2" } ) @@ -2201,7 +2194,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2224,7 +2217,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2261,21 +2254,21 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @("Test Next Major.settings.json") customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 23 + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 23 - # Two files to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 1 - $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") - $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/Test Next Major.settings.json') + # Two files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 1 + $filesToExclude[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' '.github/Test Next Major.settings.json') } It 'Return the correct files when unusedALGoSystemFiles is specified and no PP solution is present' { @@ -2284,24 +2277,24 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @("Test Next Major.settings.json", "_BuildPowerPlatformSolution.yaml") customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder - $filesToInclude | Should -Not -BeNullOrEmpty - $filesToInclude.Count | Should -Be 20 + $filesToInclude | Should -Not -BeNullOrEmpty + $filesToInclude.Count | Should -Be 20 - # Four files to remove - $filesToExclude | Should -Not -BeNullOrEmpty - $filesToExclude.Count | Should -Be 4 + # Four files to remove + $filesToExclude | Should -Not -BeNullOrEmpty + $filesToExclude.Count | Should -Be 4 - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/Test Next Major.settings.json") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") } It 'Returns the custom template settings files when there is a custom template' { @@ -2310,7 +2303,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = "PowerPlatformSolution" unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2359,7 +2352,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2383,7 +2376,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2401,7 +2394,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2424,7 +2417,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @() customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @() } } @@ -2448,7 +2441,7 @@ Describe 'GetFilesToUpdate (real template)' { powerPlatformSolutionFolder = '' unusedALGoSystemFiles = @("Test Next Major.settings.json") customALGoFiles = @{ - filesToInclude = @() + filesToInclude = @() filesToExclude = @(@{ filter = "Test Next Major.settings.json" }) } } From 2f1398bf05d6ad1487a73d612625dcedd7117221 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 16:26:38 +0100 Subject: [PATCH 89/92] Attempt to fix PSScriptAnalyzer issue --- Tests/CheckForUpdates.Action.Test.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 03d1cbdd0..818e6689a 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -33,7 +33,7 @@ Describe "CheckForUpdates Action Tests" { } } -Describe('YamlClass Tests') { +Describe "YamlClass Tests" { BeforeAll { $actionName = "CheckForUpdates" [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'scriptRoot', Justification = 'False positive.')] @@ -2175,7 +2175,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } -Describe 'GetFilesToUpdate (real template)' { +Describe "GetFilesToUpdate (real template)" { BeforeAll { $actionName = "CheckForUpdates" $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve From 2291a990f77d06dd0f5ff0704be3a39f6fee2d15 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 18:23:07 +0100 Subject: [PATCH 90/92] Fix variable suppression messages in CheckForUpdates.Action tests --- Tests/CheckForUpdates.Action.Test.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 818e6689a..11e631082 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -212,9 +212,9 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" { $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve Import-Module (Join-Path $scriptRoot "..\Github-Helper.psm1") -DisableNameChecking -Force . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'tmpSrcFile', Justification = 'False positive.')] $tmpSrcFile = Join-Path $PSScriptRoot "tempSrcFile.json" - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'actionScript', Justification = 'False positive.')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'tmpDstFile', Justification = 'False positive.')] $tmpDstFile = Join-Path $PSScriptRoot "tempDestFile.json" } From 4f976563b280bebb1000c1e78711773d6e013026 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 18:46:23 +0100 Subject: [PATCH 91/92] Fix whitespace issue in CheckForUpdates.Action test file --- Tests/CheckForUpdates.Action.Test.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 11e631082..ba31d127c 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -1,4 +1,4 @@ -Get-Module TestActionsHelper | Remove-Module -Force +Get-Module TestActionsHelper | Remove-Module -Force Import-Module (Join-Path $PSScriptRoot 'TestActionsHelper.psm1') Import-Module (Join-Path $PSScriptRoot "../Actions/TelemetryHelper.psm1") Import-Module (Join-Path $PSScriptRoot '../Actions/.Modules/ReadSettings.psm1') From 599481b6e992005c69c5622289e4f33ce6005726 Mon Sep 17 00:00:00 2001 From: Maria Zhelezova Date: Mon, 24 Nov 2025 21:15:34 +0100 Subject: [PATCH 92/92] Add telemetry logging for custom AL-Go files usage in GetFilesToUpdate function --- .../CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index e00e90091..82a8d5d22 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -996,6 +996,14 @@ function GetFilesToUpdate { Write-Host "Getting files to update from template folder '$templateFolder', original template folder '$originalTemplateFolder' and base folder '$baseFolder'" + # Send telemetery about customALGoFiles usage + if ($settings.customALGoFiles.filesToInclude.Count -gt 0) { + Trace-Information -Message "Usage: Custom AL-Go Files (Include)" + } + if ($settings.customALGoFiles.filesToExclude.Count -gt 0) { + Trace-Information -Message "Usage: Custom AL-Go Files (Exclude)" + } + $filesToInclude = GetDefaultFilesToInclude -includeCustomTemplateFiles:$($null -ne $originalTemplateFolder) $filesToInclude += $settings.customALGoFiles.filesToInclude $filesToInclude = @(ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToInclude -projects $projects)