Skip to content
Open
43 changes: 36 additions & 7 deletions Actions/.Modules/ReadSettings.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ $CustomTemplateProjectSettingsFile = Join-Path '.github' $CustomTemplateProjectS
function MergeCustomObjectIntoOrderedDictionary {
Param(
[System.Collections.Specialized.OrderedDictionary] $dst,
[PSCustomObject] $src
[PSCustomObject] $src,
[string[]] $srcImportantSettings = @(),
[string[]] $dstImportantSettings = @()
)

# If the src object contains property 'overwriteSettings' (list of settings), remove these settings from the dst object, so that they can be re-added with the new value later on
if ($src.PSObject.Properties.Name -contains "overwriteSettings") {
$src.overwriteSettings | ForEach-Object {
$prop = $_
if ($dstImportantSettings -contains $prop -and $srcImportantSettings -notcontains $prop) {
OutputDebug "Ignoring overwriteSettings for '$prop' because it is important in higher priority settings and not marked important in source"
return
}
if ($dst.Contains($prop) -and $src.PSObject.Properties.Name -contains $prop) {
# Remove the property from the destination object only if it also exists in the source object. The property will be re-added with the new value later on.
OutputDebug "Overwriting setting $prop"
Expand All @@ -37,7 +43,7 @@ function MergeCustomObjectIntoOrderedDictionary {
$src.PSObject.Properties.GetEnumerator() | ForEach-Object {
$prop = $_.Name

# Skip overwriteSettings property as it's only used to remove settings from the destination object and is specific to the source object
# Skip overwriteSettings property as it's only used for configuration, not actual settings

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.

Does importantSettings need to be transferred here? I see they are accumulated in $currentImportantSettings

@ChrisBlankDe Chris Blank (ChrisBlankDe) Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this is unnecessary from a technical standpoint, but I still think it makes sense to include them so that later on e.g., during debugging it’s clear why a setting was inherited in a certain way.
So far, we’ve been doing this with conditional settings as well. These remain part of the settings indefinitely, even after they’ve been completely resolved.

I've also considered no longer accumulating the $currentImportantSettings outside of the MergeCustomObjectIntoOrderedDictionary function, but instead just accessing the property within the function itself.
However, I think it's a cleaner design to keep the control state separate from the data state.

What do you think, keep the cleaner design or should we try to remove $currentImportantSettings?

if ($prop -eq "overwriteSettings") {
return
}
Expand All @@ -62,7 +68,7 @@ function MergeCustomObjectIntoOrderedDictionary {
# If the property exists in the source object, but is of a different type, throw an error
# If the property exists in the source object:
# If the property is an Object, call this function recursively to merge values
# If the property is an Object[], merge the arrays
# If the property is an Object[], merge the arrays (even if important - arrays always merge)

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.

That's not intuitive, why would arrays always merge even the setting in the source is marked as important?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I wasn't sure about that either, but I couldn't think of a better solution.

I think it's important for arrays that the “important” values are preserved. For example, with the buildModes, the “important” ones should always be executed, but also it should be possible to add others.

If I were to change the behavior here so that they aren’t always merged, that would mean that whenever I want to add buildModes, I’d also have to set overwriteSettings: ["buildModes"] and include the earlier-level values in the overwrite. I think this is a bit clunky, since, in my opinion, “overwrite” should only be used to remove values from earlier levels.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the biggest problem with this is that some settings are dependent of each other - shell, runs-on, useCompilerFolder, etc. and it becomes very hard to understand why a setting, which I explicitly set in my project now suddenly doesn't work because somebody has added a higher level importantSettings.

I would rather have a setup where a higher level could specify importantSettings - but it didn't affect the merge of settings, but instead - it would give an error when the setting was overridden lower down - then people can fix this - meaning a non-intrusive behavior really.

Another thing we could implement was a dump after readsettings, where it would print out specifically which settings where read from which files:

artifact - taken from project setting xxxx
runs-on - taken from .github/AL-Go-settings.json
.....

That would help people setup the right settings.
Adding overwrite and important settings seems to only complicate things, sorry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Freddy Kristiansen (@freddydk) Your comment isn't about how to handle arrays, but is more of a general point, right?

it would give an error when the setting was overridden lower down

If we look at Use Case 2 from the PR description, this would be disadvantageous. If I have to define the country in the repo again for the build mode, I might as well skip entering it in the organization settings altogether. Furthermore, I think that anyone who is allowed to edit earlier settings (org + repo) should have the foresight and sense of responsibility to be able to override the repo settings.

Another thing we could implement was a dump after readsettings, where it would print out specifically which settings where read from which files:

artifact - taken from project setting xxxx
runs-on - taken from .github/AL-Go-settings.json

i could do that, Maria Zhelezova (@mazhelez) should i?
print always to the logs or just with ::debug:: annotation?

# If the property is a simple type, replace the value in the destination object with the value from the source object
@($dst.Keys) | ForEach-Object {
$prop = $_
Expand All @@ -71,6 +77,13 @@ function MergeCustomObjectIntoOrderedDictionary {
$srcProp = $src."$prop"
$dstPropType = $dstProp.GetType().Name
$srcPropType = $srcProp.GetType().Name

# For non-array properties: skip if this setting is marked as important from higher priority source,
# unless the lower-priority source also marks this property as important.
if ($dstImportantSettings -contains $prop -and $srcPropType -ne "Object[]" -and $srcImportantSettings -notcontains $prop) {
OutputDebug "Skipping important setting '$prop' marked from higher priority source (non-array type)"
return
}
if ($srcPropType -eq "PSCustomObject" -and $dstPropType -eq "OrderedDictionary") {
MergeCustomObjectIntoOrderedDictionary -dst $dst."$prop" -src $srcProp
}
Expand Down Expand Up @@ -262,7 +275,8 @@ function GetDefaultSettings
"filesToInclude" = @()
"filesToExclude" = @()
}
"postponeProjectInBuildOrder" = $false
"postponeProjectInBuildOrder" = $false
"importantSettings" = @()
}
}

Expand Down Expand Up @@ -483,11 +497,19 @@ function ReadSettings {
}
}

foreach($settingsObject in $settingsObjects) {
$currentImportantSettings = @()
foreach ($settingsObject in $settingsObjects) {
$settingsJson = $settingsObject.Settings
if ($settingsJson) {
OutputDebug "Applying settings from $($settingsObject.Source) ($($settingsObject.Type))"
MergeCustomObjectIntoOrderedDictionary -dst $settings -src $settingsJson
$srcImportantSettings = @()
if ($settingsJson.PSObject.Properties.Name -contains "importantSettings") {
$srcImportantSettings = @($settingsJson.importantSettings)
}
MergeCustomObjectIntoOrderedDictionary -dst $settings -src $settingsJson -srcImportantSettings $srcImportantSettings -dstImportantSettings $currentImportantSettings
if ($settingsJson.PSObject.Properties.Name -contains "importantSettings") {
$currentImportantSettings = @($currentImportantSettings + $srcImportantSettings | Select-Object -Unique)
}
if ($settingsJson.PSObject.Properties.Name -eq "ConditionalSettings") {
foreach($conditionalSetting in $settingsJson.ConditionalSettings) {
if ("$conditionalSetting" -ne "") {
Expand All @@ -509,7 +531,14 @@ function ReadSettings {
}
if ($conditionMet) {
OutputDebug "Applying conditional settings for $($conditions -join ", ")"
MergeCustomObjectIntoOrderedDictionary -dst $settings -src $conditionalSetting.settings
$srcImportantSettings = @()
if ($conditionalSetting.settings.PSObject.Properties.Name -contains "importantSettings") {
$srcImportantSettings = @($conditionalSetting.settings.importantSettings)
}
MergeCustomObjectIntoOrderedDictionary -dst $settings -src $conditionalSetting.settings -srcImportantSettings $srcImportantSettings -dstImportantSettings $currentImportantSettings
if ($conditionalSetting.settings.PSObject.Properties.Name -contains "importantSettings") {
$currentImportantSettings = @($currentImportantSettings + $srcImportantSettings | Select-Object -Unique)
}
}
}
}
Expand Down
16 changes: 12 additions & 4 deletions Actions/.Modules/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@
"type": "string",
"enum": ["includeDependencies"]
},
"description": "An array of settings to be overwritten by the current deliverToAppSource setting. See https://aka.ms/ALGoSettings#overwriteSettings"
"description": "An array of settings to be overwritten by the current deliverToAppSource setting. If a setting is marked as important in higher-priority settings, overwrite is ignored unless the current source also marks that same setting as important. See https://aka.ms/ALGoSettings#overwriteSettings"
}
},
"required": [
Expand Down Expand Up @@ -654,7 +654,7 @@
"type": "string",
"enum": ["includeProjects", "excludeProjects"]
},
"description": "An array of settings to be overwritten by the current alDoc setting. See https://aka.ms/ALGoSettings#overwriteSettings"
"description": "An array of settings to be overwritten by the current alDoc setting. If a setting is marked as important in higher-priority settings, overwrite is ignored unless the current source also marks that same setting as important. See https://aka.ms/ALGoSettings#overwriteSettings"
}
},
"commitOptions": {
Expand Down Expand Up @@ -684,7 +684,7 @@
"type": "string",
"enum": ["pullRequestLabels"]
},
"description": "An array of settings to be overwritten by the current commitOptions setting. See https://aka.ms/ALGoSettings#overwriteSettings"
"description": "An array of settings to be overwritten by the current commitOptions setting. If a setting is marked as important in higher-priority settings, overwrite is ignored unless the current source also marks that same setting as important. See https://aka.ms/ALGoSettings#overwriteSettings"
}
},
"required": [
Expand Down Expand Up @@ -732,7 +732,15 @@
"type": "string",
"enum": ["unusedALGoSystemFiles", "projects", "additionalCountries", "appDependencies", "appFolders", "testDependencies", "testFolders", "bcptTestFolders", "pageScriptingTests", "restoreDatabases", "installApps", "installTestApps", "customCodeCops", "configPackages", "appSourceCopMandatoryAffixes", "deliverToAppSource", "appDependencyProbingPaths", "incrementalBuilds", "environments", "buildModes", "bcptThresholds", "fullBuildPatterns", "excludeEnvironments", "alDoc", "commitOptions", "trustedSigning"]
},
"description": "An array of settings to be overwritten by the current settings. See https://aka.ms/ALGoSettings#overwriteSettings"
"description": "An array of settings to be overwritten by the current settings. If a setting is marked as important in higher-priority settings, overwrite is ignored unless the current source also marks that same setting as important. See https://aka.ms/ALGoSettings#overwriteSettings"
},
"importantSettings": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "An array of top-level setting names that are protected from non-important lower-priority overrides. If the same setting is marked as important in both source and destination, the source value is allowed to override. See https://aka.ms/ALGoSettings#importantSettings"
},
"reportSuppressedDiagnostics": {
"type": "boolean",
Expand Down
35 changes: 35 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
### Important settings protection
Comment thread
mazhelez marked this conversation as resolved.
Comment thread
ChrisBlankDe marked this conversation as resolved.

A new `importantSettings` setting allows you to protect specific settings from being overridden by lower-priority sources in the settings hierarchy. When a setting is marked as important at a higher priority level, it cannot be overridden by non-important settings from lower priority sources.

```json
{
"importantSettings": ["country", "keyVaultName"],
"country": "de",
"keyVaultName": "orgVault"
}
```

**Behavior:**
- Settings marked as important in organization or repository settings cannot be overridden by non-important values from project, workflow, user, or environment settings
- If a lower-priority source also marks the same setting as important, the lower-priority value is allowed to override
- Important arrays are still merged by default
- The `overwriteSettings` mechanism can replace an important setting only when the source also marks that same setting as important
- `ConditionalSettings` respect importantSettings markings, allowing you to enforce conditional important settings based on buildMode, branch, trigger, or user

**Example with ConditionalSettings:**
```json
{
"ConditionalSettings": [
{
"buildModes": ["Validate"],
"settings": {
"importantSettings": ["country"],
"country": "us"
}
}
]
}
```

### New `doNotPerformUpgrade` setting

AL-Go now supports a new `doNotPerformUpgrade` setting that is passed through to `Run-AlPipeline`. Use it to skip the upgrade phase while still running the rest of the pipeline.
Expand All @@ -22,6 +56,7 @@ Workspace compilation now finds altool both in the platform-specific subfolder (

## v9.1


### Resilient Pull Request Status Check for large builds

The Pull Request Status Check action no longer fails on builds with more than one page of jobs (more than 100 jobs). The jobs API call now uses `--slurp` so multi-page responses are parsed as a single JSON array (previously `gh api --paginate | ConvertFrom-Json` failed with "Invalid JSON primitive" when more than one page was returned). The call is also retried, and requests a smaller page size, to tolerate the intermittent HTTP 502 responses that the GitHub jobs endpoint returns for large builds.
Expand Down
77 changes: 77 additions & 0 deletions Scenarios/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,83 @@ then, after merging, the result settings object will contain the following value

> _**Note**_: `overwriteSettings` isn't a setting on its own and it isn't available in the output of `ReadSetting` action, for example. It's merely used to control the settings merging mechanism and allow overwriting complex settings types. The value of `overwriteSettings` should only contain settings of types _array_ or _object_ and all the settings in `overwriteSettings` should be present with the new value.

## Important settings <a id="importantSettings"></a>

By default, AL-Go follows a standard settings hierarchy where settings from higher priority levels (closer to deployment) override settings from lower priority levels. However, you can mark specific settings as **important** to protect them from being overridden by lower priority settings using the `importantSettings` array.

When a setting is marked as important at a higher level in the hierarchy, it cannot be overridden by non-important values from lower priority levels. If a lower-priority source also marks the same setting as important, then the lower-priority value is allowed to override.

_Example_:
Say, `ALGoOrgSettings` (organization level) contains the following values:

```json
{
"importantSettings": ["country", "keyVaultName"],
"country": "de",
"keyVaultName": "OrgVault"
}
```

and `.AL-Go\settings.json` (project level, lower priority) contains the following values:

```json
{
"country": "us",
"keyVaultName": "ProjectVault"
}
```

then, after merging, the result settings object will contain the following values:

```json
{
"importantSettings": ["country", "keyVaultName"],
"country": "de",
"keyVaultName": "OrgVault"
}
```

The `country` and `keyVaultName` settings from the organization level are protected and cannot be overridden by the project level settings.

_Example with ConditionalSettings_:
Say, `ALGoOrgSettings` (organization level) contains conditional settings that set country based on buildMode:

```json
{
"ConditionalSettings": [
{
"buildModes": ["ValidateUS"],
"settings": {
"importantSettings": ["country"],
"country": "us"
}
}
]
}
```

and `.AL-Go\settings.json` (project level) contains:

```json
{
"country": "w1",
"buildModes": ["Default", "ValidateUS"]
}
```

When reading settings for buildMode `ValidateUS`, the conditional setting from the organization level will apply. The result will be:

```json
{
"country": "us",
"buildModes": ["Default", "ValidateUS"]
}
```

Even though the project specifies `country: "w1"`, the conditional setting from the organization level marked the country as important for the `ValidateUS` buildMode and the project value is not marked important, so the conditional value takes precedence.

> _**Note**_: `importantSettings` is an array of setting names that should be protected from non-important overrides from lower priority settings. If the same setting is marked as important at both levels, the source (lower-priority) value is allowed to override the destination value. Only top-level setting names can be marked as important; nested properties within complex objects cannot be individually marked as important. Array settings marked as important are still merged with lower-priority arrays. `overwriteSettings` can force replacement for important settings only when the source also marks that same setting as important.

<a id="customdelivery"></a>

## Custom Delivery
Expand Down
Loading