Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions Actions/AL-Go-Helper.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ function ConvertTo-HashTable() {
}
}

function Get-CurrentBranchName {
# $ENV:GITHUB_HEAD_REF is specified only for pull requests, so if it is not specified, use GITHUB_REF_NAME
$branchName = $ENV:GITHUB_HEAD_REF
if (!$branchName) {
$branchName = $ENV:GITHUB_REF_NAME
}
return $branchName.Replace('\', '_').Replace('/', '_')
}

function GetUniqueFolderName {
Param(
[string] $baseFolder,
Expand Down
8 changes: 1 addition & 7 deletions Actions/CalculateArtifactNames/CalculateArtifactNames.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ if ($project -eq ".") {
$project = $settings.repoName
}

$branchName = $ENV:GITHUB_HEAD_REF
# $ENV:GITHUB_HEAD_REF is specified only for pull requests, so if it is not specified, use GITHUB_REF_NAME
if (!$branchName) {
$branchName = $ENV:GITHUB_REF_NAME
}

$branchName = $branchName.Replace('\', '_').Replace('/', '_')
$branchName = Get-CurrentBranchName
$projectName = $project.Replace('\', '_').Replace('/', '_')

# If the buildmode is default, then we don't want to add it to the artifact name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
$errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0

. (Join-Path $PSScriptRoot "../AL-Go-Helper.ps1" -Resolve)
Import-Module (Join-Path $PSScriptRoot "DownloadProjectDependencies.psm1" -Resolve) -Force -DisableNameChecking

$projectDependencies = $ENV:_projectDependenciesJson | ConvertFrom-Json | ConvertTo-HashTable -recurse
$pattern = Get-DependencyArtifactPattern -Project $ENV:_project -ProjectDependencies $projectDependencies

if ($pattern) {
Write-Host "Dependency artifact pattern: $pattern"
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "hasPattern=true"
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "pattern=$pattern"
}
else {
Write-Host "No dependency projects found, skipping artifact download"
Add-Content -Encoding UTF8 -Path $env:GITHUB_OUTPUT -Value "hasPattern=false"
}
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,53 @@ function Get-DependenciesFromInstallApps {
return $install
}

<#
.SYNOPSIS
Computes a minimatch-compatible glob pattern for downloading only dependency-project artifacts.
.DESCRIPTION
Given a project and its dependency map (from projectDependenciesJson), constructs a brace-expansion
pattern that matches only the Apps, TestApps, and Dependencies artifacts for the dependency projects.
This pattern is designed for use with the actions/download-artifact 'pattern' input (which uses minimatch).

The pattern uses '*Apps' to match both 'Apps' and 'TestApps' as well as build-mode-prefixed variants
(e.g. 'CleanApps', 'CleanTestApps'). Similarly '*Dependencies' matches 'Dependencies' and variants.
.PARAMETER Project
The name of the current AL-Go project.
.PARAMETER ProjectDependencies
A hashtable mapping project names to arrays of their dependency project names (direct + transitive).
.OUTPUTS
A minimatch glob pattern string, or $null if the project has no dependencies.
#>
function Get-DependencyArtifactPattern {
param(
[Parameter(Mandatory = $true)]
[string] $Project,
[Parameter(Mandatory = $true)]
[hashtable] $ProjectDependencies
)

$dependencyProjects = @()
if ($ProjectDependencies.Keys -contains $Project) {
$dependencyProjects = @($ProjectDependencies."$Project")
}

if ($dependencyProjects.Count -eq 0) {
return $null
}

$branchName = Get-CurrentBranchName

# Build brace-expansion entries: 2 per dependency project (*Apps covers Apps+TestApps+buildMode variants)
$entries = @()
foreach ($dep in $dependencyProjects) {
$sanitizedDep = $dep.Replace('\', '_').Replace('/', '_')
$entries += "$sanitizedDep-$branchName-*Apps-*"
$entries += "$sanitizedDep-$branchName-*Dependencies-*"
}

return "{$($entries -join ',')}"
Comment thread
spetersenms marked this conversation as resolved.
}

<#
.SYNOPSIS
Resolves dependency file paths by extracting .app files from any zip archives.
Expand Down Expand Up @@ -369,4 +416,4 @@ function Resolve-DependencyFiles {
})
}

Export-ModuleMember -Function Get-AppFilesFromUrl, Get-AppFilesFromLocalPath, Get-DependenciesFromInstallApps, Resolve-DependencyFiles
Export-ModuleMember -Function Get-AppFilesFromUrl, Get-AppFilesFromLocalPath, Get-DependenciesFromInstallApps, Get-DependencyArtifactPattern, Resolve-DependencyFiles
2 changes: 2 additions & 0 deletions Actions/DownloadProjectDependencies/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Downloads artifacts from AL-Go projects, that are dependencies of a given AL-Go

The action constructs arrays of paths to .app files, that are dependencies of the apps in an AL-Go project

To optimize build performance, only artifacts from dependency projects are downloaded from the current workflow run (using a minimatch pattern filter), rather than downloading all artifacts.

## INPUT

### ENV variables
Expand Down
13 changes: 12 additions & 1 deletion Actions/DownloadProjectDependencies/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,20 @@ outputs:
runs:
using: composite
steps:
- name: Download artifacts from current build
- name: Compute dependency artifact pattern
id: computePattern
shell: ${{ inputs.shell }}
env:
_project: ${{ inputs.project }}
_projectDependenciesJson: ${{ inputs.projectDependenciesJson }}
run: |
${{ github.action_path }}/ComputeDependencyArtifactPattern.ps1
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Consider wrapping in Invoke-AlGoAction

- name: Download dependency artifacts from current build
if: steps.computePattern.outputs.hasPattern == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: ${{ steps.computePattern.outputs.pattern }}
path: ${{ github.workspace }}/.dependencies

- name: Download project dependencies
Expand Down
4 changes: 4 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Optimized dependency artifact downloads for multi-project repositories

The `DownloadProjectDependencies` action now downloads only artifacts from dependency projects instead of all workflow artifacts. For repositories with many AL-Go projects, this reduces build runner bandwidth and speeds up the dependency download step.

### Issues

- Incremental builds (`modifiedApps` mode) now correctly identify unmodified apps for projects whose `appFolders` reference paths outside the project directory (e.g. using `../`)
Expand Down
133 changes: 133 additions & 0 deletions Tests/DownloadProjectDependencies.Test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,139 @@ Describe "DownloadProjectDependencies - Get-DependenciesFromInstallApps Tests" {
}
}

Describe "DownloadProjectDependencies - Get-DependencyArtifactPattern Tests" {
BeforeEach {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'originalGitHubHeadRef', Justification = 'False positive.')]
$originalGitHubHeadRef = $ENV:GITHUB_HEAD_REF
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'originalGitHubRefName', Justification = 'False positive.')]
$originalGitHubRefName = $ENV:GITHUB_REF_NAME
}

