Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- All Boolean configuration switches now accept `$true`/`$false` **and** `1`/`0`, and reject every other value with a clear error naming the property. Previously the `Reboot`, `Remoting` and `Execution` switches (`Reboot.Enable`, `Reboot.Force`, `Remoting.AllowFallback`, `Execution.InteractiveSequences`) enforced a strict `[bool]` type and threw on `1`, while the older flags (`Binaries.ProductUpdate`, `Binaries.ShutdownServices`, `MountContentDatabase`, `UpgradeContentDatabase`, `SideBySideToken.Enable`) silently coerced any truthy value (so `1` worked but so did a typo like `'false'`, which evaluates to `$true`). A shared `ConvertTo-SPSConfigBoolean` helper now validates and normalizes every switch consistently. ([#76](https://github.com/luigilink/SPSUpdate/issues/76))

## [5.2.0] - 2026-10-07

### Changed
Expand Down
18 changes: 9 additions & 9 deletions src/Config/CONTOSO-PROD.example.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
# --- Binaries (REQUIRED block; used by -Action ProductUpdate) ---------------------
Binaries = @{
# ProductUpdate : allow the binary installation step.
# Possible values : $true | $false. Default if omitted: $true
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $true

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 08f60cb. wiki/Configuration.md now carries one shared Boolean switches rule listing all nine switches and stating they accept $true/$false or 1/0, with anything else rejected. The example psd1 comments were already updated to match.

ProductUpdate = $true

# SetupFullPath : folder (local to each server) that holds the update binaries.
Expand All @@ -80,7 +80,7 @@

# ShutdownServices : stop Search/Timer/IIS services during install to speed it up
# (they are restored to their prior state afterwards).
# Possible values : $true | $false. Default if omitted: $true
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $true
ShutdownServices = $true

# Schedule : OPTIONAL window restricting WHEN the binary install may run. Omit to
Expand Down Expand Up @@ -108,12 +108,12 @@
# completion is not recorded until the grant is added.
Reboot = @{
# Enable : allow SPSUpdate to reboot a server automatically after a CU install.
# Possible values : $true | $false. Default if omitted: $false
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $false
Enable = $false

# Force : reboot even when the installer did not request one (exit code 0). Leave
# $false to reboot strictly when a reboot was required (exit code 17022).
# Possible values : $true | $false. Default if omitted: $false
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $false
Force = $false

# Schedule : OPTIONAL window restricting WHEN a reboot may happen (same shape as
Expand All @@ -129,17 +129,17 @@
# MountContentDatabase : mount the databases listed in the generated ContentDB
# inventory (typically for a SP2019 -> Subscription Edition migration). When either
# this or UpgradeContentDatabase is $true, the inventory JSON is (re)built on run.
# Possible values : $true | $false. Default if omitted: $false
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $false
MountContentDatabase = $false

# UpgradeContentDatabase : run Upgrade-SPContentDatabase on databases that NeedsUpgrade.
# Possible values : $true | $false. Default if omitted: $true
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $true
UpgradeContentDatabase = $true

# --- Side-by-side patching (OPTIONAL block) --------------------------------------
SideBySideToken = @{
# Enable : turn EnableSideBySide on the web applications and copy side-by-side files.
# Possible values : $true | $false. Default if omitted: $false
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $false
Enable = $false

# BuildVersion : the side-by-side token build. Leave empty to skip token config.
Expand All @@ -155,7 +155,7 @@
# delegate the credential, so steps that need a second hop (config DB on SQL, or a
# binaries file share) may fail unless Kerberos delegation (KCD/RBCD) is configured.
# Leave off for secure, strict environments.
# Possible values : $true | $false. Default if omitted: $false
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $false

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 08f60cb. Test-SPSUpdateReadiness.ps1 now normalizes both Reboot.Enable and Remoting.AllowFallback through ConvertTo-SPSConfigBoolean, so the pre-flight check shares the runtime contract: 1/0 are accepted (and AllowFallback = 1 enables the fallback probes), while an invalid value is reported as a readiness FAIL with the helper's message.

AllowFallback = $false
}

Expand All @@ -165,7 +165,7 @@
# parallel content-database sequences in visible PowerShell windows instead of hidden
# scheduled tasks, so you can watch per-sequence progress live. Unattended/scheduled
# runs always use scheduled tasks regardless of this setting.
# Possible values : $true | $false. Default if omitted: $false
# Possible values : $true | $false (1 | 0 also accepted). Default if omitted: $false
InteractiveSequences = $false
}
}
56 changes: 56 additions & 0 deletions src/Modules/SPSUpdate.Common/Public/ConvertTo-SPSConfigBoolean.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
function ConvertTo-SPSConfigBoolean {
<#
.SYNOPSIS
Validates and normalizes a Boolean configuration value.

.DESCRIPTION
ConvertTo-SPSConfigBoolean gives every Boolean switch in the SPSUpdate
configuration the same, predictable contract. It accepts a real Boolean
($true / $false) or the integers 1 / 0, and returns the corresponding
[bool]. Any other value (an out-of-range integer, a string such as 'true'
or 'yes', $null, ...) throws a clear error naming the property, so a typo
in the psd1 fails fast instead of being silently coerced to $true.

Only genuine integer types are accepted for the 1/0 form; a string like
'1' is rejected on purpose, to avoid the PowerShell footgun where
[bool]'false' evaluates to $true.

.PARAMETER Value
The raw value read from the configuration for the property.

.PARAMETER PropertyName
The dotted property path (for example 'Reboot.Enable') used in the error
message so the operator knows exactly which key to fix.

.EXAMPLE
ConvertTo-SPSConfigBoolean -Value 1 -PropertyName 'Reboot.Enable'

.EXAMPLE
ConvertTo-SPSConfigBoolean -Value $false -PropertyName 'Remoting.AllowFallback'
#>
[CmdletBinding()]
[OutputType([System.Boolean])]
param
(
[Parameter(Mandatory = $true)]
[AllowNull()]
$Value,

[Parameter(Mandatory = $true)]
[System.String]
$PropertyName
)

if ($Value -is [bool]) {
return $Value
}

# Accept any integral type valued 0 or 1; the type guard keeps strings like '0'/'1'
# out (otherwise '0' -eq 0 would match via PowerShell string coercion).
$integralTypes = @([byte], [sbyte], [int16], [uint16], [int32], [uint32], [int64], [uint64])
if (($null -ne $Value) -and ($Value.GetType() -in $integralTypes) -and ($Value -eq 0 -or $Value -eq 1)) {
return [System.Boolean][int64]$Value
}

throw "Configuration property '$PropertyName' must be a Boolean (`$true/`$false) or 0/1, not '$Value'."
}
1 change: 1 addition & 0 deletions src/Modules/SPSUpdate.Common/SPSUpdate.Common.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
FunctionsToExport = @(
'Add-SPSScheduledTask'
'Add-SPSUpdateEvent'
'ConvertTo-SPSConfigBoolean'
'ConvertTo-SPSPatchStatusLabel'
'ConvertTo-SPSRoleLabel'
'Copy-SPSSideBySideFilesRemote'
Expand Down
31 changes: 23 additions & 8 deletions src/SPSUpdate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,29 @@ function Get-SPSUpdateConfiguration {
if (-not $config.Binaries.ContainsKey('ProductUpdate')) {
$config.Binaries.ProductUpdate = $true
}
else {
$config.Binaries.ProductUpdate = ConvertTo-SPSConfigBoolean -Value $config.Binaries.ProductUpdate -PropertyName 'Binaries.ProductUpdate'
}
if (-not $config.Binaries.ContainsKey('ShutdownServices')) {
$config.Binaries.ShutdownServices = $true
}
else {
$config.Binaries.ShutdownServices = ConvertTo-SPSConfigBoolean -Value $config.Binaries.ShutdownServices -PropertyName 'Binaries.ShutdownServices'
}

# Apply content-database defaults
if (-not $config.ContainsKey('MountContentDatabase')) {
$config.MountContentDatabase = $false
}
else {
$config.MountContentDatabase = ConvertTo-SPSConfigBoolean -Value $config.MountContentDatabase -PropertyName 'MountContentDatabase'
}
if (-not $config.ContainsKey('UpgradeContentDatabase')) {
$config.UpgradeContentDatabase = $true
}
else {
$config.UpgradeContentDatabase = ConvertTo-SPSConfigBoolean -Value $config.UpgradeContentDatabase -PropertyName 'UpgradeContentDatabase'
}

# Normalize the SideBySideToken block and apply defaults
if (-not $config.ContainsKey('SideBySideToken') -or $null -eq $config.SideBySideToken) {
Expand All @@ -169,6 +181,9 @@ function Get-SPSUpdateConfiguration {
if (-not $config.SideBySideToken.ContainsKey('Enable')) {
$config.SideBySideToken.Enable = $false
}
else {
$config.SideBySideToken.Enable = ConvertTo-SPSConfigBoolean -Value $config.SideBySideToken.Enable -PropertyName 'SideBySideToken.Enable'
}
if (-not $config.SideBySideToken.ContainsKey('BuildVersion')) {
$config.SideBySideToken.BuildVersion = ''
}
Expand All @@ -182,14 +197,14 @@ function Get-SPSUpdateConfiguration {
if (-not $config.Reboot.ContainsKey('Enable')) {
$config.Reboot.Enable = $false
}
elseif ($config.Reboot.Enable -isnot [bool]) {
throw "Configuration property 'Reboot.Enable' must be a Boolean (`$true or `$false), not '$($config.Reboot.Enable)'."
else {
$config.Reboot.Enable = ConvertTo-SPSConfigBoolean -Value $config.Reboot.Enable -PropertyName 'Reboot.Enable'
}
if (-not $config.Reboot.ContainsKey('Force')) {
$config.Reboot.Force = $false
}
elseif ($config.Reboot.Force -isnot [bool]) {
throw "Configuration property 'Reboot.Force' must be a Boolean (`$true or `$false), not '$($config.Reboot.Force)'."
else {
$config.Reboot.Force = ConvertTo-SPSConfigBoolean -Value $config.Reboot.Force -PropertyName 'Reboot.Force'
}

# Remote cmdlets use CredSSP by default; Remoting.AllowFallback (opt-in, off) lets
Expand All @@ -200,8 +215,8 @@ function Get-SPSUpdateConfiguration {
if (-not $config.Remoting.ContainsKey('AllowFallback')) {
$config.Remoting.AllowFallback = $false
}
elseif ($config.Remoting.AllowFallback -isnot [bool]) {
throw "Configuration property 'Remoting.AllowFallback' must be a Boolean (`$true or `$false), not '$($config.Remoting.AllowFallback)'."
else {
$config.Remoting.AllowFallback = ConvertTo-SPSConfigBoolean -Value $config.Remoting.AllowFallback -PropertyName 'Remoting.AllowFallback'
}

# Execution.InteractiveSequences (opt-in, off): in an attended run, launch the parallel
Expand All @@ -212,8 +227,8 @@ function Get-SPSUpdateConfiguration {
if (-not $config.Execution.ContainsKey('InteractiveSequences')) {
$config.Execution.InteractiveSequences = $false
}
elseif ($config.Execution.InteractiveSequences -isnot [bool]) {
throw "Configuration property 'Execution.InteractiveSequences' must be a Boolean (`$true or `$false), not '$($config.Execution.InteractiveSequences)'."
else {
$config.Execution.InteractiveSequences = ConvertTo-SPSConfigBoolean -Value $config.Execution.InteractiveSequences -PropertyName 'Execution.InteractiveSequences'
}

# StatusStorePath is optional; empty string means "use the local Results\status folder".
Expand Down
19 changes: 13 additions & 6 deletions src/Test-SPSUpdateReadiness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,14 @@ else {
# Reboot enabled + UNC store: the SYSTEM boot task writes as the computer account, which we
# cannot probe here - surface an explicit WARN about the required machine-account grant.
$rebootEnabled = $false
try { $rebootEnabled = [bool]$cfg.Reboot.Enable } catch { $rebootEnabled = $false }
if ($null -ne $cfg -and $cfg.Contains('Reboot') -and $cfg.Reboot -and $cfg.Reboot.Contains('Enable')) {
try {
$rebootEnabled = ConvertTo-SPSConfigBoolean -Value $cfg.Reboot.Enable -PropertyName 'Reboot.Enable'
}
catch {
Add-CheckResult -Section 'Dashboard' -Name 'Reboot.Enable' -Status 'FAIL' -Detail $_.Exception.Message
}
}
if ($rebootEnabled) {
$storeForReboot = ''
try { $storeForReboot = [string]$cfg.StatusStorePath } catch { $storeForReboot = '' }
Expand Down Expand Up @@ -415,14 +422,14 @@ elseif ($null -ne $cfg -and $cfg.Contains('Domain') -and $cfg.Domain) {
}

# Remoting.AllowFallback is read raw here; default to the secure $false and reject a
# non-Boolean value (which Get-SPSUpdateConfiguration also rejects at run time).
# non-Boolean value the same way Get-SPSUpdateConfiguration does at run time.
$allowFallback = $false
if ($null -ne $cfg -and $cfg.Contains('Remoting') -and $cfg.Remoting -and $cfg.Remoting.Contains('AllowFallback')) {
if ($cfg.Remoting.AllowFallback -is [bool]) {
$allowFallback = $cfg.Remoting.AllowFallback
try {
$allowFallback = ConvertTo-SPSConfigBoolean -Value $cfg.Remoting.AllowFallback -PropertyName 'Remoting.AllowFallback'
}
else {
Add-CheckResult -Section 'Network' -Name 'Remoting.AllowFallback' -Status 'FAIL' -Detail "Must be a Boolean (`$true/`$false); the run will reject '$($cfg.Remoting.AllowFallback)'"
catch {
Add-CheckResult -Section 'Network' -Name 'Remoting.AllowFallback' -Status 'FAIL' -Detail $_.Exception.Message
}
}

Expand Down
81 changes: 81 additions & 0 deletions tests/ConvertTo-SPSConfigBoolean.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Behavioural tests for ConvertTo-SPSConfigBoolean: the shared validator/normalizer
# that gives every Boolean configuration switch the same $true/$false/1/0 contract.

BeforeAll {
$repoRoot = Split-Path -Path $PSScriptRoot -Parent
$modulePath = Join-Path -Path $repoRoot -ChildPath 'src/Modules/SPSUpdate.Common/SPSUpdate.Common.psd1'
Import-Module -Name $modulePath -Force
}

AfterAll {
Remove-Module -Name SPSUpdate.Common -Force -ErrorAction SilentlyContinue
}

Describe 'ConvertTo-SPSConfigBoolean' {
Context 'Accepted values' {
It 'returns $true for a $true Boolean' {
ConvertTo-SPSConfigBoolean -Value $true -PropertyName 'Reboot.Enable' | Should -BeTrue
}

It 'returns $false for a $false Boolean' {
ConvertTo-SPSConfigBoolean -Value $false -PropertyName 'Reboot.Enable' | Should -BeFalse
}

It 'normalizes the integer 1 to $true' {
$result = ConvertTo-SPSConfigBoolean -Value 1 -PropertyName 'Reboot.Enable'
$result | Should -BeOfType [System.Boolean]
$result | Should -BeTrue
}

It 'normalizes the integer 0 to $false' {
$result = ConvertTo-SPSConfigBoolean -Value 0 -PropertyName 'Reboot.Enable'
$result | Should -BeOfType [System.Boolean]
$result | Should -BeFalse
}

It 'accepts other integral types valued 1 (byte, uint16, uint64)' {
ConvertTo-SPSConfigBoolean -Value ([byte]1) -PropertyName 'Reboot.Enable' | Should -BeTrue
ConvertTo-SPSConfigBoolean -Value ([uint16]1) -PropertyName 'Reboot.Enable' | Should -BeTrue
ConvertTo-SPSConfigBoolean -Value ([uint64]1) -PropertyName 'Reboot.Enable' | Should -BeTrue
}

It 'accepts other integral types valued 0 (sbyte, uint32)' {
ConvertTo-SPSConfigBoolean -Value ([sbyte]0) -PropertyName 'Reboot.Enable' | Should -BeFalse
ConvertTo-SPSConfigBoolean -Value ([uint32]0) -PropertyName 'Reboot.Enable' | Should -BeFalse
}
}

Context 'Rejected values' {
It 'throws for an out-of-range integer' {
{ ConvertTo-SPSConfigBoolean -Value 2 -PropertyName 'Reboot.Enable' } |
Should -Throw -ExpectedMessage "*'Reboot.Enable'*"
}

It 'rejects the string "1" (no string coercion)' {
{ ConvertTo-SPSConfigBoolean -Value '1' -PropertyName 'Reboot.Force' } |
Should -Throw -ExpectedMessage "*'Reboot.Force'*"
}

It 'rejects the string "0" (no string coercion)' {
{ ConvertTo-SPSConfigBoolean -Value '0' -PropertyName 'Reboot.Force' } | Should -Throw
}

It 'rejects an arbitrary truthy string such as "false"' {
{ ConvertTo-SPSConfigBoolean -Value 'false' -PropertyName 'Remoting.AllowFallback' } |
Should -Throw -ExpectedMessage "*'Remoting.AllowFallback'*"
}

It 'rejects "yes"' {
{ ConvertTo-SPSConfigBoolean -Value 'yes' -PropertyName 'Execution.InteractiveSequences' } | Should -Throw
}

It 'rejects $null' {
{ ConvertTo-SPSConfigBoolean -Value $null -PropertyName 'Reboot.Enable' } | Should -Throw
}

It 'names the offending property in the error message' {
{ ConvertTo-SPSConfigBoolean -Value 7 -PropertyName 'Binaries.ProductUpdate' } |
Should -Throw -ExpectedMessage "*'Binaries.ProductUpdate'*"
}
}
}
1 change: 1 addition & 0 deletions tests/SPSUpdate.Common.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Describe 'SPSUpdate.Common module' {
$expected = @(
'Add-SPSScheduledTask'
'Add-SPSUpdateEvent'
'ConvertTo-SPSConfigBoolean'
'ConvertTo-SPSPatchStatusLabel'
'ConvertTo-SPSRoleLabel'
'Copy-SPSSideBySideFilesRemote'
Expand Down
7 changes: 7 additions & 0 deletions wiki/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ If an optional key is omitted, SPSUpdate applies a safe default:
`Binaries.SetupFullPath` and `Binaries.SetupFileName` are required as soon as
`ProductUpdate` is `$true`.

> **Boolean switches.** Every Boolean configuration switch — `Binaries.ProductUpdate`,
> `Binaries.ShutdownServices`, `UpgradeContentDatabase`, `MountContentDatabase`,
> `SideBySideToken.Enable`, `Reboot.Enable`, `Reboot.Force`, `Remoting.AllowFallback`
> and `Execution.InteractiveSequences` — accepts `$true` / `$false` **or** the integers
> `1` / `0` (`1` = `$true`, `0` = `$false`). Any other value (an out-of-range number, or a
> string such as `'yes'` or `'false'`) is rejected with a clear error naming the property.

> The previous JSON `StoredCredential` key has been renamed to `CredentialKey`, and the
> configuration format moved from JSON to psd1. Runtime/output files (the ContentDB
> inventory and logs) stay JSON by design.
Expand Down
Loading