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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.2.0] - 2026-05-26

### Added

scripts/SPSUpdate.ps1:

- Added new `InitContentDB` value for the `Action` parameter that (re)generates the ContentDatabase inventory JSON file for the local farm. Intended to be run on the source farm before a farm upgrade (for example SharePoint Server 2019 → Subscription Edition).
- Added new `MountContentDatabase` configuration property in the JSON config file. When `true`, the master server iterates through the ContentDatabase inventory JSON file and mounts every database that is not already attached to the farm. Mounts are performed sequentially to avoid concurrent writes to the configuration database.
- Loader of the ContentDatabase inventory JSON file now also runs when `MountContentDatabase` is `true` (previously only when `UpgradeContentDatabase` was `true`).
- Added dedicated transcript log file naming for the `InitContentDB` action.

scripts/Modules/sps.util.psm1:

- Added `Mount-SPSContentDatabase` wrapper around `Mount-SPContentDatabase`. The wrapper validates the target web application, skips databases that are already attached, and accepts an optional `DatabaseServer` parameter.

tests/SPSUpdate.Tests.ps1:

- Added assertions for the new `InitContentDB` action and the `MountContentDatabase` flow.
- Added regression test that the ProductUpdate flow no longer calls `Test-SPSPendingReboot`.

tests/Modules/sps.util.Tests.ps1:

- Added tests for `Mount-SPSContentDatabase` (export, parameters, idempotency when the DB is already attached, mounting when missing, error when the web application is not found).

### Changed

scripts/SPSUpdate.ps1:

- Bumped `$SPSUpdateVersion` to `3.2.0`.
- Removed the blocking pending-reboot check from the `ProductUpdate` action. On production farms the Windows reboot markers (Component Based Servicing, `PendingFileRenameOperations`, etc.) commonly remain set after several reboots, which was causing legitimate updates to be aborted. The `Test-SPSPendingReboot` helper is kept available in `util.psm1` for ad-hoc usage.

### Documentation

- Updated `scripts/SPSUpdate_README.md`, `wiki/Configuration.md` and `wiki/Usage.md` to document the new `InitContentDB` action, the new `MountContentDatabase` JSON property and the removal of the blocking pending-reboot check from `ProductUpdate`.

## [3.1.1] - 2026-05-11

### Added
Expand Down
51 changes: 16 additions & 35 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,38 @@
# SPSUpdate - Release Notes

## [3.1.1] - 2026-05-11
## [3.2.0] - 2026-05-26

### Added

scripts/Modules/util.psm1:
scripts/SPSUpdate.ps1:

- Added `Test-SPSPendingReboot` to detect pending reboot state using multiple Windows markers (Windows Update, CBS, pending file rename, pending computer rename, and ConfigMgr).
- Added new `InitContentDB` value for the `Action` parameter that (re)generates the ContentDatabase inventory JSON file for the local farm. Intended to be run on the source farm before a farm upgrade (for example SharePoint Server 2019 → Subscription Edition).
- Added new `MountContentDatabase` configuration property in the JSON config file. When `true`, the master server iterates through the ContentDatabase inventory JSON file and mounts every database that is not already attached to the farm. Mounts are performed sequentially to avoid concurrent writes to the configuration database.
- Loader of the ContentDatabase inventory JSON file now also runs when `MountContentDatabase` is `true` (previously only when `UpgradeContentDatabase` was `true`).
- Added dedicated transcript log file naming for the `InitContentDB` action.

scripts/Modules/sps.util.psm1:

- Added missing `Clear-ComObject` helper used by `Get-SPSLocalVersionInfo` to safely release COM objects.

tests/Modules/util.Tests.ps1:

- Added tests for `Test-SPSPendingReboot` and module export validation for the new helper.
- Added regression tests for `Start-SPSScheduledTask` `TaskPath` parameter support and task path normalization behavior.

tests/Modules/sps.util.Tests.ps1:

