SIGN IN SIGN UP

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