AfterEach {
$ENV:GITHUB_HEAD_REF = $originalGitHubHeadRef
$ENV:GITHUB_REF_NAME = $originalGitHubRefName
}

It 'Returns null when project is not in the dependency map' {
$ENV:GITHUB_HEAD_REF = ''
$ENV:GITHUB_REF_NAME = 'main'

$projectDependencies = @{}
InModuleScope DownloadProjectDependencies -Parameters @{ Project = 'MyProject'; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -BeNullOrEmpty
}
}

It 'Returns null when project has an empty dependency array' {
$ENV:GITHUB_HEAD_REF = ''
$ENV:GITHUB_REF_NAME = 'main'

$projectDependencies = @{ 'MyProject' = @() }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = 'MyProject'; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -BeNullOrEmpty
}
}

It 'Returns correct pattern for a single dependency on the default branch' {
$ENV:GITHUB_HEAD_REF = ''
$ENV:GITHUB_REF_NAME = 'main'

$projectDependencies = @{ 'App' = @('Base') }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = 'App'; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -Be '{Base-main-*Apps-*,Base-main-*Dependencies-*}'
}
}

It 'Uses GITHUB_HEAD_REF over GITHUB_REF_NAME and sanitizes branch slashes' {
$ENV:GITHUB_HEAD_REF = 'feature/auth'
$ENV:GITHUB_REF_NAME = 'main'

$projectDependencies = @{ 'App' = @('Base') }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = 'App'; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -Be '{Base-feature_auth-*Apps-*,Base-feature_auth-*Dependencies-*}'
}
}
}

Describe "DownloadProjectDependencies - Get-DependencyArtifactPattern Advanced Tests" {
BeforeEach {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'originalHeadRef', Justification = 'False positive.')]
$originalHeadRef = $ENV:GITHUB_HEAD_REF
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'originalRefName', Justification = 'False positive.')]
$originalRefName = $ENV:GITHUB_REF_NAME

$ENV:GITHUB_HEAD_REF = ''
$ENV:GITHUB_REF_NAME = 'main'
}

AfterEach {
$ENV:GITHUB_HEAD_REF = $originalHeadRef
$ENV:GITHUB_REF_NAME = $originalRefName
}

It 'Returns pattern with 4 brace entries for two dependencies' {
$projectDependencies = @{ "App" = @("Base", "Common") }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = "App"; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -Be "{Base-main-*Apps-*,Base-main-*Dependencies-*,Common-main-*Apps-*,Common-main-*Dependencies-*}"
}
}

It 'Returns pattern with 6 brace entries for three flattened transitive dependencies' {
$projectDependencies = @{ "App" = @("Base", "Common", "Core") }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = "App"; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -Not -BeNullOrEmpty
$result | Should -BeLike "*Base-main-*"
$result | Should -BeLike "*Common-main-*"
$result | Should -BeLike "*Core-main-*"
# 3 deps x 2 entries each = 6 entries = 5 commas
($result.ToCharArray() | Where-Object { $_ -eq ',' }).Count | Should -Be 5
}
}

It 'Sanitizes forward slashes in project dependency names' {
$projectDependencies = @{ "App" = @("src/Common") }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = "App"; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -BeLike "*src_Common-main-*"
$result | Should -Not -BeLike "*src/Common*"
}
}

It 'Sanitizes forward slashes in branch name from GITHUB_REF_NAME' {
$ENV:GITHUB_HEAD_REF = ''
$ENV:GITHUB_REF_NAME = 'release/v2.0'
$projectDependencies = @{ "App" = @("Base") }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = "App"; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -BeLike "*Base-release_v2.0-*"
$result | Should -Not -BeLike "*release/v2.0*"
}
}

It 'Returns null when project is not in the dependencies map even if other projects exist' {
$projectDependencies = @{ "Other" = @("Base") }
InModuleScope DownloadProjectDependencies -Parameters @{ Project = "App"; ProjectDependencies = $projectDependencies } {
param($Project, $ProjectDependencies)
$result = Get-DependencyArtifactPattern -Project $Project -ProjectDependencies $ProjectDependencies
$result | Should -BeNullOrEmpty
}
}
}

Describe "DownloadProjectDependencies - Resolve-DependencyFiles Tests" {
BeforeEach {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'testFolder', Justification = 'False positive.')]
Expand Down
Loading