- Added regression test ensuring `Start-SPSProductUpdate` no longer exposes `InstallAccount`.
- Added `Mount-SPSContentDatabase` wrapper around `Mount-SPContentDatabase`. The wrapper validates the target web application, skips databases that are already attached, and accepts an optional `DatabaseServer` parameter.

tests/SPSUpdate.Tests.ps1:

- Added assertions for consolidated reboot detection (`Test-SPSPendingReboot`) in ProductUpdate flow.
- Added regression assertion to prevent reintroduction of legacy ProductUpdate MOF cleanup code.

### Fixed
- Added assertions for the new `InitContentDB` action and the `MountContentDatabase` flow.
- Added regression test that the ProductUpdate flow no longer calls `Test-SPSPendingReboot`.

scripts/Modules/util.psm1:

- Fixed `Start-SPSScheduledTask` to support `TaskPath` and use that path when resolving and starting tasks.
- Normalized scheduled task paths to the expected `\<TaskPath>` format for `Get-ScheduledTask` and `Start-ScheduledTask`.
- Changed `Add-SPSScheduledTask` behavior to create or update existing tasks instead of skipping them.
tests/Modules/sps.util.Tests.ps1:

scripts/Modules/sps.util.psm1:
- Added tests for `Mount-SPSContentDatabase` (export, parameters, idempotency when the DB is already attached, mounting when missing, error when the web application is not found).

- Fixed runtime failure in `Get-SPSLocalVersionInfo` when `Clear-ComObject` was missing.
### Changed

scripts/SPSUpdate.ps1:

- Updated ProductUpdate to run update binaries without `InstallAccount`.
- Replaced single reboot check with consolidated reboot detection and detailed marker reporting.
- Removed legacy ProductUpdate `finally` block used for old DSC MOF cleanup.
- Enabled script-level verbose forwarding (`-Verbose`) to downstream module cmdlets.
- Added transcript file path output (`Transcript log file: ...`) at startup for easier log discovery.
- Updated installed SharePoint version output to print `FileVersion` explicitly.

### Changed
- Bumped `$SPSUpdateVersion` to `3.2.0`.
- Removed the blocking pending-reboot check from the `ProductUpdate` action. On production farms the Windows reboot markers (Component Based Servicing, `PendingFileRenameOperations`, etc.) commonly remain set after several reboots, which was causing legitimate updates to be aborted. The `Test-SPSPendingReboot` helper is kept available in `util.psm1` for ad-hoc usage.

Documentation:
### Documentation

- Updated ProductUpdate examples and guidance to remove `InstallAccount` requirement.
- Removed stale references to temporary MOF cleanup after ProductUpdate.
- Updated `scripts/SPSUpdate_README.md`, `wiki/Configuration.md` and `wiki/Usage.md` to document the new `InitContentDB` action, the new `MountContentDatabase` JSON property and the removal of the blocking pending-reboot check from `ProductUpdate`.

