diff --git a/.github/workflows/E2E.yaml b/.github/workflows/E2E.yaml
index d8905a2f5..dbe82eab2 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") | ForEach-Object { $_.Directory.Name })
+ $filteredScenarios = $allScenarios | Where-Object { $scenario = $_; $scenariosFilter | ForEach-Object { $scenario -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" = $_ } })
};
"max-parallel" = $maxParallel
"fail-fast" = $false
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/.Modules/ReadSettings.psm1 b/Actions/.Modules/ReadSettings.psm1
index 8c80cbb36..3e483e429 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'
+$ALGoSettingsFileName = 'settings.json'
+$ALGoSettingsFile = Join-Path '.AL-Go' $ALGoSettingsFileName
+
+$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(
@@ -242,6 +249,10 @@ function GetDefaultSettings
"shortLivedArtifactsRetentionDays" = 1 # 0 means use GitHub default
"reportSuppressedDiagnostics" = $false
"workflowDefaultInputs" = @()
+ "customALGoFiles" = [ordered]@{
+ "filesToInclude" = @()
+ "filesToExclude" = @()
+ }
}
}
@@ -610,4 +621,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/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json
index 4e36caa21..bd7180fe0 100644
--- a/Actions/.Modules/settings.schema.json
+++ b/Actions/.Modules/settings.schema.json
@@ -694,6 +694,52 @@
"type": "boolean",
"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": {
+ "filesToInclude": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "sourceFolder": {
+ "type": "string",
+ "description": "The source folder from which to update files, relative to the template repository root."
+ },
+ "filter": {
+ "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",
+ "description": "Indicates whether the file update should be applied per project. In that case, the destinationFolder is considered relative to each project folder."
+ }
+ }
+ }
+ },
+ "filesToExclude": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "sourceFolder": {
+ "type": "string",
+ "description": "The source folder from which to exclude files, relative to the template repository root."
+ },
+ "filter": {
+ "type": "string",
+ "description": "A filter string to select which files to exclude. It can contain '*' and '?' wildcards."
+ }
+ }
+ }
+ }
+ }
+ },
"workflowDefaultInputs": {
"type": "array",
"items": {
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 4d264298c..82a8d5d22 100644
--- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
+++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
@@ -1,3 +1,6 @@
+Import-Module (Join-Path $PSScriptRoot '../.Modules/ReadSettings.psm1') -DisableNameChecking
+Import-Module (Join-Path $PSScriptRoot '../.Modules/DebugLogHelper.psm1') -DisableNameChecking
+
<#
.SYNOPSIS
Downloads a template repository and returns the path to the downloaded folder
@@ -61,6 +64,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
}
@@ -472,8 +476,7 @@ function GetWorkflowContentWithChangesFromSettings {
Param(
[string] $srcFile,
[hashtable] $repoSettings,
- [int] $depth,
- [bool] $includeBuildPP
+ [int] $depth
)
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($srcFile)
@@ -554,6 +557,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
}
@@ -629,11 +633,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 ''
}
@@ -732,3 +734,323 @@ 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)]
+ [string] $sourceFolder,
+ [string] $originalSourceFolder = $null,
+ [Parameter(Mandatory=$true)]
+ [string] $destinationFolder,
+ [array] $files = @(),
+ [string[]] $projects = @()
+ )
+
+ if(-not $files) {
+ return @()
+ }
+
+ $fullFilePaths = @()
+ foreach($file in $files) {
+ if($file.Keys -notcontains 'sourceFolder') {
+ $file.sourceFolder = '' # Default to current folder
+ }
+
+ if($file.Keys -notcontains 'filter') {
+ $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
+ }
+
+ 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') {
+ $file.perProject = $false # Default to false
+ }
+
+ # 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') {
+ 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
+ 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)
+
+ OutputDebug "Found $($sourceFiles.Count) files for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder', origin '$($file.origin)')"
+
+ if(-not $sourceFiles) {
+ OutputDebug "No files found for filter '$($file.filter)' in folder '$($file.sourceFolder)' (relative to folder '$sourceFolder')"
+ continue
+ }
+
+ foreach($srcFile in $sourceFiles) {
+ $fullFilePath = @{
+ 'sourceFullPath' = $srcFile
+ 'originalSourceFullPath' = $null
+ 'type' = $file.type
+ 'destinationFullPath' = $null
+ }
+
+ # Check if the source file is under the source folder
+ if ($srcFile -notlike "$sourceFolder*") {
+ OutputDebug "Skipping source file '$($srcFile)' as it is not under the source folder '$($sourceFolder)'."
+ continue
+ }
+
+ 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')) {
+ 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
+ }
+ }
+
+ $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) {
+ # Multiple file entries, one for each project
+ # Destination full path is the destination base folder + project + destinationFolder + destinationName
+
+ foreach($project in $projects) {
+ if($project -eq '.') {
+ $project = '' # If project is '.', it means the root folder, so we use an empty string
+ }
+
+ $fullProjectFilePath = $fullFilePath.Clone()
+
+ $fullProjectFilePath.destinationFullPath = Join-Path $destinationFolder $project
+ $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $file.destinationFolder
+ $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $destinationName
+
+ if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullProjectFilePath.destinationFullPath) {
+ OutputDebug "Skipping duplicate per-project file for project '$project': destinationFullPath '$($fullProjectFilePath.destinationFullPath)' already exists"
+ continue
+ }
+ OutputDebug "Adding per-project file for project '$project': sourceFullPath '$($fullProjectFilePath.sourceFullPath)', originalSourceFullPath '$($fullProjectFilePath.originalSourceFullPath)', destinationFullPath '$($fullProjectFilePath.destinationFullPath)'"
+ $fullFilePaths += $fullProjectFilePath
+ }
+ }
+ else {
+ # Single file entry
+ # Destination full path is the destination base folder + destinationFolder + destinationName
+
+ $fullFilePath.destinationFullPath = Join-Path $destinationFolder $file.destinationFolder
+ $fullFilePath.destinationFullPath = Join-Path $fullFilePath.destinationFullPath $destinationName
+
+ if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullFilePath.destinationFullPath) {
+ OutputDebug "Skipping duplicate file: destinationFullPath '$($fullFilePath.destinationFullPath)' already exists"
+ continue
+ }
+ OutputDebug "Adding file: sourceFullPath '$($fullFilePath.sourceFullPath)', originalSourceFullPath '$($fullFilePath.originalSourceFullPath)', destinationFullPath '$($fullFilePath.destinationFullPath)'"
+ $fullFilePaths += $fullFilePath
+ }
+ }
+ }
+
+ return @($fullFilePaths)
+}
+
+function GetDefaultFilesToInclude {
+ Param(
+ [switch] $includeCustomTemplateFiles
+ )
+
+ $filesToInclude = @(
+ [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)
+ $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 $filesToInclude
+}
+
+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]@{ 'sourceFolder' = '.github/workflows'; 'filter' = '_BuildPowerPlatformSolution.yaml' }
+ [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = 'PushPowerPlatformChanges.yaml' }
+ [ordered]@{ 'sourceFolder' = '.github/workflows'; 'filter' = 'PullPowerPlatformChanges.yaml' }
+ )
+ }
+
+ return @($filesToExclude)
+}
+
+<#
+.SYNOPSIS
+ 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 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
+ 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).
+ 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 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).
+ - 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(
+ [Parameter(Mandatory=$true)]
+ $settings,
+ [Parameter(Mandatory=$true)]
+ $baseFolder,
+ [Parameter(Mandatory=$true)]
+ $templateFolder,
+ $originalTemplateFolder = $null,
+ $projects = @()
+ )
+
+ 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)
+
+ $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 filesToInclude
+ $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 {
+ $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
+
+ # 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"
+
+ 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)
+ $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)
+}
diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1
index 8cf3511f2..46a95da70 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 '') {
@@ -68,6 +62,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]
@@ -76,7 +71,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) {
@@ -99,6 +94,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
@@ -106,52 +103,23 @@ if (-not $isDirectALGo) {
$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
+ $isDirectALGo = IsDirectALGo -templateUrl $originalTemplateUrl
+ if ($isDirectALGo) {
+ Trace-Information -Message "Original template repository is direct AL-Go"
}
}
}
}
-# 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' }
- )
-}
+
+$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 = @()
+
# $removeFiles will hold an array of files, which needs to be removed
$removeFiles = @()
@@ -166,121 +134,82 @@ 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($fileToInclude in $filesToInclude) {
+ $type = $fileToInclude.type
+ $srcPath = $fileToInclude.sourceFullPath
+ $originalSrcPath = $fileToInclude.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 = $fileToInclude.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)
- }
+ Write-Host "Processing file: $srcPath -> $dstPath (type: $type)"
- if ($dstFileExists) {
- if ($type -eq 'workflow') {
- Write-Host "Apply customizations from current repository, file: $dstFile"
- [Yaml]::ApplyFinalCustomizations([ref] $srcContent, $dstFile)
- }
+ 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
+ # Replace static placeholders
+ $srcContent = $srcContent.Replace('{TEMPLATEURL}', $templateUrl)
+ }
+ "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
+ }
+ }
- # 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 ($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
+ }
+
+ 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)
+ }
+
+ # 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) {
+ if ($type -eq 'workflow') {
+ 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 file $relativeDstPath"
+ $updateFiles += @{ "DstFile" = $relativeDstPath; "content" = $srcContent }
}
- finally {
- Pop-Location
+ else {
+ Write-Host "No updates for file $relativeDstPath"
}
}
+ else {
+ # new file, add to $updateFiles
+ Write-Host "New file available: $relativeDstPath"
+ $updateFiles += @{ "DstFile" = $relativeDstPath; "content" = $srcContent }
+ }
+}
+
+Push-Location -Path $baseFolder
+# 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
}
-$removeFiles = @($removeFiles | Select-Object -Unique)
+Pop-Location
if ($update -ne 'Y') {
# $update not set, just issue a warning in the CI/CD workflow that updates are available
@@ -317,7 +246,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
@@ -328,7 +257,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)) {
diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md
index bdf268968..e20ecf987 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`](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 29735d63d..de44f3d8d 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).
+
### Set default values for workflow inputs
A new setting `workflowDefaultInputs` allows you to configure default values for workflow_dispatch inputs. This makes it easier to run workflows manually with consistent settings across your team.
@@ -52,6 +56,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`](https://aka.ms/algosettings#customALGoFiles) instead.
+
## v8.0
### Mechanism to overwrite complex settings type
diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md
index beb2029ed..646dcfab8 100644
--- a/Scenarios/CustomizingALGoForGitHub.md
+++ b/Scenarios/CustomizingALGoForGitHub.md
@@ -201,6 +201,129 @@ 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 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: `filesToInclude` and `filesToExclude`.
+
+`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`.
+- `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]
+> `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 `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 `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 `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 |
+| 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 `filesToInclude` and `filesToExclude` settings in your AL-Go configuration.
+
+#### Example 1: Updating specific scripts for all projects
+
+```json
+"customALGoFiles": {
+ "filesToInclude": [
+ {
+ "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": {
+ "filesToInclude": [
+ {
+ "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": {
+ "filesToInclude": [],
+ "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.
+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": {
+ "filesToInclude": [
+ {
+ "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.
+
+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
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 5fe3bba5b..193e54878 100644
--- a/Scenarios/settings.md
+++ b/Scenarios/settings.md
@@ -119,7 +119,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 |
@@ -239,8 +239,9 @@ 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 `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 8455e9330..ba31d127c 100644
--- a/Tests/CheckForUpdates.Action.Test.ps1
+++ b/Tests/CheckForUpdates.Action.Test.ps1
@@ -1,7 +1,7 @@
-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') -Force
+Import-Module (Join-Path $PSScriptRoot '../Actions/.Modules/ReadSettings.psm1')
$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0
Describe "CheckForUpdates Action Tests" {
@@ -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.')]
@@ -81,11 +81,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
@@ -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"
}
@@ -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' {
@@ -1107,3 +1100,1361 @@ Describe "CheckForUpdates Action: CheckForUpdates.HelperFunctions.ps1" {
$yaml.Get('on:/workflow_dispatch:/inputs:/input2:/default:').content -join '' | Should -Be 'default: false'
}
}
+
+Describe "ResolveFilePaths" {
+ BeforeAll {
+ $actionName = "CheckForUpdates"
+ $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve
+ . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1")
+
+ $rootFolder = $PSScriptRoot
+
+ $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
+
+ $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
+
+ # 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
+ }
+
+ if (Test-Path $originalSourceFolder) {
+ Remove-Item -Path $originalSourceFolder -Recurse -Force
+ }
+ }
+
+ 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 '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" }
+ )
+
+ $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 ''
+ }
+
+ 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
+
+ # 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' {
+ $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" }
+ )
+
+ $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 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
+
+ $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 '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"; }
+ )
+
+ $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' {
+ $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
+
+ # 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"
+
+ # 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"
+
+ # 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: 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"
+
+ # 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"
+ }
+
+ 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 }
+ }
+
+ 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
+ }
+
+ 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")
+ }
+
+ 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" {
+ 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 (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
+
+ 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 = @{
+ type = "NotPTE"
+ unusedALGoSystemFiles = @()
+ customALGoFiles = @{
+ filesToInclude = @(@{ filter = "*.ps1" })
+ 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')
+
+ # No files to remove
+ $filesToExclude | Should -BeNullOrEmpty
+
+ $settings = @{
+ type = "NotPTE"
+ unusedALGoSystemFiles = @()
+ customALGoFiles = @{
+ filesToInclude = @(@{ filter = "*.txt" })
+ 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')
+
+ # No files to remove
+ $filesToExclude | Should -BeNullOrEmpty
+ }
+
+ It 'Returns the correct files with destinationFolder' {
+ $settings = @{
+ type = "NotPTE"
+ unusedALGoSystemFiles = @()
+ customALGoFiles = @{
+ filesToInclude = @(@{ filter = "*.txt"; destinationFolder = "customFolder" })
+ 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' '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
+
+ $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
+
+ $filesToInclude | Should -Not -BeNullOrEmpty
+
+ # Check repo settings files
+ $repoSettingsFiles = $filesToInclude | 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 $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 $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 = @($filesToInclude | 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 (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 $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 '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
+ }
+}
diff --git a/Tests/ReadSettings.Test.ps1 b/Tests/ReadSettings.Test.ps1
index 8443a5fb1..24fdb69c9 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)' {
diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1
index 9b7594653..5cdef971b 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,40 @@ $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)
+$customWorkflowfileRelativePath = '.github/workflows/CustomWorkflow.yaml'
+$customWorkflowFile = Join-Path $templateRepoPath $customWorkflowfileRelativePath
+$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
+
+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
+$customFileContent = "This is a custom file in the template repository."
+Set-Content -Path $customFile -Value $customFileContent
+
# Push
CommitAndPush -commitMessage 'Add template customizations'
@@ -227,6 +261,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) $customWorkflowfileRelativePath) | Should -Exist
+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
+
+# Add custom file to be copied via settings
+$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]'
+
+# 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) $customFileName) | Should -Exist
+Get-ContentLF -Path (Join-Path (Get-Location) $customFileName)| Should -Be $customFileContent.Replace("`r", "").TrimEnd("`n")
+
# Run CICD
$run = RunCICD -repository $repository -branch $branch -wait