Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0d9e05e
Enhance customALGoFiles feature (#3)
OleWunschmann Jun 3, 2026
73f4ef3
fixed typo
OleWunschmann Jun 3, 2026
bb38962
fixed function description
OleWunschmann Jun 3, 2026
50ad11c
Fixed tests for ps5 and string-prefix check for folder traversal
OleWunschmann Jun 9, 2026
7aa8766
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jun 9, 2026
f7496cc
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jun 12, 2026
11e2efc
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jun 16, 2026
b021013
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jun 17, 2026
025747e
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jun 22, 2026
dd1771a
Merge remote-tracking branch 'origin/main' into enhance-customALGoFil…
OleWunschmann Jun 30, 2026
2d24972
Merge remote-tracking branch 'origin/main' into enhance-customALGoFil…
OleWunschmann Jul 9, 2026
5976abe
Merge remote-tracking branch 'origin/main' into enhance-customALGoFil…
OleWunschmann Jul 14, 2026
7fa3522
Extended settings schema and documentation for "destinationName"
OleWunschmann Jul 14, 2026
a5c5fb1
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jul 20, 2026
181080e
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jul 21, 2026
c23a8ae
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jul 21, 2026
afce6d7
Potential fix for pull request finding
OleWunschmann Jul 23, 2026
00d1902
Fixed copilot review comments
OleWunschmann Jul 23, 2026
ee510bd
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jul 23, 2026
c8ded0d
Fix copilot review comments
OleWunschmann Jul 24, 2026
409371e
Cleanup
OleWunschmann Jul 24, 2026
84a656e
Cleanup
OleWunschmann Jul 24, 2026
8682d62
Fix copilot review comments
OleWunschmann Jul 24, 2026
051d08a
Remove support for fileToRemove
OleWunschmann Jul 29, 2026
1a037b8
Merge remote-tracking branch 'origin/main' into enhance-customALGoFil…
OleWunschmann Jul 29, 2026
d412a75
Fixed copilot review comment
OleWunschmann Jul 29, 2026
6376e16
Fixed issues
OleWunschmann Jul 29, 2026
73e159c
Cleanup
OleWunschmann Jul 29, 2026
cf71143
Fixed boundary escape for destinationFolder and destinationName
OleWunschmann Jul 29, 2026
f94fe4e
Merge branch 'main' into enhance-customALGoFiles-feature
OleWunschmann Jul 31, 2026
61c640d
Fixed boundary escape for destinationName and typos
OleWunschmann Jul 31, 2026
ea2688e
Fixed boundry escape for wildcards and for linux
OleWunschmann Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Actions/.Modules/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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."
Comment thread
mazhelez marked this conversation as resolved.
},
"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."
Expand All @@ -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",
Expand Down
163 changes: 129 additions & 34 deletions Actions/CheckForUpdates/CheckForUpdates.HelperFunctions.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +830 to +834

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


$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') {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -994,28 +1022,88 @@ 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
The base folder of the repository. This is the target folder where the files will be updated.
.PARAMETER templateFolder
The folder where the template files are located.
.PARAMETER originalTemplateFolder
The folder where the original template files are located (if any).
If originalTemplateFolder is provided, it means that there is a custom template in use and custom template files should be included.
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.
Expand All @@ -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
Expand All @@ -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
})

Expand Down
8 changes: 7 additions & 1 deletion Actions/CheckForUpdates/CheckForUpdates.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading