Fix modifiedApps incremental builds for projects with external appFolders (#2194)
## Summary
When `incrementalBuilds.mode` is set to `modifiedApps` and a project
references app folders **outside** its own directory via `../` paths,
unmodified apps are never downloaded from the baseline workflow run.
Instead, every app is recompiled on every PR — even when only a single
file changed.
## The bug
`Get-UnmodifiedAppsFromBaselineWorkflowRun` needs to match entries from
`$settings.appFolders` (project-relative paths) against `$skipFolders`
(repo-relative paths). The old code assumed all resolved folder paths
start with `.\` and used `SubString(2)` to strip that prefix:
```powershell
# Old code
$downloadAppFolders = @($settings.appFolders | Where-Object {
$skipFolders -contains "$projectWithSeperator$($_.SubString(2))"
})
```
This works when apps live **inside** the project folder (paths like
`.\app`), but breaks when apps live **outside** (paths like
`..\..\..\src\Apps\SomeApp\App`).
### Toy example to illustrate
Consider a repo layout where a project at `build/projects/MyProject`
references apps at `src/`:
```
repo/
src/
Apps/
Invoicing/App/app.json ← modified
Reporting/App/app.json ← unmodified
build/
projects/
MyProject/
.AL-Go/settings.json ← appFolders: ["../../../src/Apps/*/App"]
```
After `ResolveProjectFolders` resolves the glob from the project
directory, `$settings.appFolders` contains:
```
..\..\..\src\Apps\Invoicing\App
..\..\..\src\Apps\Reporting\App
```
Meanwhile, `$skipFolders` (from `GetFoldersFromAllProjects`, resolved
from the repo root) contains:
```
src\Apps\Reporting\App
```
The old matching logic did:
| Step | Value |
|---|---|
| `$_.SubString(2)` on `..\..\..\src\Apps\Reporting\App` |
`\..\..\src\Apps\Reporting\App` |
| Prepend project separator |
`build\projects\MyProject\..\..\src\Apps\Reporting\App` |
| Compare with `$skipFolders` | `src\Apps\Reporting\App` |
| **Match?** | **No — string comparison, not path resolution** |
The result: `$downloadAppFolders` is always empty → no artifacts
downloaded from baseline → **all apps recompiled**.
For comparison, the standard layout (apps inside the project) works
because paths start with `.\`:
| Step | Value |
|---|---|
| `$_.SubString(2)` on `.\app` | `app` |
| Prepend project separator (empty for root project) | `app` |
| Compare with `$skipFolders` | `app` |
| **Match?** | **Yes** |
## The fix
Replace the `SubString(2)` hack with proper path resolution. A new
helper `ConvertTo-RepoRelativePath` resolves each project-relative
folder to an absolute path via `Join-Path -Resolve`, then strips the
base folder prefix to produce a clean repo-relative path that matches
`$skipFolders` exactly:
```powershell
# New code
function ConvertTo-RepoRelativePath {
param([string] $folder, [string] $projectPath, [string] $baseFolder)
$fullPath = Join-Path $projectPath $folder -Resolve -ErrorAction SilentlyContinue
if (-not $fullPath) { return $null }
$normalizedBase = $baseFolder.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar
if ($fullPath.StartsWith($normalizedBase, [StringComparison]::OrdinalIgnoreCase)) {
return $fullPath.Substring($normalizedBase.Length)
}
return $null
}
```
This works for both layouts:
| Layout | `$settings.appFolders` entry | Resolved absolute |
Repo-relative | Matches `$skipFolders`? |
|---|---|---|---|---|
| Nested project | `..\..\..\src\Apps\Reporting\App` |
`D:\a\repo\src\Apps\Reporting\App` | `src\Apps\Reporting\App` | Yes |
| Standard | `.\app` | `D:\a\repo\app` | `app` | Yes |
## Tests
Two new Pester tests added to `DetermineProjectsToBuild.Test.ps1`:
1. **Nested project with `../` paths** — creates a repo layout where
apps are outside the project folder. **Fails without the fix**
(`Download appFolders: - None`), **passes with the fix**.
2. **Standard layout** — apps inside the project folder. **Passes both
before and after** (backwards compatibility).
## Test plan
- [x] New test fails on old code, passes on new code
- [x] All 28 existing tests continue to pass
- [ ] Validate on a real BCApps PR build
---------
Co-authored-by: Magnus Hartvig Grønbech <magnushar@microsoft.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Maria Zhelezova <43066499+mazhelez@users.noreply.github.com>
Co-authored-by: Alexander Holstrup <117829001+aholstrup1@users.noreply.github.com>
Co-authored-by: aholstrup1 <aholstrup1@users.noreply.github.com> M
Magnus Hartvig Grønbech committed
6b7ca4dbcbaa2532be5f15217ca67a72a8d5caf4
Parent: 040d14c
Committed by GitHub <noreply@github.com>
on 4/21/2026, 1:16:05 PM