Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
23ff383
Initial implementation
aholstrup1 Jan 28, 2026
abd86aa
Add PS5 compatibility
aholstrup1 Jan 28, 2026
a0b3ecd
Remove duplicate check
aholstrup1 Jan 28, 2026
2a2e1cf
Remove logic from Runpipeline
aholstrup1 Jan 28, 2026
323d90a
releasenotes
aholstrup1 Jan 28, 2026
e992e7f
Check secret exists
aholstrup1 Jan 28, 2026
37a1b20
handle empty sanitizedFileName
aholstrup1 Jan 28, 2026
840a612
Handle duplicate files
aholstrup1 Jan 28, 2026
3bbd0d1
Error message
aholstrup1 Jan 28, 2026
b0d5fa4
releasenotes
aholstrup1 Jan 28, 2026
15554dc
Use Invoke-CommandWithRetry
aholstrup1 Jan 28, 2026
c74fa44
Fix for how install apps is set
aholstrup1 Feb 3, 2026
1c2a9d3
Handling for .zip files
aholstrup1 Feb 3, 2026
7c2cc34
Add tests
aholstrup1 Feb 4, 2026
afab810
Merge branch 'main' of https://github.com/microsoft/al-go into aholst…
aholstrup1 Feb 4, 2026
0b446c0
Cleanup
aholstrup1 Feb 4, 2026
50b19d5
Import Github-Helper.psm1
aholstrup1 Feb 4, 2026
cc992ad
Add handling of local paths
aholstrup1 Feb 4, 2026
cc03d43
Make sure all app files are always copied to dependencies folder
aholstrup1 Feb 4, 2026
0ba2ae6
Update test
aholstrup1 Feb 4, 2026
25d5c24
Fix compatibility issue
aholstrup1 Feb 4, 2026
8995cc8
Fix test
aholstrup1 Feb 4, 2026
488057f
debugging
aholstrup1 Feb 4, 2026
7d393ce
Check if folder exists
aholstrup1 Feb 4, 2026
8f660fe
Better logging
aholstrup1 Feb 4, 2026
8c6e34e
Fix trailing whitespace and missing blank line
aholstrup1 Feb 4, 2026
4b9cf82
remove out folder
aholstrup1 Feb 4, 2026
697edbb
Change directory
aholstrup1 Feb 4, 2026
ff159c2
Suggestions batch 1
aholstrup1 Feb 4, 2026
324518c
suggestions batch 2
aholstrup1 Feb 4, 2026
0703fd4
Handling for missing secret
aholstrup1 Feb 9, 2026
f98b798
Merge branch 'main' of https://github.com/microsoft/al-go into aholst…
aholstrup1 Feb 9, 2026
291eb23
Update Actions/DownloadProjectDependencies/DownloadProjectDependencie…
aholstrup1 Feb 17, 2026
246de64
File name collisions
aholstrup1 Feb 17, 2026
68e6994
Add Parameter
aholstrup1 Feb 17, 2026
37c2a60
Merge branch 'main' of https://github.com/microsoft/al-go into aholst…
aholstrup1 Feb 17, 2026
ee0564c
Merge branch 'main' of https://github.com/microsoft/al-go into aholst…
aholstrup1 Feb 20, 2026
5bcb926
Suggestions
aholstrup1 Feb 20, 2026
b0e4718
Test-PathWithinWorkspace
aholstrup1 Feb 20, 2026
9419529
Harden Test-IsZipFile
aholstrup1 Feb 20, 2026
d97410d
test coverage
aholstrup1 Feb 20, 2026
5af1fde
Add a MaxDepth to Expand-ZipFileToAppFiles
aholstrup1 Feb 20, 2026
a86f485
-Raw
aholstrup1 Feb 20, 2026
5313313
Fix end-of-file newline in Github-Helper.psm1
aholstrup1 Feb 20, 2026
c71ad77
Fix wildcard path validation in Test-PathWithinWorkspace
aholstrup1 Feb 20, 2026
c4538c0
Cleanup
aholstrup1 Feb 20, 2026
efbbdfd
pre-commit
aholstrup1 Feb 20, 2026
bf76270
Merge branch 'main' of https://github.com/microsoft/al-go into aholst…
aholstrup1 Feb 20, 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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
[string] $token
)

Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "DownloadProjectDependencies.psm1" -Resolve) -DisableNameChecking

