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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
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.1] - 2026-06-11

### Changed

scripts/Modules/sps.util.psm1:

- `Initialize-SPSContentDbJsonFile` now distributes content databases across the four sequences using a **Longest Processing Time First (LPT)** algorithm based on `DiskSizeRequired`, instead of splitting them evenly by count (`floor(count / 4)`). Sequences are now balanced by total database size so parallel upgrade workloads finish closer to the same time.
- `Initialize-SPSContentDbJsonFile` now prints a distribution report to the transcript showing the count, total size (MB) and percentage for each of the four sequences.
- `Initialize-SPSContentDbJsonFile` now also writes a timestamped snapshot of the inventory (`<basename>_yyyy-MM-dd_HH-mm-ss.json`) next to the canonical file each time it runs. The canonical file (consumed by `SPSUpdate.ps1`) is still overwritten in place, while the dated snapshots accumulate in `scripts/Config/` so previous inventories can be reviewed or restored. Snapshot failures are logged via `Write-Verbose` and never block the canonical write.
- `Start-SPSProductUpdate` now invokes the SharePoint patch setup with `/passive` instead of `/quiet`. `/passive` still runs the patch without user interaction but displays a progress UI, which gives administrators visibility on the installation progress when the script is run interactively. Behavior of the returned exit code and the post-install service-restoration logic is unchanged.

scripts/SPSUpdate.ps1:

- Bumped `$SPSUpdateVersion` to `3.2.1`.

### Fixed

scripts/Modules/sps.util.psm1:

- Fixed `Initialize-SPSContentDbJsonFile` edge case where fewer than 4 content databases caused `groupSize` to evaluate to `0`, dumping every database into `SPContentDatabase4` and leaving `SPContentDatabase1..3` empty.

### Tests

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

- Added Pester tests for `Initialize-SPSContentDbJsonFile` covering: no file written when `Get-SPContentDatabase` returns `$null`, fair distribution of fewer than 4 databases (regression for the `floor(count / 4)` bug), LPT balancing by size (the largest database lands alone in its sequence), and the `SPContentDatabase1..4` JSON property contract consumed by `SPSUpdate.ps1`.

## [3.2.0] - 2026-05-26

### Added
Expand Down
36 changes: 14 additions & 22 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
# SPSUpdate - Release Notes

## [3.2.0] - 2026-05-26
## [3.2.1] - 2026-06-11

### 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.
### Changed

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.
- `Initialize-SPSContentDbJsonFile` now distributes content databases across the four sequences using a **Longest Processing Time First (LPT)** algorithm based on `DiskSizeRequired`, instead of splitting them evenly by count (`floor(count / 4)`). Sequences are now balanced by total database size so parallel upgrade workloads finish closer to the same time.
- `Initialize-SPSContentDbJsonFile` now prints a distribution report to the transcript showing the count, total size (MB) and percentage for each of the four sequences.
- `Initialize-SPSContentDbJsonFile` now also writes a timestamped snapshot of the inventory (`<basename>_yyyy-MM-dd_HH-mm-ss.json`) next to the canonical file each time it runs. The canonical file (consumed by `SPSUpdate.ps1`) is still overwritten in place, while the dated snapshots accumulate in `scripts/Config/` so previous inventories can be reviewed or restored. Snapshot failures are logged via `Write-Verbose` and never block the canonical write.
- `Start-SPSProductUpdate` now invokes the SharePoint patch setup with `/passive` instead of `/quiet`. `/passive` still runs the patch without user interaction but displays a progress UI, which gives administrators visibility on the installation progress when the script is run interactively. Behavior of the returned exit code and the post-install service-restoration logic is unchanged.

tests/SPSUpdate.Tests.ps1:
scripts/SPSUpdate.ps1:

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

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

- 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).
scripts/Modules/sps.util.psm1:

### Changed
- Fixed `Initialize-SPSContentDbJsonFile` edge case where fewer than 4 content databases caused `groupSize` to evaluate to `0`, dumping every database into `SPContentDatabase4` and leaving `SPContentDatabase1..3` empty.

scripts/SPSUpdate.ps1:
### Tests

- 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
tests/Modules/sps.util.Tests.ps1:

