diff --git a/Actions/.Modules/settings.schema.json b/Actions/.Modules/settings.schema.json index 4a1cd415d3..7251869715 100644 --- a/Actions/.Modules/settings.schema.json +++ b/Actions/.Modules/settings.schema.json @@ -743,6 +743,7 @@ "type": "object", "properties": { "filesToInclude": { + "description": "An array of file specifications to include in the update. Files that match these specifications are copied from the template to the repository. When used in a custom template's settings, inclusions are also propagated from the original template to consumer repos even if the files no longer exist in the custom template.", "type": "array", "items": { "type": "object", @@ -759,6 +760,10 @@ "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." }, + "destinationName": { + "type": "string", + "description": "The filename to use at the destination. If specified, overrides the source filename, allowing the file to be renamed when copied. Should be used together with a filter that matches a single file." + }, "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." @@ -767,6 +772,7 @@ } }, "filesToExclude": { + "description": "An array of file specifications to exclude from the update. Files that match these specifications are not copied from the template to the repository. When used in a custom template's settings, exclusions are also propagated from the original template to consumer repos even if the files no longer exist in the custom template.", "type": "array", "items": { "type": "object", diff --git a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 index 29e6dbdb09..dc99a8f852 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 @@ -827,6 +827,17 @@ function ResolveFilePaths { return @() } + $sourceFolder = [System.IO.Path]::GetFullPath($sourceFolder) # Canonicalize the source folder to an absolute path + $sourceFolder = Join-Path $sourceFolder '' # Ensure source folder has a trailing slash for correct path resolution + + $destinationFolder = [System.IO.Path]::GetFullPath($destinationFolder) # Canonicalize the destination folder to an absolute path + $destinationFolder = Join-Path $destinationFolder '' # Ensure destination folder has a trailing slash for correct path resolution + + $pathComparison = [System.StringComparison]::OrdinalIgnoreCase + if ($PSVersionTable.PSVersion.Major -ge 6 -and ($IsLinux -or $IsMacOS)) { + $pathComparison = [System.StringComparison]::Ordinal + } + $fullFilePaths = @() foreach($file in $files) { if($file.Keys -notcontains 'sourceFolder') { @@ -880,7 +891,7 @@ function ResolveFilePaths { } # Check if the source file is under the source folder - if ($srcFile -notlike "$sourceFolder*") { + if (-not $srcFile.StartsWith($sourceFolder, $pathComparison)) { OutputDebug "Skipping source file '$($srcFile)' as it is not under the source folder '$($sourceFolder)'." continue } @@ -912,11 +923,19 @@ function ResolveFilePaths { $project = '' # If project is '.', it means the root folder, so we use an empty string } + $fileDestinationFolder = Join-Path $destinationFolder $project + $fileDestinationFolder = Join-Path $fileDestinationFolder $file.destinationFolder + $fileDestinationFolder = Join-Path $fileDestinationFolder '' # Ensure file destination folder has a trailing slash for correct path resolution + $fullProjectFilePath = $fullFilePath.Clone() + $fullProjectFilePath.destinationFullPath = Join-Path $fileDestinationFolder $destinationName + $fullProjectFilePath.destinationFullPath = [System.IO.Path]::GetFullPath($fullProjectFilePath.destinationFullPath) # Canonicalize the destination full path to an absolute path - $fullProjectFilePath.destinationFullPath = Join-Path $destinationFolder $project - $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $file.destinationFolder - $fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $destinationName + # Check if the destination file is under the file destination folder + if (-not $fullProjectFilePath.destinationFullPath.StartsWith($fileDestinationFolder, $pathComparison)) { + OutputWarning "Skipping file '$srcFile' for project '$project': destination file '$($fullProjectFilePath.destinationFullPath)' is outside the destination folder '$fileDestinationFolder'." + continue + } if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullProjectFilePath.destinationFullPath) { OutputDebug "Skipping duplicate per-project file for project '$project': destinationFullPath '$($fullProjectFilePath.destinationFullPath)' already exists" @@ -930,8 +949,17 @@ function ResolveFilePaths { # 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 + $fileDestinationFolder = Join-Path $destinationFolder $file.destinationFolder + $fileDestinationFolder = Join-Path $fileDestinationFolder '' # Ensure file destination folder has a trailing slash for correct path resolution + + $fullFilePath.destinationFullPath = Join-Path $fileDestinationFolder $destinationName + $fullFilePath.destinationFullPath = [System.IO.Path]::GetFullPath($fullFilePath.destinationFullPath) # Canonicalize the destination full path to an absolute path + + # Check if the destination file is under the file destination folder + if (-not $fullFilePath.destinationFullPath.StartsWith($fileDestinationFolder, $pathComparison)) { + OutputWarning "Skipping file '$srcFile': destination file '$($fullFilePath.destinationFullPath)' is outside the destination folder '$fileDestinationFolder'." + continue + } if($fullFilePaths -and $fullFilePaths.destinationFullPath -contains $fullFilePath.destinationFullPath) { OutputDebug "Skipping duplicate file: destinationFullPath '$($fullFilePath.destinationFullPath)' already exists" @@ -994,12 +1022,70 @@ function GetDefaultFilesToExclude { return @($filesToExclude) } +<# +.SYNOPSIS + Reads settings using the current custom template repository settings without changing the workspace. +.DESCRIPTION + Temporarily refreshes the custom template repository settings snapshot, reads the merged settings, and restores + the snapshot to its original state. This allows the current template settings to affect the current run while + preserving the workspace state for the normal update comparison. +.PARAMETER baseFolder + The base folder of the repository whose settings are read. +.PARAMETER templateFolder + The folder where the custom template files are located. +#> +function ReadSettingsWithCurrentCustomTemplateRepoSettings { + Param( + [Parameter(Mandatory=$true)] + [string] $baseFolder, + [Parameter(Mandatory=$true)] + [string] $templateFolder + ) + + $templateFolderRepoSettingsPath = Join-Path $templateFolder $RepoSettingsFile + + $baseFolderTemplateSettingsPath = Join-Path $baseFolder $CustomTemplateRepoSettingsFile + $baseFolderTemplateSettingsBackupPath = $null + + if (Test-Path -LiteralPath $baseFolderTemplateSettingsPath -PathType Leaf) { + $baseFolderTemplateSettingsBackupPath = Join-Path (GetTemporaryPath) ([Guid]::NewGuid().ToString()) + Copy-Item -LiteralPath $baseFolderTemplateSettingsPath -Destination $baseFolderTemplateSettingsBackupPath -Force + } + + try { + if (Test-Path -LiteralPath $templateFolderRepoSettingsPath -PathType Leaf) { + Copy-Item -LiteralPath $templateFolderRepoSettingsPath -Destination $baseFolderTemplateSettingsPath -Force + } + return ReadSettings -baseFolder $baseFolder -buildMode '' -project '' -workflowName '' -userName '' -branchName '' -trigger '' | ConvertTo-HashTable -recurse + } + finally { + if ($baseFolderTemplateSettingsBackupPath) { + Copy-Item -LiteralPath $baseFolderTemplateSettingsBackupPath -Destination $baseFolderTemplateSettingsPath -Force + Remove-Item -LiteralPath $baseFolderTemplateSettingsBackupPath -Force + } + elseif (Test-Path -LiteralPath $baseFolderTemplateSettingsPath -PathType Leaf) { + Remove-Item -LiteralPath $baseFolderTemplateSettingsPath -Force + } + } +} + <# .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. + Builds two lists by merging defaults, repository settings, and the original AL-Go template (if given): + + 1. filesToInclude: Files to copy from the template or original template to the destination. + Built from default files to include and customALGoFiles.filesToInclude in settings, resolved against the template folder and original template folder (if any). + 2. filesToExclude: Files to skip from copying; if they already exist in the destination they should be deleted. + Built from default files to exclude and customALGoFiles.filesToExclude in settings, resolved against the template folder and original template folder (if any). + + Note: when a custom template is in use, the caller is expected to call + ReadSettingsWithCurrentCustomTemplateRepoSettings before this function, so that the template's + customALGoFiles/unusedALGoSystemFiles are already merged into settings. + + The deprecated unusedALGoSystemFiles setting is also applied: matching files are moved from filesToInclude to + filesToExclude with a deprecation warning. .PARAMETER settings The settings object containing the customALGoFiles configuration. .PARAMETER baseFolder @@ -1007,15 +1093,17 @@ function GetDefaultFilesToExclude { .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. + The folder where the original AL-Go template files are located (if any). + When provided, it signals that a custom template is in use. Both filesToInclude and filesToExclude specs are + resolved against this folder in addition to templateFolder; entries not already covered by originalSourceFullPath + tracking are appended to propagate upstream template additions and deletions to consumer repositories. .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. + - sourceFullPath: The full path to the source file. - 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. @@ -1032,6 +1120,7 @@ function GetFilesToUpdate { $projects = @() ) + $hasOriginalTemplate = $null -ne $originalTemplateFolder Write-Host "Getting files to update from template folder '$templateFolder', original template folder '$originalTemplateFolder' and base folder '$baseFolder'" # Send telemetery about customALGoFiles usage @@ -1041,32 +1130,38 @@ function GetFilesToUpdate { 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 + # Determine files to include + $filesToIncludeUnresolved = GetDefaultFilesToInclude -includeCustomTemplateFiles:$hasOriginalTemplate + $filesToIncludeUnresolved += $settings.customALGoFiles.filesToInclude + $filesToInclude = @(ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToIncludeUnresolved -projects $projects) + if ($hasOriginalTemplate) { + $filesToInclude += @(ResolveFilePaths -sourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToIncludeUnresolved -projects $projects) + } + # Deduplicate files to include based on destinationFullPath, keeping the first one (default > settings; template folder > original template folder) + $filesToInclude = @($filesToInclude | Group-Object { $_.destinationFullPath } | Sort-Object -Property Name | ForEach-Object { $_.Group[0] }) + + # Determine files to exclude + $filesToExcludeUnresolved = GetDefaultFilesToExclude -settings $settings + $filesToExcludeUnresolved += $settings.customALGoFiles.filesToExclude + $filesToExclude = @(ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExcludeUnresolved -projects $projects) + if ($hasOriginalTemplate) { + $filesToExclude += @(ResolveFilePaths -sourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToExcludeUnresolved -projects $projects) + } + # filesToExclude is not deduplicated by destinationFullPath here. + # Its destinationFullPath is never part of the actual output; only sourceFullPath is used below to match against filesToInclude. + + # Map files from filesToExclude to files that are in filesToInclude (based on source) + # Settings for filesToExclude only define the sources (sourceFolder and filter) but not the destinations (destinationFolder, destinationName and perProject) + $filesToExclude = @($filesToInclude | Where-Object { + $fileToInclude = $_ + return $filesToExclude | Where-Object { $_.sourceFullPath -eq $fileToInclude.sourceFullPath } }) - # Exclude files from filesToInclude that are in filesToExclude + # Exclude files from filesToInclude that are in filesToExclude (based on source) $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" - } + $file = $_ + $include = -not ($filesToExclude | Where-Object { $_.sourceFullPath -eq $file.sourceFullPath }) + if (-not $include) { OutputDebug "Excluding source file '$($file.sourceFullPath)' from include list as it is in the exclude list" } return $include }) diff --git a/Actions/CheckForUpdates/CheckForUpdates.ps1 b/Actions/CheckForUpdates/CheckForUpdates.ps1 index 46a95da706..3025bf2f6b 100644 --- a/Actions/CheckForUpdates/CheckForUpdates.ps1 +++ b/Actions/CheckForUpdates/CheckForUpdates.ps1 @@ -52,7 +52,7 @@ if ($token) { # if $downloadLatest is set to true, CheckForUpdates will download the latest version of the template repository, else it will use the templateSha setting in the .github/AL-Go-Settings file # 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 +$repoSettings = ReadSettings -buildMode '' -project '' -workflowName '' -userName '' -branchName '' -trigger '' | ConvertTo-HashTable -recurse $templateSha = $repoSettings.templateSha # If templateUrl has changed, download latest version of the template repository (ignore templateSha) @@ -113,6 +113,12 @@ if (-not $isDirectALGo) { # Get the list of projects in the current repository $baseFolder = $ENV:GITHUB_WORKSPACE + +if ($originalTemplateFolder) { + # Use current custom template settings for this run without changing the workspace before comparison. + $repoSettings = ReadSettingsWithCurrentCustomTemplateRepoSettings -baseFolder $baseFolder -templateFolder $templateFolder +} + $projects = @(GetProjectsFromRepository -baseFolder $baseFolder -projectsFromSettings $repoSettings.projects) $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $repoSettings -projects $projects -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 67f72ce759..3f4db236ad 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,3 +1,13 @@ +### Enhanced `customALGoFiles` setting + +The `customALGoFiles` setting of a custom template was only applied on the next Update (from `AL-Go-TemplateRepoSettings.doNotEdit.json`). Now the up-to-date settings of the custom template are used directly during "Update AL-Go System Files". The template's `filesToInclude` and `filesToExclude` settings are merged with the consumer repo's settings before resolution. + +- **`filesToInclude`** now also resolves files from the original AL-Go template. Files present in the official template are propagated even when they are absent from your custom template. When a file exists in both, the official template supplies the base content; for workflow files, customizations from the custom template are reapplied. +- **`filesToExclude`** now also resolves files from the original AL-Go template (same dual-resolution as `filesToInclude`). Files resolved by `filesToInclude` whose source matches a `filesToExclude` entry are not copied to consumer repos, and existing copies are removed. +- **`destinationName`** (new property on `filesToInclude`): Allows renaming a file at the destination. When set, the file is written to `/` instead of keeping the source filename. + +Read more at [Customizing AL-Go for GitHub](Scenarios/CustomizingALGoForGitHub.md#Using-custom-template-files). + ### New `doNotPerformUpgrade` setting AL-Go now supports a new `doNotPerformUpgrade` setting that is passed through to `Run-AlPipeline`. Use it to skip the upgrade phase while still running the rest of the pipeline. diff --git a/Scenarios/CustomizingALGoForGitHub.md b/Scenarios/CustomizingALGoForGitHub.md index f5d80b24ad..b8c7c581e3 100644 --- a/Scenarios/CustomizingALGoForGitHub.md +++ b/Scenarios/CustomizingALGoForGitHub.md @@ -239,10 +239,25 @@ In order to instruct AL-Go which files to look for at the template repository, y - `filter`: A string to use for filtering in the specified source path. 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`. +- `destinationName`: The filename to use at the destination. If specified, overrides the source filename, allowing the file to be renamed when copied. Should be used together with a `filter` that matches a single file. _Example_: `customScript.ps1`. > [!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. +When using a custom template repository, `filesToInclude` also resolves files from the **original** AL-Go template (i.e. the official [AL-Go-PTE](https://github.com/microsoft/AL-Go-PTE) or [AL-Go-AppSource](https://github.com/microsoft/AL-Go-AppSource) template). This means files present in the official AL-Go template that are not overridden by your custom template are still propagated to consumer repositories. When a file exists in both the original template and your custom template, how the file's **content** is resolved depends on the file's type: + +- **Workflow files** (`.github/workflows/*.yaml`/`*.yml`): the content is based on the original template's file, with customizations from your custom template's copy (see [Adding custom jobs](#adding-custom-jobs)) re-applied on top. +- **Settings files** and **all other files** (e.g. PowerShell scripts, `.copy.md`, `.agent.md`): the original template's file content is used as-is; changes made to that same file in your custom template are not applied in this case. + +The following table summarizes how `filesToInclude` resolves files when a custom template is in use: + +| File is present in original template | File is present in custom template | File is matched by `filesToInclude` | Result | +|---|---|---|---| +| Yes | No | Yes | File from **original template** is propagated | +| No | Yes | Yes | File from **custom template** is propagated | +| Yes | Yes | Yes | File from **original template** is propagated; for Workflow files, customizations from the **custom template** are also applied | +| Yes/No | Yes/No | No | File is **ignored** | + `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`. @@ -251,13 +266,16 @@ In order to instruct AL-Go which files to look for at the template repository, y > [!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. +> [!TIP] +> When using a custom template repository, you can use `filesToExclude` in the custom template's settings to prevent files from the original AL-Go template from being propagated to consumer repos. For example, if the original template includes a workflow you don't want in your consumer repos, adding it to `filesToExclude` in your custom template's settings will remove it during the next update. + 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 | +| Yes | No | Yes | The file 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 diff --git a/Tests/CheckForUpdates.Action.Test.ps1 b/Tests/CheckForUpdates.Action.Test.ps1 index 9f73255520..def629004a 100644 --- a/Tests/CheckForUpdates.Action.Test.ps1 +++ b/Tests/CheckForUpdates.Action.Test.ps1 @@ -289,6 +289,7 @@ Describe "CheckForUpdates Action: ApplyWorkflowDefaultInputs Tests" { BeforeAll { $actionName = "CheckForUpdates" $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve) . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") } @@ -1319,6 +1320,47 @@ Describe "ResolveFilePaths" { $fullFilePaths[1].type | Should -Be '' } + It 'ResolveFilePaths warns and skips destinations outside the destination folder' { + $destinationFolder = Join-Path $rootFolder "destinationFolder" + $destinationSubfolder = Join-Path $destinationFolder "subfolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationName" = "../outside.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File2.log"; "destinationFolder" = "../outside" } + @{ "sourceFolder" = "folder"; "filter" = "File3.txt"; "destinationFolder" = "subfolder"; "destinationName" = "../outside.txt" } + @{ "sourceFolder" = "folder"; "filter" = "File4.md" } + ) + Mock OutputWarning {} + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "folder/File4.md") + Should -Invoke OutputWarning -Times 3 -ParameterFilter { $message -like "*outside the destination folder '$destinationFolder*" } + Should -Invoke OutputWarning -Times 1 -ParameterFilter { $message -like "*outside the destination folder '$destinationSubfolder*" } + } + + It 'ResolveFilePaths warns and skips per-project destinations outside the destination folder' { + $destinationFolder = Join-Path $rootFolder "destinationFolder" + $destinationProjectFolder = Join-Path $destinationFolder "project" + $destinationProjectSubfolder = Join-Path $destinationProjectFolder "subfolder" + $files = @( + @{ "sourceFolder" = "folder"; "filter" = "File1.txt"; "destinationName" = "../outside.txt"; "perProject" = $true } + @{ "sourceFolder" = "folder"; "filter" = "File2.log"; "destinationFolder" = "../outside"; "perProject" = $true } + @{ "sourceFolder" = "folder"; "filter" = "File3.txt"; "destinationFolder" = "subfolder"; "destinationName" = "../outside.txt"; "perProject" = $true } + @{ "sourceFolder" = "folder"; "filter" = "File4.md"; "perProject" = $true } + ) + Mock OutputWarning {} + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @("project")) + + $fullFilePaths.Count | Should -Be 1 + $fullFilePaths[0].sourceFullPath | Should -Be (Join-Path $sourceFolder "folder/File4.md") + $fullFilePaths[0].destinationFullPath | Should -Be (Join-Path $destinationFolder "project/folder/File4.md") + Should -Invoke OutputWarning -Times 3 -ParameterFilter { $message -like "*outside the destination folder '$destinationProjectFolder*" } + Should -Invoke OutputWarning -Times 1 -ParameterFilter { $message -like "*outside the destination folder '$destinationProjectSubfolder*" } + } + It 'ResolveFilePaths with type' { $destinationFolder = "destinationFolder" $destinationFolder = Join-Path $PSScriptRoot $destinationFolder @@ -1489,6 +1531,77 @@ Describe "ResolveFilePaths" { if (Test-Path $externalFolder) { Remove-Item -Path $externalFolder -Recurse -Force } } + It 'ResolveFilePaths skips files in folder whose name starts with source folder name' { + # Create an external file in a folder whose name starts with the same prefix as sourceFolder + $externalFolder = "${sourceFolder}-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" = "../sourceFolder-external"; "filter" = "*.txt" } + ) + + $fullFilePaths = ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder + + # The file in the prefix-colliding folder must NOT be included + $fullFilePaths | ForEach-Object { $_.sourceFullPath | Should -Not -BeLike "${externalFolder}*" } + + # Cleanup + if (Test-Path $externalFile) { Remove-Item -Path $externalFile -Force } + if (Test-Path $externalFolder) { Remove-Item -Path $externalFolder -Recurse -Force } + } + + It 'ResolveFilePaths skips files in a source folder that differs only by case' -Skip:($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows) { + $externalFolder = Join-Path $rootFolder 'sourcefolder' + $externalFile = Join-Path $externalFolder 'outside.txt' + New-Item -Path $externalFolder -ItemType Directory -Force | Out-Null + Set-Content -Path $externalFile -Value 'outside' + + try { + $destinationFolder = Join-Path $rootFolder 'destinationFolder' + $files = @( + @{ 'sourceFolder' = '../sourcefolder'; 'filter' = '*.txt' } + ) + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -BeNullOrEmpty + } + finally { + if (Test-Path $externalFolder) { Remove-Item -Path $externalFolder -Recurse -Force } + } + } + + It 'ResolveFilePaths skips destinations in a folder that differs only by case' -Skip:($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows) { + $destinationFolder = Join-Path $rootFolder 'destinationFolder' + $files = @( + @{ 'sourceFolder' = 'folder'; 'filter' = 'File1.txt'; 'destinationFolder' = 'CaseFolder'; 'destinationName' = '../casefolder/outside.txt' } + ) + Mock OutputWarning {} + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder) + + $fullFilePaths | Should -BeNullOrEmpty + Should -Invoke OutputWarning -Times 1 + } + + It 'ResolveFilePaths skips per-project destinations in a folder that differs only by case' -Skip:($PSVersionTable.PSVersion.Major -lt 6 -or $IsWindows) { + $destinationFolder = Join-Path $rootFolder 'destinationFolder' + $files = @( + @{ 'sourceFolder' = 'folder'; 'filter' = 'File1.txt'; 'destinationFolder' = ''; 'destinationName' = '../caseproject/outside.txt'; 'perProject' = $true } + ) + Mock OutputWarning {} + + $fullFilePaths = @(ResolveFilePaths -sourceFolder $sourceFolder -files $files -destinationFolder $destinationFolder -projects @('CaseProject')) + + $fullFilePaths | Should -BeNullOrEmpty + Should -Invoke OutputWarning -Times 1 + } + It 'ResolveFilePaths returns empty when no files match filter' { $destinationFolder = "destinationFolder" $destinationFolder = Join-Path $rootFolder $destinationFolder @@ -1892,15 +2005,83 @@ Describe "GetFilesToUpdate (general files to update logic)" { # . # ├── test.ps1 # ├── test.txt - # └── test2.txt + # ├── test2.txt + # └── subfolder + # ├── testsub.txt + # └── testsub2.txt + + $originalTemplateFolder = Join-Path $PSScriptRoot "originalTemplate" + Copy-Item -Path $templateFolder -Destination $originalTemplateFolder -Recurse -Force | Out-Null + + $testOriginalTemplateTxtFile = Join-Path $originalTemplateFolder "test.original.txt" + Set-Content -Path $testOriginalTemplateTxtFile -Value "test original template txt file" + + $testOriginalTemplatePSFile = Join-Path $originalTemplateFolder "test.original.ps1" + Set-Content -Path $testOriginalTemplatePSFile -Value "# test original template ps file" + + # Display the created files structure for original template folder + # . + # ├── test.ps1 + # ├── test.txt + # ├── test2.txt + # ├── test.original.ps1 + # ├── test.original.txt # └── subfolder - # └── testsub.txt + # ├── testsub.txt + # └── testsub2.txt + + $baseFolder = Join-Path $PSScriptRoot "base" + Copy-Item -Path $templateFolder -Destination $baseFolder -Recurse -Force | Out-Null + + $testBaseTxtFile = Join-Path $baseFolder "test.base.txt" + Set-Content -Path $testBaseTxtFile -Value "test base txt file" + + $testBasePSFile = Join-Path $baseFolder "test.base.ps1" + Set-Content -Path $testBasePSFile -Value "# test base ps file" + + $baseProject1Folder = Join-Path $baseFolder "project1" + Copy-Item -Path $templateFolder -Destination $baseProject1Folder -Recurse -Force | Out-Null + + $baseProject2Folder = Join-Path $baseFolder "project2" + Copy-Item -Path $templateFolder -Destination $baseProject2Folder -Recurse -Force | Out-Null + + Remove-Item -Path (Join-Path $baseFolder 'test2.txt') -Recurse -Force | Out-Null + + # Display the created files structure for base folder + # . + # ├── test.ps1 + # ├── test.txt + # ├── test.base.ps1 + # ├── test.base.txt + # ├── subfolder + # │ ├── testsub.txt + # │ └── testsub2.txt + # ├── project1 + # │ ├── test.ps1 + # │ ├── test.txt + # │ ├── test2.txt + # │ └── subfolder + # │ ├── testsub.txt + # │ └── testsub2.txt + # └── project2 + # ├── test.ps1 + # ├── test.txt + # ├── test2.txt + # └── subfolder + # ├── testsub.txt + # └── testsub2.txt } AfterAll { if (Test-Path $templateFolder) { Remove-Item -Path $templateFolder -Recurse -Force } + if (Test-Path $originalTemplateFolder) { + Remove-Item -Path $originalTemplateFolder -Recurse -Force + } + if (Test-Path $baseFolder) { + Remove-Item -Path $baseFolder -Recurse -Force + } } It "Returns the correct files to update with filters" { @@ -1913,14 +2094,14 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.ps1') - # No files to remove + # No files to exclude or remove $filesToExclude | Should -BeNullOrEmpty $settings = @{ @@ -1932,15 +2113,15 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 2 $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.txt') $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path $baseFolder 'test2.txt') - # No files to remove + # No files to exclude or remove $filesToExclude | Should -BeNullOrEmpty } @@ -1954,16 +2135,16 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 2 $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + $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') + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path $baseFolder 'customFolder/test2.txt') - # No files to remove + # No files to exclude or remove $filesToExclude | Should -BeNullOrEmpty $settings = @{ @@ -1975,18 +2156,18 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'customFolder/test.txt') + $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') + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'customFolder/test2.txt') } It 'Returns the correct files with destinationName' { @@ -1999,14 +2180,14 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'renamed.txt') + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'renamed.txt') - # No files to remove + # No files to exclude or remove $filesToExclude | Should -BeNullOrEmpty $settings = @{ @@ -2018,12 +2199,12 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'dstPath/renamed.txt') + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'dstPath/renamed.txt') } It 'Return the correct files with types' { @@ -2036,15 +2217,15 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 $filesToInclude[0].sourceFullPath | Should -Be $testPSFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.ps1') + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.ps1') $filesToInclude[0].type | Should -Be "script" - # No files to remove + # No files to exclude or remove $filesToExclude | Should -BeNullOrEmpty $settings = @{ @@ -2056,19 +2237,19 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 1 $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $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') + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.txt') } It 'Return the correct files when unusedALGoSystemFiles is specified' { @@ -2081,20 +2262,20 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 2 $filesToInclude[0].sourceFullPath | Should -Be $testTxtFile - $filesToInclude[0].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test.txt') + $filesToInclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.txt') $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude[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') + $filesToExclude[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.ps1') } It 'GetFilesToUpdate with perProject true and empty projects returns no per-project entries' { @@ -2108,7 +2289,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { } # Pass empty projects array - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects @() + $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 @@ -2125,15 +2306,15 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $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[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.txt') $filesToInclude[1].sourceFullPath | Should -Be $testTxtFile2 - $filesToInclude[1].destinationFullPath | Should -Be (Join-Path 'baseFolder' 'test2.txt') + $filesToInclude[1].destinationFullPath | Should -Be (Join-Path $baseFolder 'test2.txt') $filesToExclude | Should -BeNullOrEmpty } @@ -2153,13 +2334,13 @@ Describe "GetFilesToUpdate (general files to update logic)" { } $projects = @('.', 'ProjectOne') - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder -projects $projects + $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' + $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 @@ -2200,7 +2381,6 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $baseFolder = 'baseFolder' $projects = @('ProjectA') $filesWithoutOriginal, $excludesWithoutOriginal = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $customTemplateFolder -projects $projects @@ -2227,6 +2407,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { $filesWithOriginal.destinationFullPath | Should -Contain (Join-Path $baseFolder (Join-Path '.github' $CustomTemplateProjectSettingsFileName)) $excludesWithoutOriginal | Should -BeNullOrEmpty + $excludesWithOriginal | Should -BeNullOrEmpty } finally { @@ -2249,7 +2430,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $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") } @@ -2270,7 +2451,7 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder # All txt files should be included $filesToInclude | Should -Not -BeNullOrEmpty @@ -2282,6 +2463,28 @@ Describe "GetFilesToUpdate (general files to update logic)" { $excludedNonExistent | Should -BeNullOrEmpty } + It 'GetFilesToUpdate excludes files with different destinations that match both include and exclude patterns' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.txt" }, @{ filter = "test.txt"; destinationName = "test.renamed.txt" }) + filesToExclude = @(@{ filter = "test.txt" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder + + # test.txt should not be in filesToInclude + $filesToInclude | Should -BeNullOrEmpty + + # test.txt should be in filesToExclude two times with different destinations + $testTxtFiles = $filesToExclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") } + $testTxtFiles.Count | Should -Be 2 + $testTxtFiles[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.renamed.txt') + $testTxtFiles[1].destinationFullPath | Should -Be (Join-Path $baseFolder 'test.txt') + } + It 'GetFilesToUpdate handles overlapping include patterns with different destinations' { $settings = @{ type = "NotPTE" @@ -2295,13 +2498,208 @@ Describe "GetFilesToUpdate (general files to update logic)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $templateFolder + $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') + $testTxtFiles[0].destinationFullPath | Should -Be (Join-Path $baseFolder 'folder1/test.txt') + $testTxtFiles[1].destinationFullPath | Should -Be (Join-Path $baseFolder 'folder2/test.txt') + } + + It 'GetFilesToUpdate filesToInclude keeps the first entry when two entries collide on the same destination' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.ps1"; destinationName = "conflict.txt" }, @{ filter = "test.txt"; destinationName = "conflict.txt" }) + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder + + # Only one entry should be resolved for the colliding destination + $conflict = @($filesToInclude | Where-Object { $_.destinationFullPath -eq (Join-Path $baseFolder "conflict.txt") }) + $conflict.Count | Should -Be 1 + + # The first-listed entry should win over the later entry for the same destination + $conflict[0].sourceFullPath | Should -Be $testPSFile + } + + It 'GetFilesToUpdate filesToInclude includes original template files missing in template' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.original.txt" }) + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder + + # test.original.txt of original template should be in filesToInclude + $testOriginalTemplateTxtFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -eq $testOriginalTemplateTxtFile }) + $testOriginalTemplateTxtFiles | Should -Not -BeNullOrEmpty + $testOriginalTemplateTxtFiles.Count | Should -Be 1 + $testOriginalTemplateTxtFiles[0].sourceFullPath | Should -Be $testOriginalTemplateTxtFile + $testOriginalTemplateTxtFiles[0].originalSourceFullPath | Should -Be $null + $testOriginalTemplateTxtFiles[0].destinationFullPath | Should -Be ( Join-Path $baseFolder "test.original.txt" ) + } + + It 'GetFilesToUpdate filesToExclude excludes original template files missing in template' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.original.txt" }) + filesToExclude = @(@{ filter = "test.original.txt" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder + + # test.original.txt of original template should be in filesToExclude + $testOriginalTemplateTxtFiles = @($filesToExclude | Where-Object { $_.sourceFullPath -eq $testOriginalTemplateTxtFile }) + $testOriginalTemplateTxtFiles | Should -Not -BeNullOrEmpty + $testOriginalTemplateTxtFiles.Count | Should -Be 1 + $testOriginalTemplateTxtFiles[0].sourceFullPath | Should -Be $testOriginalTemplateTxtFile + $testOriginalTemplateTxtFiles[0].originalSourceFullPath | Should -Be $null + $testOriginalTemplateTxtFiles[0].destinationFullPath | Should -Be ( Join-Path $baseFolder "test.original.txt" ) + } + + It 'GetFilesToUpdate filesToInclude not including original template files existing in template' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.txt" }) + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder + + # test.txt of template should be in filesToInclude + $testTxtFiles = @($filesToInclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") }) + $testTxtFiles | Should -Not -BeNullOrEmpty + $testTxtFiles.Count | Should -Be 1 + $testTxtFiles[0].sourceFullPath | Should -Be (Join-Path $templateFolder "test.txt") + $testTxtFiles[0].originalSourceFullPath | Should -Be ( Join-Path $originalTemplateFolder "test.txt" ) + $testTxtFiles[0].destinationFullPath | Should -Be (Join-Path $baseFolder "test.txt") + + # test.txt of original template should not be in filesToInclude + $filesToInclude.SourceFullPath | Should -Not -Contain ( Join-Path $originalTemplateFolder "test.txt" ) + } + + It 'GetFilesToUpdate filesToExclude not excluding original template files existing in template' { + $settings = @{ + type = "NotPTE" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @(@{ filter = "test.txt" }) + filesToExclude = @(@{ filter = "test.txt" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $templateFolder -originalTemplateFolder $originalTemplateFolder + + # test.txt of template should be in filesToExclude + $testTxtFiles = @($filesToExclude | Where-Object { $_.sourceFullPath -eq (Join-Path $templateFolder "test.txt") }) + $testTxtFiles | Should -Not -BeNullOrEmpty + $testTxtFiles.Count | Should -Be 1 + $testTxtFiles[0].sourceFullPath | Should -Be (Join-Path $templateFolder "test.txt") + $testTxtFiles[0].originalSourceFullPath | Should -Be ( Join-Path $originalTemplateFolder "test.txt" ) + $testTxtFiles[0].destinationFullPath | Should -Be (Join-Path $baseFolder "test.txt") + + # test.txt of original template should not be in filesToExclude + $filesToExclude.SourceFullPath | Should -Not -Contain ( Join-Path $originalTemplateFolder "test.txt" ) + } +} + +Describe "ReadSettingsWithCurrentCustomTemplateRepoSettings" { + BeforeAll { + $actionName = "CheckForUpdates" + $scriptRoot = Join-Path $PSScriptRoot "..\Actions\$actionName" -Resolve + . (Join-Path -Path $scriptRoot -ChildPath "..\AL-Go-Helper.ps1" -Resolve) + . (Join-Path -Path $scriptRoot -ChildPath "CheckForUpdates.HelperFunctions.ps1") + } + + It 'Uses current template settings and restores an existing snapshot' { + $templateFolder = Join-Path $TestDrive "templateWithCurrentSettings" + $baseFolder = Join-Path $TestDrive "baseWithExistingSnapshot" + New-Item -ItemType Directory -Path (Join-Path $templateFolder ".github") -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $baseFolder ".github") -Force | Out-Null + + $templateSettingsFile = Join-Path $templateFolder $RepoSettingsFile + $templateSettingsContent = '{"customALGoFiles":{"filesToInclude":[{"filter":"current.txt"}]}}' + Set-Content -LiteralPath $templateSettingsFile -Value $templateSettingsContent -Encoding UTF8 + + $snapshotFile = Join-Path $baseFolder $CustomTemplateRepoSettingsFile + $snapshotContent = '{"customALGoFiles":{"filesToInclude":[{"filter":"stale.txt"}]}}' + Set-Content -LiteralPath $snapshotFile -Value $snapshotContent -Encoding UTF8 + $snapshotHash = (Get-FileHash -LiteralPath $snapshotFile).Hash + + $settings = ReadSettingsWithCurrentCustomTemplateRepoSettings -baseFolder $baseFolder -templateFolder $templateFolder + + $settings.customALGoFiles.filesToInclude.Count | Should -Be 1 + $settings.customALGoFiles.filesToInclude[0].filter | Should -Be "current.txt" + (Get-FileHash -LiteralPath $snapshotFile).Hash | Should -Be $snapshotHash + Get-ContentLF -Path $snapshotFile | Should -Be $snapshotContent + } + + It 'Removes a temporary snapshot when none existed before reading settings' { + $templateFolder = Join-Path $TestDrive "templateWithoutSnapshot" + $baseFolder = Join-Path $TestDrive "baseWithoutSnapshot" + New-Item -ItemType Directory -Path (Join-Path $templateFolder ".github") -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $baseFolder ".github") -Force | Out-Null + + $templateSettingsFile = Join-Path $templateFolder $RepoSettingsFile + Set-Content -LiteralPath $templateSettingsFile -Value '{"customALGoFiles":{"filesToInclude":[{"filter":"current.txt"}]}}' -Encoding UTF8 + + $snapshotFile = Join-Path $baseFolder $CustomTemplateRepoSettingsFile + Test-Path -LiteralPath $snapshotFile | Should -Be $false + + $settings = ReadSettingsWithCurrentCustomTemplateRepoSettings -baseFolder $baseFolder -templateFolder $templateFolder + + $settings.customALGoFiles.filesToInclude[0].filter | Should -Be "current.txt" + Test-Path -LiteralPath $snapshotFile | Should -Be $false + } + + It 'Does not change an existing snapshot when the template has no settings file' { + $templateFolder = Join-Path $TestDrive "templateWithoutSettings" + $baseFolder = Join-Path $TestDrive "baseWithUnchangedSnapshot" + New-Item -ItemType Directory -Path $templateFolder -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $baseFolder ".github") -Force | Out-Null + + $snapshotFile = Join-Path $baseFolder $CustomTemplateRepoSettingsFile + $snapshotContent = '{"customALGoFiles":{"filesToInclude":[{"filter":"existing.txt"}]}}' + Set-Content -LiteralPath $snapshotFile -Value $snapshotContent -Encoding UTF8 + $snapshotHash = (Get-FileHash -LiteralPath $snapshotFile).Hash + + $settings = ReadSettingsWithCurrentCustomTemplateRepoSettings -baseFolder $baseFolder -templateFolder $templateFolder + + $settings.customALGoFiles.filesToInclude[0].filter | Should -Be "existing.txt" + (Get-FileHash -LiteralPath $snapshotFile).Hash | Should -Be $snapshotHash + } + + It 'Restores an existing snapshot when reading refreshed settings fails' { + $templateFolder = Join-Path $TestDrive "templateWithInvalidSettings" + $baseFolder = Join-Path $TestDrive "baseWithSnapshotAfterFailure" + New-Item -ItemType Directory -Path (Join-Path $templateFolder ".github") -Force | Out-Null + New-Item -ItemType Directory -Path (Join-Path $baseFolder ".github") -Force | Out-Null + + $templateSettingsFile = Join-Path $templateFolder $RepoSettingsFile + Set-Content -LiteralPath $templateSettingsFile -Value '{ invalid json' -Encoding UTF8 + + $snapshotFile = Join-Path $baseFolder $CustomTemplateRepoSettingsFile + $snapshotContent = '{"customALGoFiles":{"filesToInclude":[{"filter":"stale.txt"}]}}' + Set-Content -LiteralPath $snapshotFile -Value $snapshotContent -Encoding UTF8 + + { ReadSettingsWithCurrentCustomTemplateRepoSettings -baseFolder $baseFolder -templateFolder $templateFolder } | Should -Throw + + Get-ContentLF -Path $snapshotFile | Should -Be $snapshotContent } } @@ -2316,6 +2714,16 @@ Describe "GetFilesToUpdate (real template)" { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'realAppSourceAppTemplateFolder', Justification = 'False positive.')] $realAppSourceAppTemplateFolder = Join-Path $PSScriptRoot "../Templates/AppSource App" -Resolve + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'baseFolder', Justification = 'False positive.')] + $baseFolder = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot 'baseFolder')) + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'powerPlatformFiles', Justification = 'False positive.')] + $powerPlatformFiles = @( + ".github/workflows/_BuildPowerPlatformSolution.yaml", + ".github/workflows/PullPowerPlatformChanges.yaml", + ".github/workflows/PushPowerPlatformChanges.yaml" + ) } It 'Return the correct files to exclude when type is PTE and powerPlatformSolutionFolder is not empty' { @@ -2329,18 +2737,80 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $realPTETemplateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 25 - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildPowerPlatformSolution.yaml") - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PullPowerPlatformChanges.yaml") - $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/PushPowerPlatformChanges.yaml") + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder $powerPlatformFiles[0]) + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder $powerPlatformFiles[1]) + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder $powerPlatformFiles[2]) - # No files to remove + # No files to exclude or remove $filesToExclude | Should -BeNullOrEmpty } + It 'GetFilesToUpdate defaults filesToInclude takes precedence over repository settings for the same destination' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + # Redirect a different template file onto the destination of the default AL-Go-Settings.json entry + filesToInclude = @(@{ filter = "Test Next Major.settings.json"; sourceFolder = ".github"; destinationFolder = ".github"; destinationName = "$RepoSettingsFileName" }) + filesToExclude = @() + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $realPTETemplateFolder + + $repoSettingsDestination = Join-Path $baseFolder (Join-Path '.github' $RepoSettingsFileName) + $conflict = @($filesToInclude | Where-Object { $_.destinationFullPath -eq $repoSettingsDestination }) + $conflict.Count | Should -Be 1 + + # The default entry should win over the repository settings entry for the same destination + $conflict[0].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder (Join-Path '.github' $RepoSettingsFileName)) + } + + It 'GetFilesToUpdate defaults filesToExclude combined with repository settings filesToExclude for non-colliding files' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @(@{ filter = "_BuildALGoProject.yaml"; sourceFolder = ".github/workflows" }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $realPTETemplateFolder + + # The default exclude entries (PowerPlatform files) and the repository settings' own exclude entry are both applied + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder $powerPlatformFiles[0]) + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildALGoProject.yaml") + $filesToInclude.sourceFullPath | Should -Not -Contain (Join-Path $realPTETemplateFolder ".github/workflows/_BuildALGoProject.yaml") + } + + It 'GetFilesToUpdate defaults filesToExclude and repository settings filesToExclude for the same source file are both applied without duplicates' { + # The repository settings entry excludes the exact same file that the default PowerPlatform exclude entries + # already exclude (since powerPlatformSolutionFolder is empty). This should not error out or produce a + # duplicate entry: the file should end up excluded exactly once. + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = '' + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @(@{ filter = [System.IO.Path]::GetFileName($powerPlatformFiles[0]); sourceFolder = [System.IO.Path]::GetDirectoryName($powerPlatformFiles[0]).Replace('\', '/') }) + } + } + + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $realPTETemplateFolder + + $ppFileSourcePath = Join-Path $realPTETemplateFolder $powerPlatformFiles[0] + @($filesToExclude | Where-Object { $_.sourceFullPath -eq $ppFileSourcePath }).Count | Should -Be 1 + $filesToInclude.sourceFullPath | Should -Not -Contain $ppFileSourcePath + } + It 'Return PP files in filesToExclude when type is PTE but powerPlatformSolutionFolder is empty' { $settings = @{ type = "PTE" @@ -2352,30 +2822,26 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $realPTETemplateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 22 $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") + $fileToInclude = $_ + $powerPlatformFiles | ForEach-Object { + $fileToInclude.sourceFullPath | Should -Not -Be (Join-Path $realPTETemplateFolder $_) + } } # 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") + $filesToExclude.Count | Should -Be $powerPlatformFiles.Count + for ($i = 0; $i -lt $powerPlatformFiles.Count; $i++) { + $filesToExclude[$i].sourceFullPath | Should -Be (Join-Path $realPTETemplateFolder $powerPlatformFiles[$i]) + $filesToExclude[$i].destinationFullPath | Should -Be (Join-Path $baseFolder $powerPlatformFiles[$i]) + } } It 'Return the correct files when unusedALGoSystemFiles is specified' { @@ -2389,7 +2855,7 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $realPTETemplateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 24 @@ -2398,7 +2864,7 @@ Describe "GetFilesToUpdate (real template)" { $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') + $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' { @@ -2412,7 +2878,7 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -templateFolder $realPTETemplateFolder $filesToInclude | Should -Not -BeNullOrEmpty $filesToInclude.Count | Should -Be 21 @@ -2422,9 +2888,9 @@ Describe "GetFilesToUpdate (real template)" { $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") + $powerPlatformFiles | ForEach-Object { + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $realPTETemplateFolder $_) + } } It 'Returns the custom template settings files when there is a custom template' { @@ -2440,7 +2906,7 @@ Describe "GetFilesToUpdate (real template)" { $customTemplateFolder = $realPTETemplateFolder $originalTemplateFolder = $realAppSourceAppTemplateFolder - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template $filesToInclude | Should -Not -BeNullOrEmpty @@ -2451,11 +2917,11 @@ Describe "GetFilesToUpdate (real template)" { $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].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].destinationFullPath | Should -Be (Join-Path $baseFolder '.github/AL-Go-TemplateRepoSettings.doNotEdit.json') $repoSettingsFiles[1].type | Should -Be '' # Check project settings files @@ -2465,17 +2931,74 @@ Describe "GetFilesToUpdate (real template)" { $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].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].destinationFullPath | Should -Be (Join-Path $baseFolder '.github/AL-Go-TemplateProjectSettings.doNotEdit.json') $projectSettingsFilesFromCustomTemplate[1].type | Should -Be '' - # No files to exclude + # No files to exclude or remove $filesToExclude | Should -BeNullOrEmpty } + It 'Returns the original template PP files in filesToInclude when there is a custom template without them and powerPlatformSolutionFolder is not empty' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = "PowerPlatformSolution" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } + + # AppSource App is used as custom template because it has no PP workflows, simulating a custom PTE fork that stripped them out + $customTemplateFolder = $realAppSourceAppTemplateFolder + $originalTemplateFolder = $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template + + $filesToInclude | Should -Not -BeNullOrEmpty + $powerPlatformFiles | ForEach-Object { + $filesToInclude.sourceFullPath | Should -Not -Contain (Join-Path $customTemplateFolder $_) + $filesToInclude.sourceFullPath | Should -Contain (Join-Path $originalTemplateFolder $_) + } + + # No files to exclude or remove + $filesToExclude | Should -BeNullOrEmpty + } + + It 'Returns the original template PP files in filesToExclude when there is a custom template without them and powerPlatformSolutionFolder is empty' { + $settings = @{ + type = "PTE" + powerPlatformSolutionFolder = "" + unusedALGoSystemFiles = @() + customALGoFiles = @{ + filesToInclude = @() + filesToExclude = @() + } + } + + # AppSource App is used as custom template because it has no PP workflows, simulating a custom PTE fork that stripped them out + $customTemplateFolder = $realAppSourceAppTemplateFolder + $originalTemplateFolder = $realPTETemplateFolder + $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder $baseFolder -projects @('.') -templateFolder $customTemplateFolder -originalTemplateFolder $originalTemplateFolder # Indicate custom template + + $filesToInclude | Should -Not -BeNullOrEmpty + $powerPlatformFiles | ForEach-Object { + $filesToInclude.sourceFullPath | Should -Not -Contain (Join-Path $customTemplateFolder $_) + $filesToInclude.sourceFullPath | Should -Not -Contain (Join-Path $originalTemplateFolder $_) + } + + $filesToExclude | Should -Not -BeNullOrEmpty + $powerPlatformFiles | ForEach-Object { + $filesToExclude.sourceFullPath | Should -Not -Contain (Join-Path $customTemplateFolder $_) + $filesToExclude.sourceFullPath | Should -Contain (Join-Path $originalTemplateFolder $_) + } + + # No files to remove + } + It 'GetFilesToUpdate handles AppSource template type correctly' { $settings = @{ type = "AppSource App" @@ -2487,7 +3010,7 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realAppSourceAppTemplateFolder + $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 @@ -2511,7 +3034,7 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $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" } @@ -2529,7 +3052,7 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects @('Project1') + $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" }) @@ -2553,7 +3076,7 @@ Describe "GetFilesToUpdate (real template)" { } $projects = @('ProjectA', 'ProjectB', 'ProjectC') - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder -projects $projects + $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*" } @@ -2576,7 +3099,7 @@ Describe "GetFilesToUpdate (real template)" { } } - $filesToInclude, $filesToExclude = GetFilesToUpdate -settings $settings -baseFolder 'baseFolder' -templateFolder $realPTETemplateFolder + $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" } diff --git a/e2eTests/scenarios/CustomTemplate/runtest.ps1 b/e2eTests/scenarios/CustomTemplate/runtest.ps1 index 5cdef971b3..93132e89e3 100644 --- a/e2eTests/scenarios/CustomTemplate/runtest.ps1 +++ b/e2eTests/scenarios/CustomTemplate/runtest.ps1 @@ -31,8 +31,12 @@ Write-Host -ForegroundColor Yellow @' # - Create a new repository based on the PTE template with 1 app, using compilerfolder and donotpublishapps (this will be the "final" template repository) # - Run Update AL-Go System Files in final repo (using custom template repository as template) # - Run Update AL-Go System files in custom template repository +# - Validate that custom AL-Go files are applied in custom template repository # - Validate that custom job is present in custom template repository # - Run Update AL-Go System files in final repo +# - Validate that custom AL-Go files of template repository are applied in final repository +# - Run Update AL-Go System files in final repo +# - Validate that custom AL-Go files of template repository and final repository are applied in final repository # - Validate that custom job is present in final repo # '@ @@ -55,6 +59,8 @@ $template = "https://github.com/$pteTemplate" # Login SetTokenAndRepository -github:$github -githubOwner $githubOwner -appId $e2eAppId -appKey $e2eAppKey -repository $repository +#region create repositories + # Create template repository CreateAlGoRepository ` -github:$github ` @@ -64,6 +70,9 @@ CreateAlGoRepository ` -branch $branch $templateRepoPath = (Get-Location).Path +# Stop all currently running workflows on template repository +CancelAllWorkflows -repository $templateRepository + Set-Location $prevLocation $appName = 'MyApp' @@ -76,15 +85,26 @@ CreateAlGoRepository ` -template $template ` -repository $repository ` -branch $branch ` + -addRepoSettings @{ "useCompilerFolder" = $true; "doNotPublishApps" = $true } ` -contentScript { Param([string] $path) $null = CreateNewAppInFolder -folder $path -name $appName -publisher $publisherName } $finalRepoPath = (Get-Location).Path +# Stop all currently running workflows on final repository +CancelAllWorkflows -repository $repository + # Update AL-Go System Files to use template repository RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $templateRepository -ghTokenWorkflow $algoauthapp -repository $repository -branch $branch | Out-Null +# Stop all currently running workflows on final repository +CancelAllWorkflows -repository $repository + +#endregion + +#region setup template repository customizations + Set-Location $templateRepoPath Pull @@ -154,6 +174,10 @@ on: branches: - main +defaults: + run: + shell: powershell + jobs: CustomJob: runs-on: [ windows-latest ] @@ -166,23 +190,97 @@ jobs: "@ Set-Content -Path $customWorkflowFile -Value $customWorkflowContent +$finalRepoCustomWorkflowContent = $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' + $finalRepoCustomWorkflowContent = $finalRepoCustomWorkflowContent -replace 'windows-latest', 'ubuntu-latest' + $finalRepoCustomWorkflowContent = $finalRepoCustomWorkflowContent -replace 'shell: powershell', 'shell: pwsh' } -# 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 +# Add custom files in the template repository +$defaultCustomFileName = 'CustomTemplateFile.Default.txt' +$defaultCustomFile = Join-Path $templateRepoPath $defaultCustomFileName +$defaultCustomFileContent = "This is a default custom file in the template repository." +Set-Content -Path $defaultCustomFile -Value $defaultCustomFileContent + +$optionalCustomFileName = 'CustomTemplateFile.Optional.txt' +$optionalCustomFile = Join-Path $templateRepoPath $optionalCustomFileName +$optionalCustomFileContent = "This is an optional custom file in the template repository." +Set-Content -Path $optionalCustomFile -Value $optionalCustomFileContent + +# Remove workflow files from template repository +$excludedWorkflowFileName = 'DeployReferenceDocumentation.yaml' +$excludedWorkflowFileRelativePath = Join-Path '.github/workflows' $excludedWorkflowFileName +$excludedWorkflowFile = Join-Path $templateRepoPath $excludedWorkflowFileRelativePath +Remove-Item -Path $excludedWorkflowFile -Force | Out-Null + +$missingWorkflowFileName = 'Troubleshooting.yaml' +$missingWorkflowFileRelativePath = Join-Path '.github/workflows' $missingWorkflowFileName +$missingWorkflowFile = Join-Path $templateRepoPath $missingWorkflowFileRelativePath +Remove-Item -Path $missingWorkflowFile -Force | Out-Null + +# Add customALGoFiles settings to the template repository +$templateRepoSettingsFile = Join-Path $templateRepoPath $RepoSettingsFile +$null = Add-PropertiesToJsonFile -path $templateRepoSettingsFile -properties @{ + "customALGoFiles" = @{ + "filesToInclude" = @( @{ "filter" = $defaultCustomFileName } ) + "filesToExclude" = @( @{ "sourceFolder" = ".github/workflows"; "filter" = $excludedWorkflowFileName } ) + } +} # Push -CommitAndPush -commitMessage 'Add template customizations' +CommitAndPush -commitMessage 'Add template customizations [skip ci]' + +#endregion + +#region update template repository with template repository customizations + +# Update AL-Go System Files for template repository to update customizations from template repository +RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $template -ghTokenWorkflow $algoauthapp -repository $templateRepository -branch $branch | Out-Null -# Do not run workflows on template repository +# Stop all currently running workflows on template repository CancelAllWorkflows -repository $templateRepository +# Pull changes +Pull + +# 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 default custom file is present +(Join-Path (Get-Location) $defaultCustomFileName) | Should -Exist +Get-ContentLF -Path (Join-Path (Get-Location) $defaultCustomFileName) | Should -Be $defaultCustomFileContent.Replace("`r", "").TrimEnd("`n") +# Check that optional custom file is present +(Join-Path (Get-Location) $optionalCustomFileName) | Should -Exist +Get-ContentLF -Path (Join-Path (Get-Location) $optionalCustomFileName) | Should -Be $optionalCustomFileContent.Replace("`r", "").TrimEnd("`n") + +# Check that excluded workflow file is NOT present (in template's filesToExclude) +(Join-Path (Get-Location) $excludedWorkflowFileRelativePath) | Should -Not -Exist +# Check that missing workflow file is present (in default filesToInclude) +(Join-Path (Get-Location) $missingWorkflowFileRelativePath) | Should -Exist + +# Remove missing workflow files from template repository again +Remove-Item -Path $missingWorkflowFile -Force | Out-Null + +# Push +CommitAndPush -commitMessage 'Restore template customizations [skip ci]' + +#endregion + +#region validate template repository CI/CD workflow + +# Run CICD +$run = RunCICD -repository $templateRepository -branch $branch -wait + +# Check Custom Jobs +Test-LogContainsFromRun -repository $templateRepository -runid $run.id -jobName 'CustomJob-TemplateInit' -stepName 'Init' -expectedText 'CustomJob-TemplateInit was here!' +Test-LogContainsFromRun -repository $templateRepository -runid $run.id -jobName 'CustomJob-TemplateDeploy' -stepName 'Deploy' -expectedText 'CustomJob-TemplateDeploy was here!' +{ Test-LogContainsFromRun -repository $templateRepository -runid $run.id -jobName 'JustSomeTemplateJob' -stepName 'JustSomeTemplateStep' -expectedText 'JustSomeTemplateJob was here!' } | Should -Throw + +#endregion + +#region setup final repository customizations + # Add local customizations to the final repository Set-Location $finalRepoPath Pull @@ -245,14 +343,42 @@ $cicdYaml.AddCustomJobsToYaml($customJobs, [CustomizationOrigin]::FinalRepositor # save $cicdYaml.Save($cicdWorkflow) +# Remove workflow files from final repository +Remove-Item -Path (Join-Path (Get-Location) $missingWorkflowFileRelativePath) -Force | Out-Null + +# Check that custom workflow file is NOT present +(Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Not -Exist + +# Check that default custom file is NOT present in final repository +(Join-Path (Get-Location) $defaultCustomFileName) | Should -Not -Exist +# Check that optional custom file is NOT present in final repository +(Join-Path (Get-Location) $optionalCustomFileName) | Should -Not -Exist + +# Check that excluded workflow file is present in final repository +(Join-Path (Get-Location) $excludedWorkflowFileRelativePath) | Should -Exist +# Check that missing workflow file is NOT present in final repository +(Join-Path (Get-Location) $missingWorkflowFileRelativePath) | Should -Not -Exist + +# Create a stale snapshot of the template repository settings file in the final repository, +# to simulate a scenario where the final repository has an outdated snapshot of the template repository's settings +Copy-Item -Path $templateRepoSettingsFile -Destination $CustomTemplateRepoSettingsFile -Force +$null = Add-PropertiesToJsonFile -path $CustomTemplateRepoSettingsFile -properties @{ + "customALGoFiles" = @{ + "filesToExclude" = @( @{ "sourceFolder" = ".github/workflows"; "filter" = $missingWorkflowFileName } ) + } +} # Push -CommitAndPush -commitMessage 'Add final repo customizations' +CommitAndPush -commitMessage 'Add final repo customizations [skip ci]' + +#endregion -# Update AL-Go System Files to uptake UseProjectDependencies setting +#region update final repository with template repository customizations + +# Update AL-Go System Files for the final repository to uptake customizations from template repository RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $templateRepository -ghTokenWorkflow $algoauthapp -repository $repository -branch $branch | Out-Null -# Stop all currently running workflows and run a new CI/CD workflow +# Stop all currently running workflows on final repository CancelAllWorkflows -repository $repository # Pull changes @@ -260,42 +386,87 @@ Pull (Join-Path (Get-Location) $CustomTemplateRepoSettingsFile) | Should -Exist (Join-Path (Get-Location) $CustomTemplateProjectSettingsFile) | Should -Exist +Get-ContentLF -Path (Join-Path (Get-Location) $CustomTemplateRepoSettingsFile) | Should -Be (Get-ContentLF -Path $templateRepoSettingsFile) # 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") +Get-ContentLF -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Be $finalRepoCustomWorkflowContent.Replace("`r", "").TrimEnd("`n") + +# Check that default custom file is present (in template's filesToInclude) +(Join-Path (Get-Location) $defaultCustomFileName) | Should -Exist +Get-ContentLF -Path (Join-Path (Get-Location) $defaultCustomFileName) | Should -Be $defaultCustomFileContent.Replace("`r", "").TrimEnd("`n") +# Check that optional custom file is NOT present (not in default or template's filesToInclude) +(Join-Path (Get-Location) $optionalCustomFileName) | Should -Not -Exist -# Check that custom file is NOT present -(Join-Path (Get-Location) $customFileName) | Should -Not -Exist # Custom file should not be copied by default +# Check that excluded workflow file is NOT present (in default filesToInclude and template's filesToExclude) +(Join-Path (Get-Location) $excludedWorkflowFileRelativePath) | Should -Not -Exist +# Check that missing workflow file is present (in default filesToInclude, propagated from PTE template). +# This proves the stale snapshot exclusion seeded during final repository setup was replaced. +(Join-Path (Get-Location) $missingWorkflowFileRelativePath) | Should -Exist -# Add custom file to be copied via settings -$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ "customALGoFiles" = @{ "filesToInclude" = @( @{ "filter" = $customFileName } ) } } +#endregion + +#region setup final repository customizations for next update run + +# Add customALGoFiles settings to the final repository +$null = Add-PropertiesToJsonFile -path '.github/AL-Go-Settings.json' -properties @{ + "customALGoFiles" = @{ + "filesToInclude" = @( @{ "filter" = $optionalCustomFileName } ) + "filesToExclude" = @( @{ "filter" = $defaultCustomFileName } ) + } +} # Push -CommitAndPush -commitMessage 'Add custom file to be updated when updating AL-Go system files [skip ci]' +CommitAndPush -commitMessage 'Add custom files to be updated when updating AL-Go system files [skip ci]' -# Update AL-Go System Files to uptake custom file +#endregion + +#region update final repository with template and final repository customizations + +# Update AL-Go System Files for final repository to uptake customizations from final repository RunUpdateAlGoSystemFiles -directCommit -wait -templateUrl $templateRepository -ghTokenWorkflow $algoauthapp -repository $repository -branch $branch | Out-Null +# Stop all currently running workflows on final repository +CancelAllWorkflows -repository $repository + # 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") +# Check that custom workflow file is present +(Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Exist +Get-ContentLF -Path (Join-Path (Get-Location) $customWorkflowfileRelativePath) | Should -Be $finalRepoCustomWorkflowContent.Replace("`r", "").TrimEnd("`n") + + # Check that default custom file is NOT present (in repo's filesToExclude and template's filesToInclude) +(Join-Path (Get-Location) $defaultCustomFileName) | Should -Not -Exist +# Check that optional custom file is present (in repo's filesToInclude) +(Join-Path (Get-Location) $optionalCustomFileName) | Should -Exist +Get-ContentLF -Path (Join-Path (Get-Location) $optionalCustomFileName) | Should -Be $optionalCustomFileContent.Replace("`r", "").TrimEnd("`n") + +# Check that excluded workflow file is NOT present (in default filesToInclude and template's filesToExclude) +(Join-Path (Get-Location) $excludedWorkflowFileRelativePath) | Should -Not -Exist +# Check that missing workflow file is present (in default filesToInclude, propagated from PTE template) +(Join-Path (Get-Location) $missingWorkflowFileRelativePath) | Should -Exist + +#endregion + +#region validate final repository CI/CD workflow # Run CICD $run = RunCICD -repository $repository -branch $branch -wait # Check Custom Jobs -Test-LogContainsFromRun -runid $run.id -jobName 'CustomJob-TemplateInit' -stepName 'Init' -expectedText 'CustomJob-TemplateInit was here!' -Test-LogContainsFromRun -runid $run.id -jobName 'CustomJob-TemplateDeploy' -stepName 'Deploy' -expectedText 'CustomJob-TemplateDeploy was here!' -Test-LogContainsFromRun -runid $run.id -jobName 'CustomJob-PreDeploy' -stepName 'PreDeploy' -expectedText 'CustomJob-PreDeploy was here!' -Test-LogContainsFromRun -runid $run.id -jobName 'CustomJob-PostDeploy' -stepName 'PostDeploy' -expectedText 'CustomJob-PostDeploy was here!' -{ Test-LogContainsFromRun -runid $run.id -jobName 'JustSomeJob' -stepName 'JustSomeStep' -expectedText 'JustSomeJob was here!' } | Should -Throw -{ Test-LogContainsFromRun -runid $run.id -jobName 'JustSomeTemplateJob' -stepName 'JustSomeTemplateStep' -expectedText 'JustSomeTemplateJob was here!' } | Should -Throw +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'CustomJob-TemplateInit' -stepName 'Init' -expectedText 'CustomJob-TemplateInit was here!' +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'CustomJob-TemplateDeploy' -stepName 'Deploy' -expectedText 'CustomJob-TemplateDeploy was here!' +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'CustomJob-PreDeploy' -stepName 'PreDeploy' -expectedText 'CustomJob-PreDeploy was here!' +Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'CustomJob-PostDeploy' -stepName 'PostDeploy' -expectedText 'CustomJob-PostDeploy was here!' +{ Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'JustSomeJob' -stepName 'JustSomeStep' -expectedText 'JustSomeJob was here!' } | Should -Throw +{ Test-LogContainsFromRun -repository $repository -runid $run.id -jobName 'JustSomeTemplateJob' -stepName 'JustSomeTemplateStep' -expectedText 'JustSomeTemplateJob was here!' } | Should -Throw + +#endregion Set-Location $prevLocation +RefreshToken -repository $repository RemoveRepository -repository $repository -path $finalRepoPath +RefreshToken -repository $templateRepository RemoveRepository -repository $templateRepository -path $templateRepoPath