Enhance customALGoFiles feature - #2273
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR enhances the customALGoFiles behavior during “Update AL-Go System Files”, adding support for unconditional removals (filesToRemove) and improving custom-template behavior by merging template settings directly and resolving files from the original AL-Go template where applicable.
Changes:
- Add
customALGoFiles.filesToRemovesupport end-to-end (schema, defaults, resolution logic, docs, and release notes). - Update
CheckForUpdatesto read template repo settings viaReadSettingsand merge template settings during file resolution. - Expand automated coverage (unit + e2e) for include/exclude/remove resolution and custom-template propagation.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| e2eTests/scenarios/CustomTemplate/runtest.ps1 | Extends e2e scenario to validate custom-template file include/exclude/remove propagation and workflow presence. |
| Tests/CheckForUpdates.Action.Test.ps1 | Adds unit tests for destination-folder resolution and expanded GetFilesToUpdate behaviors (including filesToRemove). |
| Actions/CheckForUpdates/CheckForUpdates.ps1 | Updates settings reading (incl. trigger) and wires template settings + filesToRemove into update/removal flow. |
| Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1 | Implements ResolveFilePathsInDestinationFolder and extends GetFilesToUpdate to produce include/exclude/remove lists. |
| Actions/.Modules/settings.schema.json | Extends settings schema with customALGoFiles.filesToRemove and clarifying descriptions. |
| Actions/.Modules/ReadSettings.psm1 | Adds default filesToRemove array under customALGoFiles. |
| Scenarios/settings.md | Documents customALGoFiles.filesToRemove in settings reference. |
| Scenarios/CustomizingALGoForGitHub.md | Adds conceptual docs + examples for original-template resolution and filesToRemove. |
| RELEASENOTES.md | Documents enhanced customALGoFiles behavior and new filesToRemove. |
|
@mazhelez The failing PS5 tests should be fixed now. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:1205
- Removal destinations are not constrained to
$baseFolder. AfilesToRemoveentry with a matching template file anddestinationFolder = '..'produces adestinationFullPathoutside the repository;CheckForUpdates.ps1later converts it to a relative path and passes it toRemove-Item. Normalize every removal destination and reject paths that are not descendants of the repository root before returning this list.
$filesToRemove += @(ResolveFilePaths -sourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToRemoveUnresolved -projects $projects)
}
$filesToRemove += @(ResolveFilePaths -sourceFolder $templateFolder -originalSourceFolder $originalTemplateFolder -destinationFolder $baseFolder -files $filesToRemoveUnresolved -projects $projects)
$filesToRemove += @(ResolveFilePathsInDestinationFolder -destinationFolder $baseFolder -files $filesToRemoveUnresolved -projects $projects)
| if ($null -ne $templateSettings) { | ||
| $filesToIncludeUnresolved += $templateSettings.customALGoFiles.filesToInclude | ||
| } | ||
| $filesToIncludeUnresolved += $settings.customALGoFiles.filesToInclude |
There was a problem hiding this comment.
What Copilot says he was what I was worried about - not sure whether it is a problem though
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
Scenarios/CustomizingALGoForGitHub.md:250
- This description is inaccurate for settings files.
CheckForUpdates.ps1:164-167passes them throughGetModifiedSettingsContent, which preserves existing destination settings and primarily refreshes template metadata such as$schema; it does not use the original template file content as-is. Separate settings files from the “all other files” behavior so consumers do not expect their repository settings to be replaced.
- **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.
Actions/.Modules/settings.schema.json:746
- The PR description promises a new
customALGoFiles.filesToRemovearray for unconditional removal, but the updated schema still defines onlyfilesToIncludeandfilesToExclude, and no runtime implementation or documentation forfilesToRemoveremains in this revision. Either restore that advertised feature across schema/code/docs/tests or update the PR description to reflect the actual scope.
"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.",
e2eTests/scenarios/CustomTemplate/runtest.ps1:261
- This comment names the wrong list:
Troubleshooting.yamlis restored because it is matched by the defaultfilesToIncludeworkflow pattern and resolved from the original template, not because it is infilesToExclude.
# Check that missing workflow file is present (in default filesToExclude)
e2eTests/scenarios/CustomTemplate/runtest.ps1:445
- Use the possessive “repo's,” not “repos's.”
# Check that optional custom file is present (in repos's filesToInclude)
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
e2eTests/scenarios/CustomTemplate/runtest.ps1:359
- This second
$legacyCustomFileNameassertion also references an undefined variable under strict mode. Since this scenario no longer creates a legacy file, remove the stale assertion (or restore a complete setup if that behavior still needs coverage).
(Join-Path (Get-Location) $legacyCustomFileName) | Should -Exist
e2eTests/scenarios/CustomTemplate/runtest.ps1:257
$legacyCustomFileNameis never defined anywhere in this scenario (or the repository), and strict mode is enabled at line 44. The test will terminate here with an undefined-variable error before reaching the new update validations. Remove this stale assertion or restore the missing legacy-file setup.
This issue also appears on line 359 of the same file.
(Join-Path (Get-Location) $legacyCustomFileName) | Should -Not -Exist
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (5)
RELEASENOTES.md:3
- The PR description still advertises a new
filesToRemovesetting and unconditional removal behavior, but the current schema, release notes, and runtime expose onlyfilesToIncludeandfilesToExclude. Please update the PR description so it does not promise functionality that was removed from this revision.
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.
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:830
- Appending a separator does not make the later
-like "$sourceFolder*"check literal: PowerShell still interprets wildcard metacharacters in the resolved path. A workspace path containing[/]can reject legitimate files, and*can broaden this boundary check. Use canonical full paths with a literalStartsWithcomparison (and the platform-appropriate case comparison) instead of a wildcard pattern.
$sourceFolder = Join-Path $sourceFolder '' # Ensure source folder has a trailing slash for correct path resolution
e2eTests/scenarios/CustomTemplate/runtest.ps1:441
- Correct the possessive typo from “repos's” to “repo's.”
# Check that optional custom file is present (in repos's filesToInclude)
Actions/.Modules/settings.schema.json:765
destinationNameis described as a filename but accepts path separators and...ResolveFilePathspasses it directly toJoin-Path, so a value such as../outside.txtproduces a destination outsidedestinationFolderand can escape the repository when the update is written. Reject non-leaf names/path traversal at runtime and constrain the schema accordingly.
"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."
Scenarios/CustomizingALGoForGitHub.md:242
- The new
destinationNamesetting is documented here and in the schema, but the canonical settings reference atScenarios/settings.md:251was not updated with its type, constraints, and behavior. Add it there so the setting documentation remains complete and aligned.
- `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`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (4)
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:830
- Appending a separator does not make the later wildcard operations path-safe:
Get-ChildItem -Pathand-like "$sourceFolder*"still interpret characters such as[in a valid repository path as wildcard syntax, causing contained files to be skipped or misresolved. Use-LiteralPathfor enumeration and a canonicalGetFullPath/StartsWithboundary comparison instead.
$sourceFolder = Join-Path $sourceFolder '' # Ensure source folder has a trailing slash for correct path resolution
RELEASENOTES.md:7
destinationNamewas already supported byResolveFilePathsand its existing tests; this PR documents it and adds it to the schema rather than introducing a new runtime property. Reword the release note to avoid announcing an existing capability as new.
- **`destinationName`** (new property on `filesToInclude`): Allows renaming a file at the destination. When set, the file is written to `<destinationFolder>/<destinationName>` instead of keeping the source filename.
e2eTests/scenarios/CustomTemplate/runtest.ps1:441
- Use the possessive “repo's,” not “repos's.”
# Check that optional custom file is present (in repos's filesToInclude)
Actions/.Modules/settings.schema.json:775
- The PR description still advertises
filesToRemoveandResolveFilePathsInDestinationFolderas major additions, but the current schema and implementation only supportfilesToIncludeandfilesToExclude. Update the PR description to reflect the removed functionality, or restore the promised implementation before merging.
"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.",
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:1135
Group-Objectgroups strings case-insensitively by default. On the Ubuntu/PowerShell 7 test target, destinations such asFoo.ps1andfoo.ps1are distinct filesystem paths, but this deduplication silently drops one—particularly when one comes from the custom template and the other from the original template. Group case-sensitively on non-Windows platforms while retaining case-insensitive grouping on Windows.
$filesToInclude = @($filesToInclude | Group-Object { $_.destinationFullPath } | Sort-Object -Property Name | ForEach-Object { $_.Group[0] })
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:834
- These roots are later interpolated directly into
-likepatterns for all three containment checks.-liketreats characters such as[as wildcard syntax and is case-insensitive even on Linux, so a valid self-hosted runner path containing wildcard characters can be rejected, while a traversal into a case-variant sibling (for example/tmp/Repoto/tmp/repo) can pass on a case-sensitive filesystem. The trailing separator only fixes ordinary prefix collisions; use a literal, OS-aware canonical-path containment helper for the source and both destination checks.
$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
e2eTests/scenarios/CustomTemplate/runtest.ps1:441
- Use the possessive “repo's” here.
# Check that optional custom file is present (in repos's filesToInclude)
|
@mazhelez |
There was a problem hiding this comment.
🟡 Not ready to approve
destinationName can still escape its configured destination folder, and a minor spelling correction remains.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (2)
e2eTests/scenarios/CustomTemplate/runtest.ps1:441
- Correct the possessive typo
repos'storepo's.
# Check that optional custom file is present (in repos's filesToInclude)
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:927
- The new containment check only protects the repository root, so a value such as
destinationName = "../renamed.txt"still escapes the configureddestinationFolderwhile remaining inside the repository and passes this check. That contradicts the documented filename-only contract and can overwrite an unrelated destination; validatedestinationNameas a leaf name before either the per-project or regular path is constructed.
$fullProjectFilePath.destinationFullPath = Join-Path $fullProjectFilePath.destinationFullPath $destinationName
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
There was a problem hiding this comment.
🟡 Not ready to approve
The path-containment checks remain bypassable on case-sensitive platforms, and the central settings reference is incomplete.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (1)
Scenarios/CustomizingALGoForGitHub.md:242
- The new
destinationNamesetting is missing from the centralScenarios/settings.mdsettings reference; that file'scustomALGoFilesrow still only names the two arrays and does not describe this property's type/default or that it is honored by Update AL-Go System Files. Add the new property to that reference so the setting documentation remains complete and aligned with the schema.
- `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`.
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| $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 |
There was a problem hiding this comment.
🟡 Not ready to approve
Destination normalization rejects valid relative paths, and case-insensitive deduplication drops valid Linux files.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Suppressed comments (3)
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:956
- The destination file is canonicalized but
$fileDestinationFolderis not. Consequently, valid relative folder paths such as.orscripts/../sharedare treated as traversal and skipped because the normalized file path cannot start with the unnormalized folder string. Canonicalize the folder first and check that it remains under$destinationFolder; then retain the existing filename-boundary check.
$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
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:928
$fileDestinationFolderis compared before it is canonicalized, whiledestinationFullPathis canonicalized. A valid relative path containing.or an internal..(for example,destinationFolder: './scripts') therefore failsStartsWithand is skipped. Canonicalize the project and file destination folders first, then separately verify the project stays under the base folder and the file folder stays under the project folder before checkingdestinationName.
This issue also appears on line 952 of the same file.
$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
Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1:1141
Group-Objectgroups strings case-insensitively by default. On Linux, the new dual-template resolution can produce distinct destinations such asFoo.ps1from the custom template andfoo.ps1from the original template, but this line drops one even though both paths are valid and distinct. Deduplicate with an OS-appropriateStringComparer, matching the path comparison used byResolveFilePaths.
$filesToInclude = @($filesToInclude | Group-Object { $_.destinationFullPath } | Sort-Object -Property Name | ForEach-Object { $_.Group[0] })
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
❔What, Why & How
This pull request significantly enhances how custom template file settings are handled during the "Update AL-Go System Files" process. The main improvements are that the latest custom template settings are now used immediately (rather than only after a subsequent update), both
filesToIncludeandfilesToExcludeare resolved from both the custom and original templates, and a newdestinationNameproperty enables file renaming on copy. Additionally, several path resolution and safety improvements have been made to prevent files from being copied outside intended directories.Improvements to custom template file handling:
customALGoFilessettings from the custom template are now merged and used directly during updates, ensuring that the latest template configuration is always applied without waiting for a new update cycle. [1] [2]filesToIncludeandfilesToExcludeare now resolved against both the custom template and the original AL-Go template, ensuring that files added or removed upstream are properly propagated to consumer repositories. [1] [2]destinationNameproperty is added tofilesToInclude, allowing files to be renamed when copied to the destination. This is reflected in both the schema and implementation. [1] [2]Path resolution and safety:
Documentation and schema updates:
filesToIncludeandfilesToExcludehave been updated to clarify their new behavior and the newdestinationNameproperty. [1] [2] [3]These changes make custom template management more robust, predictable, and safer for consumer repositories.
Related to discussion: #2227
✅ Checklist