- 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`.
- Added Pester tests for `Initialize-SPSContentDbJsonFile` covering: no file written when `Get-SPContentDatabase` returns `$null`, fair distribution of fewer than 4 databases (regression for the `floor(count / 4)` bug), LPT balancing by size (the largest database lands alone in its sequence), and the `SPContentDatabase1..4` JSON property contract consumed by `SPSUpdate.ps1`.

A full list of changes in each version can be found in the [change log](CHANGELOG.md)
105 changes: 70 additions & 35 deletions scripts/Modules/sps.util.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -418,41 +418,54 @@ function Initialize-SPSContentDbJsonFile {
$spAllDatabases = Get-SPContentDatabase -ErrorAction SilentlyContinue

if ($null -ne $spAllDatabases) {
#Calculate the number of databases in each group
$groupSize = [math]::Floor($spAllDatabases.Count / 4)
#Loop through each content database and assign to groups
for ($i = 0; $i -lt $spAllDatabases.Count; $i++) {
$spDatabase = $spAllDatabases[$i]
#Determine which group to add the database to
if ($i -lt $groupSize) {
[void]$tbSPContentDb1.Add([SPDbContent]@{
Name = $spDatabase.Name;
Server = $spDatabase.Server;
WebAppUrl = $spDatabase.WebApplication.Url;
})
}
elseif ($i -lt ($groupSize * 2)) {
[void]$tbSPContentDb2.Add([SPDbContent]@{
Name = $spDatabase.Name;
Server = $spDatabase.Server;
WebAppUrl = $spDatabase.WebApplication.Url;
})
}
elseif ($i -lt ($groupSize * 3)) {
[void]$tbSPContentDb3.Add([SPDbContent]@{
Name = $spDatabase.Name;
Server = $spDatabase.Server;
WebAppUrl = $spDatabase.WebApplication.Url;
})
# --- LPT (Longest Processing Time First) scheduling ---
# Balance databases across 4 sequences by total DiskSizeRequired
# rather than by count, so parallel upgrade workloads finish closer
# to the same time.

# 1. Sort databases by size descending
$spSortedDatabases = $spAllDatabases |
Sort-Object -Property DiskSizeRequired -Descending

# 2. Track cumulative load (bytes) per sequence (index 0 = Seq1 .. 3 = Seq4)
$sequenceLoad = @(0.0, 0.0, 0.0, 0.0)
$sequenceLists = @($tbSPContentDb1, $tbSPContentDb2, $tbSPContentDb3, $tbSPContentDb4)

# 3. Assign each database to the sequence with the lowest current load
foreach ($spDatabase in $spSortedDatabases) {
$minLoad = $sequenceLoad[0]
$minIndex = 0
for ($s = 1; $s -lt 4; $s++) {
if ($sequenceLoad[$s] -lt $minLoad) {
$minLoad = $sequenceLoad[$s]
$minIndex = $s
}
}
else {
[void]$tbSPContentDb4.Add([SPDbContent]@{
Name = $spDatabase.Name;
Server = $spDatabase.Server;
WebAppUrl = $spDatabase.WebApplication.Url;
})
[void]$sequenceLists[$minIndex].Add([SPDbContent]@{
Name = $spDatabase.Name;
Server = $spDatabase.Server;
WebAppUrl = $spDatabase.WebApplication.Url;
})
$sequenceLoad[$minIndex] += $spDatabase.DiskSizeRequired
}

# --- Distribution report (visible in transcript) ---
$totalBytes = ($sequenceLoad | Measure-Object -Sum).Sum
$totalMB = [math]::Round($totalBytes / 1MB, 0)
$dbCount = @($spSortedDatabases).Count
Write-Output '--- ContentDatabase Distribution Report ---'
Write-Output ("Total : {0} database(s) | {1:N0} MB" -f $dbCount, $totalMB)
for ($s = 0; $s -lt 4; $s++) {
$loadMB = [math]::Round($sequenceLoad[$s] / 1MB, 0)
$pct = if ($totalBytes -gt 0) {
[math]::Round($sequenceLoad[$s] / $totalBytes * 100, 1)
}
else { 0 }
Write-Output (" Sequence {0} : {1,3} database(s) | {2,7:N0} MB | {3,5:N1}%" `
-f ($s + 1), $sequenceLists[$s].Count, $loadMB, $pct)
}
Write-Output '-------------------------------------------'

#Add each array to jsonObject
$jsonObject | Add-Member -MemberType NoteProperty `
-Name 'SPContentDatabase1' `
Expand All @@ -470,8 +483,30 @@ function Initialize-SPSContentDbJsonFile {
-Name 'SPContentDatabase4' `
-Value $tbSPContentDb4

#Convert jsonObject to JSON and save to a file
$jsonObject | ConvertTo-Json | Set-Content -Path $Path -Force
# Serialize once and write both the canonical file (consumed by SPSUpdate.ps1)
# and a timestamped snapshot in the same folder so previous inventories are
# retained for troubleshooting and rollback.
$jsonPayload = $jsonObject | ConvertTo-Json
$jsonPayload | Set-Content -Path $Path -Force

try {
$snapshotDir = [System.IO.Path]::GetDirectoryName($Path)
$snapshotBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Path)
$snapshotExtension = [System.IO.Path]::GetExtension($Path)
$snapshotTimestamp = Get-Date -Format 'yyyy-MM-dd_HH-mm-ss'
$snapshotFileName = '{0}_{1}{2}' -f $snapshotBaseName, $snapshotTimestamp, $snapshotExtension
$snapshotPath = if ([string]::IsNullOrEmpty($snapshotDir)) {
$snapshotFileName
}
else {
Join-Path -Path $snapshotDir -ChildPath $snapshotFileName
}
$jsonPayload | Set-Content -Path $snapshotPath -Force
Write-Output "ContentDatabase inventory snapshot saved to: $snapshotPath"
}
catch {
Write-Verbose -Message "Failed to write ContentDatabase inventory snapshot: $($_.Exception.Message)"
}
}
}

Expand Down Expand Up @@ -682,7 +717,7 @@ Setup file is blocked! Please use 'Unblock-File -Path $SetupFile' to unblock the

}

$setupInstall = Start-Process -FilePath $SetupFile -ArgumentList '/quiet /passive' -Wait -PassThru
$setupInstall = Start-Process -FilePath $SetupFile -ArgumentList '/passive' -Wait -PassThru
# Error codes: https://aka.ms/installerrorcodes
switch ($setupInstall.ExitCode) {
0 {
Expand Down
6 changes: 3 additions & 3 deletions scripts/SPSUpdate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
.NOTES
FileName: SPSUpdate.ps1
Author: Jean-Cyril DROUHIN
Date: May 26, 2026
Version: 3.2.0
Date: June 11, 2026
Version: 3.2.1

.LINK
https://spjc.fr/
Expand Down Expand Up @@ -183,7 +183,7 @@ catch {
}

# Define variables
$SPSUpdateVersion = '3.2.0'
$SPSUpdateVersion = '3.2.1'
$getDateFormatted = Get-Date -Format yyyy-MM-dd_H-mm
$spsUpdateFileName = "$($Application)-$($Environment)_$($getDateFormatted)"
$spsUpdateDBsFile = "$($Application)-$($Environment)-$($spFarmName)-ContentDBs.json"
Expand Down
Loading
Loading