Skip to content

Reuse apps already published in the BC artifact database - #9816

Draft
Groenbech96 wants to merge 6 commits into
mainfrom
user/magnushar/artifact-baseline-app-reuse
Draft

Reuse apps already published in the BC artifact database#9816
Groenbech96 wants to merge 6 commits into
mainfrom
user/magnushar/artifact-baseline-app-reuse

Conversation

@Groenbech96

@Groenbech96 Groenbech96 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What & why

Every Test Apps * build restores the BC artifact database — in which all apps are already published, installed and synchronized — and then throws it away, so AL-Go republishes ~300 apps one at a time before a single test runs. Install-App Packages alone takes ~46 minutes.

# build/scripts/NewBcContainer.ps1 (before)
# Clean the container for all apps. Apps will be installed by AL-Go
foreach($app in $installedApps) { UnInstall-BcContainerApp ...; Unpublish-BcContainerApp ... }

The apps in the artifact are built by the NAV build from a known BCApps commit. The companion NAV change records it in the artifact manifest as bcAppsCommit, so this PR can diff that commit against the commit being built, work out which apps actually changed, and refresh only those.

Retained apps are uninstalled but left published and synchronized — exactly the state AL-Go's publish step produces today. That keeps two invariants intact: ImportTestDataInBcContainer.ps1 retains full control of install order (which the Legacy type depends on), and test discovery — driven by runTestsInAllInstalledTestApps from the installed apps — is unchanged. Only the publish/sync work is skipped.

No extra storage, nothing to upload or invalidate: the artifact is downloaded and restored by the build anyway.

Off by default. Nothing changes until "useArtifactBaseline": true is set in .github/AL-Go-Settings.json.

Design, rejected storage alternatives and the full fallback matrix: build/scripts/ArtifactBaseline.md.

Only the owning app, never its dependents

The first revision of this PR expanded the refresh set down the dependency graph, via BuildOptimization's Get-AffectedApps, and also consulted fullBuildPatterns. Both were the wrong tool.

Those exist to answer "which tests must we run?" — a change in a dependency can change a dependent's behaviour, so expanding downstream is correct there. "Which published binaries are still valid?" is a different question. An app whose own source did not change has identical source at both commits, is already published, installed and synchronized in the restored database, and still resolves its dependencies because BCApps pins them at <major>.<minor>.0.0, so a refreshed Base Application at 29.0.2147483647.x still satisfies it.

Measured on a real 16-commit, 3-day window (1e77e9c2..28ff6645, 432 changed files, 841 apps):

apps to republish reused
expanded to dependents (previous revision) 841 0 (0.0%)
the app that owns the change 10 831 (98.8%)