A full list of changes in each version can be found in the [change log](CHANGELOG.md)
1 change: 1 addition & 0 deletions scripts/Config/CONTOSO-PROD-CONTENT.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"SetupFileName": ["uber-subscription-kb5002651-fullfile-x64-glb.exe"],
"ShutdownServices": false
},
"MountContentDatabase": true,
"UpgradeContentDatabase": true,
"SideBySideToken": {
"Enable": true,
Expand Down
50 changes: 50 additions & 0 deletions scripts/Modules/sps.util.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,56 @@ function Update-SPSContentDatabase {

}

function Mount-SPSContentDatabase {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Name,

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

[Parameter()]
[System.String]
$DatabaseServer
)

# Skip mount if the content database is already attached to the farm
$getSPContentDb = Get-SPContentDatabase -Identity $Name -ErrorAction SilentlyContinue
if ($null -ne $getSPContentDb) {
Write-Output "SPContentDatabase $($Name) is already mounted - No action needed"
return
}

# Validate that the target web application exists
$getSPWebApp = Get-SPWebApplication -Identity $WebAppUrl -ErrorAction SilentlyContinue
if ($null -eq $getSPWebApp) {
$catchMessage = "SPWebApplication '$WebAppUrl' was not found - Cannot mount SPContentDatabase '$Name'"
Add-SPSUpdateEvent -Message $catchMessage -Source 'Mount-SPSContentDatabase' -EntryType 'Error'
throw $catchMessage
}

Write-Output "Mounting SPContentDatabase $($Name) on WebApplication $($WebAppUrl)"
$mountStarted = Get-Date
Write-Output "Started at $mountStarted - Please Wait ..."
if ($PSCmdlet.ShouldProcess($Name, "Mount SharePoint content database on $WebAppUrl")) {
$mountParams = @{
Name = $Name
WebApplication = $WebAppUrl
Confirm = $false
}
if (-not [string]::IsNullOrWhiteSpace($DatabaseServer)) {
$mountParams['DatabaseServer'] = $DatabaseServer
}
Mount-SPContentDatabase @mountParams -Verbose
}
$mountFinished = Get-Date
Write-Output "Mount for SPContentDatabase $($Name) is finished at $mountFinished"
}

function Set-SPSSideBySideToken {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param
Expand Down
110 changes: 83 additions & 27 deletions scripts/SPSUpdate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
Use the Action parameter equal to ProductUpdate if you want to run the ProductUpdate locally
PS D:\> E:\SCRIPT\SPSUpdate.ps1 -Action ProductUpdate -ConfigFile 'contoso-PROD.json'

Use the Action parameter equal to InitContentDB if you want to (re)generate the ContentDatabase JSON
inventory file used to prepare a farm upgrade (for example SharePoint 2019 to Subscription Edition).
This action runs Initialize-SPSContentDbJsonFile against the local farm and overwrites the existing
inventory file so that it always reflects the current state of the source farm.
PS D:\> E:\SCRIPT\SPSUpdate.ps1 -Action InitContentDB -ConfigFile 'contoso-PROD.json'

.PARAMETER Sequence
Need parameter Sequence for SPS Farm, example:
PS D:\> E:\SCRIPT\SPSUpdate.ps1 -ConfigFile 'contoso-PROD.json' -Sequence 1
Expand All @@ -33,12 +39,13 @@
SPSUpdate.ps1 -Action Install -InstallAccount (Get-Credential) -ConfigFile 'contoso-PROD.json'
SPSUpdate.ps1 -Action Uninstall -ConfigFile 'contoso-PROD.json'
SPSUpdate.ps1 -Action ProductUpdate -ConfigFile 'contoso-PROD.json'
SPSUpdate.ps1 -Action InitContentDB -ConfigFile 'contoso-PROD.json'

.NOTES
FileName: SPSUpdate.ps1
Author: Jean-Cyril DROUHIN
Date: May 11, 2026
Version: 3.1.1
Date: May 26, 2026
Version: 3.2.0

.LINK
https://spjc.fr/
Expand All @@ -53,7 +60,7 @@ param
$ConfigFile, # Path to the configuration file

[Parameter(Position = 1)]
[validateSet('Install', 'Uninstall', 'Default', 'ProductUpdate', IgnoreCase = $true)]
[validateSet('Install', 'Uninstall', 'Default', 'ProductUpdate', 'InitContentDB', IgnoreCase = $true)]
[System.String]
$Action = 'Default',

Expand Down Expand Up @@ -176,7 +183,7 @@ catch {
}

# Define variables
$SPSUpdateVersion = '3.1.1'
$SPSUpdateVersion = '3.2.0'
$getDateFormatted = Get-Date -Format yyyy-MM-dd_H-mm
$spsUpdateFileName = "$($Application)-$($Environment)_$($getDateFormatted)"
$spsUpdateDBsFile = "$($Application)-$($Environment)-$($spFarmName)-ContentDBs.json"
Expand All @@ -196,6 +203,9 @@ if ($PSBoundParameters.ContainsKey('Sequence')) {
elseif ($PSBoundParameters.ContainsKey('Action') -and $Action -eq 'ProductUpdate') {
$pathLogFile = Join-Path -Path $pathLogsFolder -ChildPath ("$($Application)-$($Environment)_ProductUpdate-$($env:COMPUTERNAME)_" + (Get-Date -Format yyyy-MM-dd_H-mm) + '.log')
}
elseif ($PSBoundParameters.ContainsKey('Action') -and $Action -eq 'InitContentDB') {
$pathLogFile = Join-Path -Path $pathLogsFolder -ChildPath ("$($Application)-$($Environment)_InitContentDB-$($env:COMPUTERNAME)_" + (Get-Date -Format yyyy-MM-dd_H-mm) + '.log')
}
else {
$pathLogFile = Join-Path -Path $pathLogsFolder -ChildPath ($spsUpdateFileName + '.log')
}
Expand Down Expand Up @@ -251,9 +261,9 @@ Exception: $_
Add-SPSUpdateEvent -Message $catchMessage -Source 'Get-SPSInstalledProductVersion' -EntryType 'Error'
}

# 2. Initialize or read ContentDatabase json file if UpgradeContentDatabase equal to true
# 2. Initialize or read ContentDatabase json file if UpgradeContentDatabase or MountContentDatabase equal to true
try {
if ($jsonEnvCfg.UpgradeContentDatabase) {
if ($jsonEnvCfg.UpgradeContentDatabase -or $jsonEnvCfg.MountContentDatabase) {
if (-Not (Test-Path -Path $pathConfigFolder)) {
# If the path does not exist, create the directory
New-Item -ItemType Directory -Path $pathConfigFolder
Expand Down Expand Up @@ -282,6 +292,39 @@ Exception: $_

# 3. Execute Action parameter
switch ($Action) {
'InitContentDB' {
# (Re)generate the ContentDatabase inventory JSON file for the local farm.
# Typically used on a source farm (for example SP2019) to prepare an upgrade
# to a target farm (for example Subscription Edition) where the file will
# later be consumed by the MountContentDatabase flow.
try {
if (-Not (Test-Path -Path $pathConfigFolder)) {
New-Item -ItemType Directory -Path $pathConfigFolder -Force | Out-Null
}
Write-Output "Initializing ContentDatabase json file for SPFARM: $($spFarmName)"
Write-Output "Target file: $spsUpdateDBsPath"
Initialize-SPSContentDbJsonFile -Path $spsUpdateDBsPath
if (Test-Path -Path $spsUpdateDBsPath) {
Write-Output "ContentDatabase json file generated successfully: $spsUpdateDBsPath"
}
else {
throw "ContentDatabase json file was not created: $spsUpdateDBsPath"
}
}
catch {
$catchMessage = @"
Failed to (re)Initialize ContentDatabase json file for SPFARM: $($spFarmName)
Exception: $_
"@
Write-Error -Message $catchMessage
Add-SPSUpdateEvent -Message $catchMessage -Source 'Initialize-SPSContentDbJsonFile' -EntryType 'Error'
if ($script:TranscriptStarted) {
Stop-Transcript | Out-Null
$script:TranscriptStarted = $false
}
exit
}
}
'Uninstall' {
# Remove scheduled Task for Update Full Script
try {
Expand Down Expand Up @@ -393,8 +436,8 @@ Exception: $_
Write-Error -Message $catchMessage
Add-SPSUpdateEvent -Message $catchMessage -Source 'Add-SPSScheduledTask' -EntryType 'Error'
}
# Add scheduled Task for Upgrade SPContentDatabase if UpgradeContentDatabase equal to true
if ($jsonEnvCfg.UpgradeContentDatabase) {
# Add scheduled Task for Upgrade SPContentDatabase if UpgradeContentDatabase or MountContentDatabase equal to true
if ($jsonEnvCfg.UpgradeContentDatabase -or $jsonEnvCfg.MountContentDatabase) {
# Get credential from Credential Manager
$credential = Get-StoredCredential -Target "$($jsonEnvCfg.StoredCredential)" -ErrorAction SilentlyContinue
if ($null -eq $credential) {
Expand Down Expand Up @@ -453,22 +496,10 @@ SharePoint Server: $($spTargetServer)
Setup File Path: $($fullSetupFilePath)
Shutdown Services: $($jsonEnvCfg.Binaries.ShutdownServices)
"@
Write-Output "Getting Reboot Status on server: $spTargetServer"
$rebootStatus = Test-SPSPendingReboot
if ($rebootStatus.IsPending) {
$rebootReasons = if ($null -ne $rebootStatus.Reasons -and $rebootStatus.Reasons.Count -gt 0) {
$rebootStatus.Reasons -join ', '
}
else {
'UnknownReason'
}
Write-Warning "A reboot is required on server: $($spTargetServer). Detected pending reboot markers: $rebootReasons. Please reboot the server and re-run the script."
if ($script:TranscriptStarted) {
Stop-Transcript | Out-Null
$script:TranscriptStarted = $false
}
exit
}
# NOTE: Pending reboot detection was removed because on production farms the
# Windows reboot markers (CBS, PendingFileRenameOperations, etc.) commonly
# remain set after several reboots, which caused the script to abort the
# ProductUpdate even when the system was actually in a healthy state.
# Unblock setup file if it is blocked
Unblock-File -Path $fullSetupFilePath -Verbose
Start-SPSProductUpdate -SetupFile $fullSetupFilePath -ShutdownServices $jsonEnvCfg.Binaries.ShutdownServices -Verbose
Expand Down Expand Up @@ -501,7 +532,30 @@ Exception: $_
4 { $dbs = $jsonDbCfg.SPContentDatabase4 }
}
foreach ($db in $dbs) {
Update-SPSContentDatabase -Name $db.Name
# Mount SPContentDatabase (typically used to attach databases coming from a
# previous farm version, for example SP2019 -> Subscription Edition migration).
# Mounts run inside each Sequence scheduled task, so the 4 sequences process
# their respective DB groups in parallel. Each database list is loaded from
# the ContentDatabase inventory JSON file produced by Initialize-SPSContentDbJsonFile.
# Mount and Upgrade are independent: a farm can be configured for Mount only,
# Upgrade only, or both (Mount then Upgrade for SP2019 -> SE migration).
if ($jsonEnvCfg.MountContentDatabase) {
try {
Mount-SPSContentDatabase -Name $db.Name -WebAppUrl $db.WebAppUrl -DatabaseServer $db.Server
}
catch {
$catchMessage = @"
Failed to Mount SPContentDatabase '$($db.Name)' on WebApplication '$($db.WebAppUrl)'
Target SPFarm: $($spFarmName)
Exception: $_
"@
Write-Error -Message $catchMessage
Add-SPSUpdateEvent -Message $catchMessage -Source 'Mount-SPSContentDatabase' -EntryType 'Error'
}
}
if ($jsonEnvCfg.UpgradeContentDatabase) {
Update-SPSContentDatabase -Name $db.Name
}
}
}
catch {
Expand Down Expand Up @@ -537,8 +591,10 @@ Exception: $_
exit
}
Write-Output "Update Script in progress | FULL Mode - Please Wait ..."
# Update SPContentDatabase
if ($jsonEnvCfg.UpgradeContentDatabase) {
# Mount and/or Upgrade SPContentDatabase via parallel scheduled tasks.
# The sequence tasks themselves decide what to do for each database based on
# the MountContentDatabase and UpgradeContentDatabase flags in the JSON config.
if ($jsonEnvCfg.UpgradeContentDatabase -or $jsonEnvCfg.MountContentDatabase) {
# Add scheduled Task for Upgrade SPContentDatabase in Parallel
foreach ($taskId in (1..4)) {
try {
Expand Down
Loading
Loading