function DownloadDependenciesFromProbingPaths {
param(
$baseFolder,
Expand Down Expand Up @@ -123,6 +125,10 @@ Write-Host "::group::Downloading project dependencies from probing paths"
$downloadedDependencies += DownloadDependenciesFromProbingPaths -baseFolder $baseFolder -project $project -destinationPath $destinationPath -token $token
Write-Host "::endgroup::"

Write-Host "::group::Downloading dependencies from settings (installApps and installTestApps)"
$settingsDependencies = Get-DependenciesFromInstallApps -DestinationPath $destinationPath
Write-Host "::endgroup::"

$downloadedApps = @()
$downloadedTestApps = @()

Expand All @@ -137,6 +143,10 @@ $downloadedDependencies | ForEach-Object {
}
}

# Add dependencies from settings
$downloadedApps += $settingsDependencies.Apps
$downloadedTestApps += $settingsDependencies.TestApps

OutputMessageAndArray -message "Downloaded dependencies (Apps)" -arrayOfStrings $downloadedApps
OutputMessageAndArray -message "Downloaded dependencies (Test Apps)" -arrayOfStrings $downloadedTestApps

Expand Down
154 changes: 154 additions & 0 deletions Actions/DownloadProjectDependencies/DownloadProjectDependencies.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
<#
.SYNOPSIS
Downloads a file from a URL to a specified download path.
.DESCRIPTION
Downloads a file from a URL to a specified download path.
It handles URL decoding and sanitizes the file name.
If the downloaded file is a zip file, it extracts the .app files from it.
.PARAMETER Url
The URL of the file to download.
.PARAMETER DownloadPath
The path where the file should be downloaded.
.OUTPUTS
An array of paths to the downloaded/extracted .app files.
#>
function Get-AppFilesFromUrl {
Param(
[string] $Url,
[string] $DownloadPath
)
# Get the file name from the URL
$urlWithoutQuery = $Url.Split('?')[0].TrimEnd('/')
$rawFileName = [System.IO.Path]::GetFileName($urlWithoutQuery)
$decodedFileName = [Uri]::UnescapeDataString($rawFileName)
$decodedFileName = [System.IO.Path]::GetFileName($decodedFileName)

# Sanitize file name by removing invalid characters
$sanitizedFileName = $decodedFileName.Split([System.IO.Path]::getInvalidFileNameChars()) -join ""
$sanitizedFileName = $sanitizedFileName.Trim()

if ([string]::IsNullOrWhiteSpace($sanitizedFileName)) {
# Assume the file is an .app file if no valid name could be determined
$sanitizedFileName = "$([Guid]::NewGuid().ToString()).app"
}

# Get the final file path
$downloadedFile = Join-Path $DownloadPath $sanitizedFileName
if (Test-Path -LiteralPath $downloadedFile) {
OutputDebug -message "Overwriting existing file '$sanitizedFileName'. Multiple dependencies may resolve to the same filename."
}

# Download with retry logic
Invoke-CommandWithRetry -ScriptBlock {
Invoke-WebRequest -Method GET -UseBasicParsing -Uri $Url -OutFile $downloadedFile | Out-Null
} -RetryCount 3 -FirstDelay 5 -MaxWaitBetweenRetries 10

# Check if the downloaded file is a zip file
$extension = [System.IO.Path]::GetExtension($downloadedFile).ToLowerInvariant()
if ($extension -eq '.zip') {
Write-Host "Extracting .app files from zip archive: $sanitizedFileName"

Check notice

Code scanning / PSScriptAnalyzer

Line has trailing whitespace Note

Line has trailing whitespace
# Extract to runner temp folder
$extractPath = Join-Path $env:RUNNER_TEMP ([System.IO.Path]::GetFileNameWithoutExtension($sanitizedFileName))
Expand-Archive -Path $downloadedFile -DestinationPath $extractPath -Force
Remove-Item -Path $downloadedFile -Force

# Find all .app files in the extracted folder and copy them to the download path
$appFiles = @()
foreach ($appFile in (Get-ChildItem -Path $extractPath -Filter '*.app' -Recurse)) {
$destFile = Join-Path $DownloadPath $appFile.Name
Copy-Item -Path $appFile.FullName -Destination $destFile -Force
$appFiles += $destFile
}

# Clean up the extracted folder
Remove-Item -Path $extractPath -Recurse -Force

if ($appFiles.Count -eq 0) {
throw "Zip archive '$sanitizedFileName' does not contain any .app files"
}
Write-Host "Found $($appFiles.Count) .app file(s) in zip archive"
return $appFiles
}

return @($downloadedFile)
}

<#
.SYNOPSIS
Downloads dependencies from URLs specified in installApps and installTestApps settings.
.DESCRIPTION
Reads the installApps and installTestApps arrays from the repository settings.
For each entry that is a URL (starts with http:// or https://):
- Resolves any secret placeholders in the format ${{ secretName }} by looking up the secret value
- Downloads the app file to the specified destination path
For entries that are not URLs (local paths), they are returned as-is.
.PARAMETER DestinationPath
The path where the app files should be downloaded.
.OUTPUTS
A hashtable with Apps and TestApps arrays containing the resolved local file paths.
#>
function Get-DependenciesFromInstallApps {
Param(
[string] $DestinationPath
)

$settings = $env:Settings | ConvertFrom-Json | ConvertTo-HashTable

# ENV:Secrets is not set when running Pull_Request trigger
if ($env:Secrets) {
$secrets = $env:Secrets | ConvertFrom-Json | ConvertTo-HashTable
}
else {
$secrets = @{}
}

$install = @{
"Apps" = @($settings.installApps)
"TestApps" = @($settings.installTestApps)
}

# Check if the installApps and installTestApps settings are empty
if (($settings.installApps.Count -eq 0) -and ($settings.installTestApps.Count -eq 0)) {
Write-Host "No installApps or installTestApps settings found."
return $install
}

# Replace secret names in install.apps and install.testApps and download files from URLs
foreach($list in @('Apps','TestApps')) {
$install."$list" = @($install."$list" | ForEach-Object {
$appFile = $_

# If the app file is not a URL, return it as is
if ($appFile -notlike 'http*://*') {
Write-Host "install$($list) contains a local path: $appFile"
return $appFile
}

# Else, check for secrets in the URL and replace them
$appFileUrl = $appFile
$pattern = '.*(\$\{\{\s*([^}]+?)\s*\}\}).*'
if ($appFile -match $pattern) {
$secretName = $matches[2]
if (-not $secrets.ContainsKey($secretName)) {
throw "Setting: install$($list) references unknown secret '$secretName' in URL: $appFile"
}
$appFileUrl = $appFileUrl.Replace($matches[1],[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($secrets."$secretName")))
}

# Download the file (may return multiple .app files if it's a zip)
try {
Write-Host "Downloading from URL: $appFile"
$appFiles = Get-AppFilesFromUrl -Url $appFileUrl -DownloadPath $DestinationPath
} catch {
throw "Setting: install$($list) contains an inaccessible URL: $appFile. Error was: $($_.Exception.Message)"
}

return $appFiles
})
}

return $install
}

Export-ModuleMember -Function Get-AppFilesFromUrl, Get-DependenciesFromInstallApps
32 changes: 4 additions & 28 deletions Actions/RunPipeline/RunPipeline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -193,13 +193,13 @@ try {
}

$install = @{
"Apps" = $settings.installApps
"TestApps" = $settings.installTestApps
"Apps" = @()
"TestApps" = @()
}

if ($installAppsJson -and (Test-Path $installAppsJson)) {
try {
$install.Apps += @(Get-Content -Path $installAppsJson -Raw | ConvertFrom-Json)
$install.Apps = Get-Content -Path $installAppsJson | ConvertFrom-Json
}
catch {
throw "Failed to parse JSON file at path '$installAppsJson'. Error: $($_.Exception.Message)"
Expand All @@ -208,7 +208,7 @@ try {

if ($installTestAppsJson -and (Test-Path $installTestAppsJson)) {
try {
$install.TestApps += @(Get-Content -Path $installTestAppsJson -Raw | ConvertFrom-Json)
$install.TestApps = Get-Content -Path $installTestAppsJson | ConvertFrom-Json
}
catch {
throw "Failed to parse JSON file at path '$installTestAppsJson'. Error: $($_.Exception.Message)"
Expand All @@ -220,30 +220,6 @@ try {
$install.TestApps = $install.TestApps | ForEach-Object { $_.TrimStart("(").TrimEnd(")") }
}

# Replace secret names in install.apps and install.testApps
foreach($list in @('Apps','TestApps')) {
$install."$list" = @($install."$list" | ForEach-Object {
$pattern = '.*(\$\{\{\s*([^}]+?)\s*\}\}).*'
$url = $_
if ($url -match $pattern) {
$finalUrl = $url.Replace($matches[1],[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($secrets."$($matches[2])")))
}
else {
$finalUrl = $url
}
# Check validity of URL
if ($finalUrl -like 'http*://*') {
try {
Invoke-WebRequest -Method Head -UseBasicParsing -Uri $finalUrl | Out-Null
}
catch {
throw "Setting: install$($list) contains an inaccessible URL: $($url). Error was: $($_.Exception.Message)"
}
}
return $finalUrl
})
}

# Analyze app.json version dependencies before launching pipeline

# Analyze InstallApps and InstallTestApps before launching pipeline
Expand Down
7 changes: 7 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
- AL-Go repositories with large amounts of projects may run into issues with too large environment variables
- Discussion 1855 Add trigger 'workflow_call' to workflow 'Update AL-Go System Files' for reusability

### Improving error detection and build reliability when downloading project dependencies

The `DownloadProjectDependencies` action now downloads app files from URLs specified in the `installApps` and `installTestApps` settings upfront, rather than validating URLs at build time. This change provides:
- Earlier detection of inaccessible or misconfigured URLs
- Clearer error messages when secrets are missing or URLs are invalid
- Warnings for potential issues like duplicate filenames or insecure HTTP connections

### Set default values for workflow inputs

The `workflowDefaultInputs` setting now also applies to `workflow_call` inputs when an input with the same name exists for `workflow_dispatch`.
Expand Down
Loading
Loading