src/Layers/W1/BaseApp changed in that window, and everything depends on Base Application. Base Application changes in nearly every multi-day window, so the previous revision would have fired approximately never — on top of fullBuildPatterns vetoing the whole thing on build/Packages.json and src/DisabledTests/* before any app mapping happened.

Per container, after intersecting with the apps the container actually holds:

Test Apps W1   chain W1        container 232 apps    republish  8    keep 224
Test Apps DK   chain W1 -> DK  container 258 apps    republish 10    keep 248

Country layers go through the view chain

A country layer folder carries no app.json. PreCompileApp.ps1 calls New-GDLView, which composes src/Views/<CC> from a chain declared in src/Layers/.config/views_config.json, and compiles the localized app from that view:

W1   -> [W1]           DK -> [W1, DK]
NA   -> [W1, NA]       US -> [W1, NA, US]
DACH -> [W1, DACH]     AT -> [W1, DACH, AT]

A change to src/Layers/<L>/<AppFolder>/** now marks <AppFolder> for republish only when L is in chain(CC), resolving app identity by walking the chain from the most specific layer back to the base. For Test Apps W1 the RU/IT/ES/NA/APAC layer changes (316 of the 432 files in the example) are genuinely irrelevant; for Test Apps DK a src/Layers/DK/BaseApp change does invalidate that container's Base Application. Ignoring country layers wholesale would publish a stale localized Base Application — a correctness bug, not a missed optimization.

The country comes from the artifact url (.../sandbox/29.0.53247.0/w1w1), which is the country the container is built for.

What is deliberately not an invalidator

A retained app is never compiled by this build — its binary is fixed inside the artifact — so no repository-side setting can retroactively change it. Rulesets and disabled-test lists are diagnostic and test-selection inputs: they can fail a build, they cannot make a published .app wrong.

The artifact and platform pins matter most here. In the example window they are the only build/** and .github/** changes:

.github/AL-Go-Settings.json   artifact   29.0.53094.0 -> 29.0.53221.0
build/Packages.json           BCPlatform 29.0.53093.0 -> 29.0.53216.0

That is UpdateBCArtifactVersion housekeeping, roughly weekly. Bumping a pin downloads a newer artifact carrying a newer bcAppsCommit, which makes the diff smaller. Treating those files as a full-refresh trigger meant the commit that improves this optimization was the one that switched it off.

Linked work

⚠️ No approved issue yet — this is a draft and needs one before review.

Companion change: NAV PR 251642, shipped 2026-07-31. Verified in a published artifact: sandbox/29.0.53247.0/w1 and .../dk both carry "bcAppsCommit": "1e77e9c2e8d4c58e45e32be2876550dbb68b773c", which is a real commit on BCApps main. The repo-level base artifact is not stamped, but no Test Apps project uses it — Test Apps W1 sets country: w1 and each localization project sets its own.

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I built the affected app(s) locally with no new analyzer warnings. — n/a: no AL app is touched, this is build tooling only.
  • I ran the change in Business Central and confirmed it behaves as expected. — not done. This is the main gap; see below.
  • I added or updated tests for the new behavior.

What I tested and the outcome

  • 65 Pester tests in build/scripts/tests/ArtifactBaseline.Test.ps1, all passing: commit resolution, country extraction from the artifact url, layer-chain resolution (W1, DK, AT, US, unknown), the explicit no dependent expansion contract, per-country layer attribution in both directions, the inert-path list, unattributable and unrecognized paths, the ratio guard, the unpublish/keep split, stale-state rejection and every fallback.
  • Full build/scripts suite: 181/182. The one failure is the pre-existing BuildOptimization "50 vs 51 affected apps" assertion — it fails identically on an untouched main (a newly added Payables Agent app), and is untouched here.
  • Replayed against the real delta: the artifact commit read from the live w1 manifest, diffed against main, produces the 10 apps above and the 8/10 per-container split.
  • Verified against a real artifact on disk that app names match repo app.json names, and that BCApps dependencies are pinned at <major>.<minor>.0.0 so a reused app at the artifact's version still satisfies apps built at 29.0.2147483647.x.
  • Not yet done: a real CI run. This code path has still never executed in CI. The actual saving, and -upgrade/install behavior against artifact-installed apps, must be measured on a real Test Apps build before this is enabled.

Risk & compatibility

  • Everything is behind useArtifactBaseline, which is not set. With it unset, both hooks behave exactly as before.
  • Fallbacks to today's full clean: no commit in the manifest; commit unreachable (a shallow CI clone is fetched once, a complete clone is never made shallow); git diff failure; a build mode that changes compilation (derived from conditionalSettings.preprocessorSymbols, i.e. Clean); a changed src/ file that maps to no app; a layer change whose country has no resolvable view chain; an unrecognized path; or >75% of apps changed.
  • Provenance: reused apps are the NAV-built binaries at the artifact version rather than the ones this build compiled — same source commit, different compiler invocation. Anything asserting on exact app versions would notice.
  • Container parity: apps this repo does not produce, and apps that must never be in a test container (Library - No Transactions, Prevent Metadata Updates Library), are always unpublished, so the container holds what it holds today.
  • Per-country validation. The chain rule is exercised by unit tests for W1 and DK, but only a real CI run per country will prove the localized Base Application is refreshed when it should be. Enable W1 first.
  • Freshness: the payoff shrinks as the artifact ages. UpdateBCArtifactVersion already keeps it current — and, unlike the previous revision, that bump no longer disables the optimization.
  • Follow-up (out of scope): demo data generation is now the biggest remaining fixed cost, and the artifact database already contains a populated CRONUS company.

Test builds unpublish every app from the restored artifact database and
republish ~300 of them before a single test runs. The artifact is built from
a known BCApps commit, which the NAV build now records in the artifact
manifest as "bcAppsCommit". Use it: diff that commit against the commit being
built, map the changed files to apps via the existing dependency graph, and
only unpublish/republish those apps and their dependents.

Off by default - enable with "useArtifactBaseline": true in
.github/AL-Go-Settings.json. Anything that cannot be proven safe falls back to
today's behavior: no commit in the manifest, an unreachable commit, a build
mode other than Default, a fullBuildPatterns hit, an unmappable src file, or a
change set covering most of the apps.

See build/scripts/ArtifactBaseline.md.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added Build: scripts & configs Build scripts and configuration files Other GitHub request for other area than SCM, Finance or Integration Ownership: Needs Review Ownership is Other, low confidence, or needs manual correction labels Jul 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Could not find a linked ADO work item. Please link one by using the pattern 'AB#' followed by the relevant work item number. You may use the 'Fixes' keyword to automatically resolve the work item when the pull request is merged. E.g. 'Fixes AB#1234'

Magnus Hartvig Grønbech and others added 3 commits July 29, 2026 13:31
…ontainer shape

Review found that the feature could never activate, and that fixing that would
expose three real correctness problems. Both are addressed here.

The guard required BuildMode -eq 'Default', but no container-building project uses
that mode: the test projects build in IntegrationTests / UncategorizedTests /
LegacyTestsBucket1 / LegacyTestsBucket2. Reuse was therefore skipped in 100% of CI
builds. The guard now derives the unsafe modes from the settings - any build mode
whose conditionalSettings define preprocessorSymbols, which today means 'Clean' -
and a regression test asserts the four real modes are allowed.

Retained apps are now uninstalled but left published and synchronized, which is the
state AL-Go's publish step produces today. This keeps two invariants the previous
version broke once reuse could actually happen:

- ImportTestDataInBcContainer.ps1 keeps control of install order. The Legacy test
  type installs base apps + DemoTool, generates demo data, and only then installs
  the rest; leaving apps installed made Install-BaseAppsForDemoTool a silent no-op.
- Test discovery is unchanged. Every test project sets runTestsInAllInstalledTestApps,
  so the executed test set is derived from the INSTALLED apps.

Further correctness fixes:

- Identify apps by publisher + name. Several names are not unique in this repository
  (Tests-Local in the BE/MX/W1 layers, Data Archive in src/Apps and src/System
  Application), and name-only matching could skip publishing an app the container
  never held.
- Always unpublish the apps that must never be in a test container. The list moved
  into the module so the container hook and the publish hook agree on it; both
  entries are BCApps apps, so they previously looked like ordinary reusable apps -
  and Prevent Metadata Updates Library changes runtime behavior.
- Treat .github/** and project settings.json as full-build triggers. They change how
  apps compile but match no fullBuildPattern and map to no app.
- Stamp the state with the container name and run/attempt, and ignore state that does
  not match. The publish hook runs in a separate process against a shared temp folder.
- Ignore BCAPPS_ARTIFACT_BASELINE_COMMIT in CI so the manifest is the only source of
  truth there.
- Only fetch a missing commit into an already-shallow clone, so a developer's complete
  clone is never made shallow.
- Look up bcContainerHelperConfig without constraining the scope, and wrap the whole
  reuse block in try/catch so it can never introduce a failure the old code did not have.

Tests: 53 (was 35), including the build-mode regression, publisher collisions, the
exclusion list, stale-state rejection and an end-to-end resolve. The hook smoke test
now runs with LegacyTestsBucket1 instead of Default - the fixture that hid the bug.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The reuse decision was built on BuildOptimization's Get-AffectedApps and
fullBuildPatterns. Both answer a different question - "which tests must we
run?" - where a change in a dependency can change a dependent's behaviour, so
expanding downstream is correct there.

"Which published binaries are still valid?" is not that question. An app whose
own source did not change has identical source at both commits, is already
published, installed and synchronized in the restored artifact database, and
still resolves its dependencies because BCApps pins them at <major>.<minor>.0.0.

Measured on a real 16-commit, 3-day window (1e77e9c..28ff664, 432 changed
files, 841 apps): expanding to dependents marks 841 of 841 apps, because
src/Layers/W1/BaseApp changed and everything depends on Base Application.
Attributing each file to the app that owns it marks 10. Base Application
changes in nearly every multi-day window, so the previous logic would have
fired approximately never.

Changes:

- Get-ChangedAppNames attributes each changed file to exactly one app and no
  longer calls Get-AffectedApps or Test-FullBuildPatternsMatch.
- Country layers are resolved through the GDL view chain. A layer folder has no
  app.json of its own; PreCompileApp.ps1 composes src/Views/<CC> from the chain
  in src/Layers/.config/views_config.json. A change in layer L now marks its app
  only for a build of a country whose chain contains L, so src/Layers/DK affects
  the DK container's Base Application but not W1's. Ignoring country layers
  altogether would publish a stale localized Base Application.
- The country is taken from the artifact url, which is the country the container
  is built for.
- Paths that cannot invalidate an already-published binary are listed
  explicitly: rulesets and disabled-test lists are diagnostic inputs, and the
  artifact/platform pins fetch a NEWER artifact carrying a newer bcAppsCommit,
  which makes the diff smaller. Treating those as a full-refresh trigger meant
  the weekly UpdateBCArtifactVersion commit disabled the optimization.
- Unattributable and unrecognized paths still fall back to a full clean.

Also corrects the NAV-side reference: the stamp comes from Get-BCAppsCommitSha
in Eng\Normal\Lib\SubmodulesHelper.psm1 (NAV PR 251642), verified present in
the published sandbox/29.0.53247.0/w1 and /dk artifacts.

Tests: 65 pass in ArtifactBaseline.Test.ps1, covering chain resolution, the
no-dependent-expansion contract, per-country layer attribution and the inert
path list. Full build/scripts suite 181/182 - the remaining failure is the
pre-existing BuildOptimization "50 vs 51 affected apps" assertion, which fails
identically on an untouched main.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 26e96054-4eb6-4b68-8d9a-4aee2f5b27df
@microsoft microsoft deleted a comment from github-actions Bot Aug 3, 2026
'Run PS Tests' only runs on PRs that touch build/**, so nothing exercises this
assertion in between. The hard-coded "50 affected apps" silently rotted when
Payables Agent was added to the E-Document dependency set, and the test now
fails on an untouched main - blocking the next build-scripts PR that comes
along, which happens to be this one.

Bumping the number to 51 would just move the trip wire. Assert what the test
actually means instead: the change reaches E-Document Core, it does not fan out
to the whole repository, and every app in the affected set genuinely depends on
E-Document Core. That is stronger than a count and cannot drift when an app is
added or removed.

Verified: the full build/scripts suite is 182/182 with this change, and the
invariant holds for all 51 apps currently in the set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 26e96054-4eb6-4b68-8d9a-4aee2f5b27df

$configPath = Join-Path $BaseFolder ($script:LayersConfigPath -replace '/', '\')
if (-not (Test-Path $configPath)) {
return @()
$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json
}
catch {
return @()
$resolved = $names | Where-Object { $_ -eq $CountryCode.ToUpperInvariant() }
}
if (-not $resolved) {
return @()
$guard++
}

return @($chain)
The selection logic has unit tests, but nothing has ever exercised the hooks
end to end: no CI run has created a container with this on, so the container
shape, install order, test discovery and the actual time saved are all still
unproven.

Turn it on for one project rather than repo-wide. Get-ALGoSetting reads
$env:settings, which AL-Go populates per project, so this stays scoped to
Test Apps W1 and every other project keeps today's behaviour.

Test Apps W1 builds in IntegrationTests, UncategorizedTests, LegacyTestsBucket1
and LegacyTestsBucket2, so this also exercises the Legacy buckets - the riskiest
path, because they depend on Install-BaseAppsForDemoTool seeing an uninstalled
container.

This is an experiment, not a claim that it works. What to read from the run:

  ARTIFACT BASELINE: <n> file(s) changed since artifact commit <sha>
  ARTIFACT BASELINE: country 'w1', layer chain 'W1'
  ARTIFACT BASELINE: usable=True ... N of 841 apps changed since the artifact
  ARTIFACT BASELINE: reusing X app(s) from the artifact, refreshing Y
  ARTIFACT BASELINE: skipping <k> app(s) already published by the artifact

An empty sha, an empty chain, usable=False or N near 841 all mean it fell back
and nothing was saved. Test count must match a baseline run exactly, since
discovery comes from the installed apps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 26e96054-4eb6-4b68-8d9a-4aee2f5b27df
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Build: scripts & configs Build scripts and configuration files Other GitHub request for other area than SCM, Finance or Integration Ownership: Needs Review Ownership is Other, low confidence, or needs manual correction

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants