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
21 changes: 21 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4

# Windows PowerShell 5.1 needs a BOM to read non-ASCII correctly, and CRLF is the
# native line ending on the target SharePoint servers.
[*.{ps1,psm1,psd1}]
charset = utf-8-bom
end_of_line = crlf

[*.{yml,yaml}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false
28 changes: 28 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SPSUpdate line-ending and encoding policy
#
# PowerShell files run under Windows PowerShell 5.1 on SharePoint servers. Without a
# BOM, Windows PowerShell 5.1 reads .ps1/.psm1/.psd1 as ANSI (the system code page),
# which corrupts any non-ASCII character. We therefore store all PowerShell files as
# UTF-8 with BOM (the BOM is part of the committed content) and check them out with
# CRLF line endings (native on the target servers).
#
# YAML, Markdown and other text files must NOT carry a BOM (it breaks some YAML
# parsers and is unwanted on GitHub-rendered files); they use LF.

* text=auto eol=lf

# PowerShell -- committed with a UTF-8 BOM, checked out with CRLF
*.ps1 text eol=crlf
*.psm1 text eol=crlf
*.psd1 text eol=crlf

# Workflows / docs / config -- no BOM, LF
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf
*.json text eol=lf

# Binary artifacts (release ZIP, screenshots)
*.dll binary
*.png binary
*.zip binary
38 changes: 23 additions & 15 deletions .github/workflows/pester.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This is the SPSUpdate CI Pester workflow to run tests on pull requests
# SPSUpdate CI Pester workflow to run tests and code analysis on pull requests.

name: SPSUpdate CI Pester Tests

Expand All @@ -7,14 +7,23 @@ on:
branches:
- main
paths:
- 'scripts/**'
- 'src/**'
- 'tests/**'
- 'PSScriptAnalyzerSettings.psd1'

# The "Publish Test Results" step creates a check run, which requires write
# access to checks. The default GITHUB_TOKEN is read-only on repositories
# created with the modern default, so grant the minimum needed here.
permissions:
contents: read
checks: write
pull-requests: write

jobs:
test:
name: Run Pester Tests
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -34,7 +43,7 @@ jobs:
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = './test-results.xml'
$config.Output.Verbosity = 'Detailed'

Invoke-Pester -Configuration $config

- name: Upload test results
Expand All @@ -47,6 +56,7 @@ jobs:
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action/composite@v2
if: always()
continue-on-error: true
with:
files: |
test-results.xml
Expand All @@ -55,10 +65,10 @@ jobs:
code-quality:
name: Code Quality Check
runs-on: windows-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Install PSScriptAnalyzer
shell: pwsh
Expand All @@ -68,18 +78,16 @@ jobs:
- name: Run PSScriptAnalyzer
shell: pwsh
run: |
# Exclude credentialmanager folder (third-party dependency)
$scriptFiles = Get-ChildItem -Path ./scripts -Include '*.ps1', '*.psm1' -Recurse |
Where-Object { $_.FullName -notlike '*credentialmanager*' }

$results = $scriptFiles | ForEach-Object {
Invoke-ScriptAnalyzer -Path $_.FullName -Severity Error,Warning
}

# Analyze our own code only: the entry script and the SPSUpdate.Common module.
$results = @()
$results += Invoke-ScriptAnalyzer -Path ./src/SPSUpdate.ps1 -Settings ./PSScriptAnalyzerSettings.psd1
$results += Invoke-ScriptAnalyzer -Path ./src/Modules/SPSUpdate.Common -Recurse -Settings ./PSScriptAnalyzerSettings.psd1

if ($results) {
$results | Format-Table -AutoSize
Write-Error "PSScriptAnalyzer found issues"
exit 1
} else {
}
else {
Write-Host "No issues found by PSScriptAnalyzer"
}
13 changes: 10 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# This is the SPSUpdate CI Release workflow to create release from tag push
# This is the SPSUpdate CI Release workflow to create a release from a tag push.
# Triggered by pushing a v* tag (e.g. v4.0.0). It packages the *contents* of the
# src/ folder into a ZIP (so the archive extracts straight to Config/, Modules/ and
# SPSUpdate.ps1, with no src/ wrapper) and publishes a GitHub Release using
# RELEASE-NOTES.md as the body.

name: SPSUpdate CI Release

Expand All @@ -8,6 +12,9 @@ on:
tags:
- "v*" # Push events to matching v*, i.e. v1.0, v20.15.10

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -17,10 +24,10 @@ jobs:
id: checkout_code
uses: actions/checkout@v4
# Create a ZIP file with project name and tag version
- name: Create ZIP file of scripts
- name: Create ZIP file of src contents
run: |
zip_name="SPSUpdate-${{ github.ref_name }}.zip"
zip -r $zip_name scripts/
( cd src && zip -r "../$zip_name" . )
shell: bash
# Create Release with tag version
- name: Create GitHub Release
Expand Down
19 changes: 18 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
**/.DS_Store
**/.DS_Store

# Test / analysis artifacts
test-results.xml
testResults.xml
coverage.xml

# Local secrets (DPAPI credential store) -- NEVER commit real secrets
**/secrets.psd1

# Real environment configs live in src/Config; only *.example.psd1 templates are tracked
src/Config/*.psd1
!src/Config/*.example.psd1

# Runtime output kept out of source control (logs and generated ContentDB inventory)
src/Logs/
**/*-ContentDBs.json
**/*-ContentDBs_*.json
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,38 @@
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).

## [4.0.0] - 2026-06-29

### Added

- New `SPSUpdate.Common` PowerShell module (`src/Modules/SPSUpdate.Common`) with a manifest-driven version, a dot-sourcing loader, and a one-file-per-function layout split into `Public/` and `Private/`.
- DPAPI credential store: `Get-SPSSecret` / `Set-SPSSecret` persist the `InstallAccount` as an encrypted SecureString in `Config\secrets.psd1`, replacing the Windows Credential Manager module. `-Action Install` writes the secret and `-Action Uninstall` removes it.
- Tolerant configuration loader: optional keys fall back to safe defaults (`Binaries.ProductUpdate`, `Binaries.ShutdownServices` and `UpgradeContentDatabase` default to `$true`; `MountContentDatabase` and `SideBySideToken.Enable` default to `$false`).
- Repository scaffolding aligned with the other SPS* projects: `.editorconfig`, `.gitattributes` (UTF-8 BOM + CRLF for PowerShell files), `PSScriptAnalyzerSettings.psd1`, and an expanded `.gitignore`.
- Wiki: new `Release-Process` page and `_Sidebar.md` navigation.
- Offline install guide (`src/SPSUpdate_README.md`) shipped inside the release ZIP, rewritten for the psd1 config, the DPAPI secret store and the `Modules\` layout (no DSC).

### Changed

- **BREAKING** — Project layout moved from `scripts/` to `src/`. The release ZIP now packages the *contents* of `src/`, so the archive extracts straight to `SPSUpdate.ps1`, `Config\` and `Modules\` (no wrapper folder).
- **BREAKING** — Environment configuration moved from JSON to a PowerShell data file (`*.psd1`), read with `Import-PowerShellDataFile`. A single documented template (`Config\CONTOSO-PROD.example.psd1`) lists every option with its possible values inline. Runtime/output files (the ContentDB inventory and logs) stay JSON by design.
- **BREAKING** — The config key `StoredCredential` is renamed to `CredentialKey` and now points at an entry in `Config\secrets.psd1` instead of a Windows Credential Manager target.
- `SPSUpdate.ps1` now imports `SPSUpdate.Common`, derives its version banner from the module manifest, and reads/writes the InstallAccount credential through `Get-SPSSecret`/`Set-SPSSecret`.
- `Add-SPSUpdateEvent` is now self-contained (version/user resolved from the module) and re-points a mis-mapped event source to the `SPSUpdate` log instead of returning silently.
- CI: `release.yml` zips the contents of `src/`; `pester.yml` triggers on `src/**`, `tests/**` and `PSScriptAnalyzerSettings.psd1`, and runs PSScriptAnalyzer against `src/SPSUpdate.ps1` and the `SPSUpdate.Common` module.
- `README.md` trimmed to a short overview with quick links; the wiki (`Home`, `Getting-Started`, `Configuration`, `Usage`) rewritten for the new layout, psd1 config and DPAPI secret store.

### Fixed

- `Invoke-SPSCommand` now fails fast when the CredSSP `New-PSSession` cannot be established (`-ErrorAction Stop` + a clear `throw`) instead of falling back to running the SharePoint scriptblock **locally** on the master server with no session — which previously made PSConfig and side-by-side operations silently target the wrong server. Added an `OpenTimeout` and clearer remote-failure error messages, aligned with the SPSWeather hardening.

- **BREAKING** — The bundled third-party `credentialmanager` module (DLLs + manifest) and all `Get-/New-/Remove-StoredCredential` usage.
- The legacy `scripts/` tree, including the flat `util.psm1` / `sps.util.psm1` helpers and the per-farm JSON configs.

### Tests

- Replaced the scripts/JSON-oriented Pester tests with a cross-platform suite (`tests/SPSUpdate.Common.Tests.ps1`, `tests/Configuration.Tests.ps1`) covering the manifest, the exact public export set, hidden private helpers, file conventions (one function per file, UTF-8 BOM, parse), parameter contracts, the `Add-SPSUpdateEvent` self-heal, a DPAPI secret round-trip (Windows-only), and the example psd1 config/secret contract.

## [3.2.1] - 2026-06-11

### Changed
Expand Down
14 changes: 14 additions & 0 deletions PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@{
# PSScriptAnalyzer settings for SPSUpdate.
# Run locally with:
# Invoke-ScriptAnalyzer -Path ./src -Recurse -Settings ./PSScriptAnalyzerSettings.psd1
Severity = @('Error', 'Warning')

# PSUseSingularNouns is disabled defensively: a couple of helpers intentionally use a
# plural noun because they act on a collection (e.g. Copy-SPSSideBySideFilesRemote
# operates on the side-by-side file set), mirroring built-in cmdlets such as
# Get-ChildItem.
ExcludeRules = @(
'PSUseSingularNouns'
)
}
39 changes: 11 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,18 @@
![Latest release date](https://img.shields.io/github/release-date/luigilink/SPSUpdate.svg?style=flat)
![Total downloads](https://img.shields.io/github/downloads/luigilink/SPSUpdate/total.svg?style=flat)
![Issues opened](https://img.shields.io/github/issues/luigilink/SPSUpdate.svg?style=flat)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](code_of_conduct.md)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)

## Description
**SPSUpdate** is a PowerShell tool that installs SharePoint Server cumulative updates and runs the post-setup Configuration Wizard (PSConfig) across a farm. It installs the binaries, mounts/upgrades content databases in parallel, runs PSConfig on local and remote servers over CredSSP remoting, and configures the side-by-side token for zero-downtime patching.

SPSUpdate is a PowerShell script tool designed to install cumulative updates in your SharePoint environment.
Compatible with SharePoint Server **2016**, **2019**, and **Subscription Edition**. Requires PowerShell 5.1 or later — no DSC module needed.

[Download the latest release, Click here!](https://github.com/luigilink/SPSUpdate/releases/latest)
## Quick links

## Requirements

### PowerShell 5.1 or later

SPSUpdate no longer depends on a DSC module.
Use PowerShell 5.1 or later on each SharePoint server where you run the script.
The script relies on standard PowerShell remoting, scheduled tasks, Credential Manager integration, and native SharePoint update binaries.
This is discussed further on the [SPSUpdate Wiki Getting-Started](https://github.com/luigilink/SPSUpdate/wiki/Getting-Started)

## CredSSP

Impersonation is handled using the `Invoke-Command` cmdlet in PowerShell, together with the creation of a remote session via `New-PSSession`. In the SPSUpdate script, we authenticate as the InstallAccount and specify CredSSP as the authentication mechanism. This is explained further in the [SPSUpdate Wiki Getting-Started](https://github.com/luigilink/SPSUpdate/wiki/Getting-Started)

## ProductUpdate

The `ProductUpdate` action runs the SharePoint update binaries directly on the local server. You only need the update files accessible on that server.

## Documentation

For detailed usage, configuration, and getting started information, visit the [SPSUpdate Wiki](https://github.com/luigilink/SPSUpdate/wiki)

## Changelog

A full list of changes in each version can be found in the [change log](CHANGELOG.md)
- 📦 [Latest release](https://github.com/luigilink/SPSUpdate/releases/latest)
- 📖 [Documentation (Wiki)](https://github.com/luigilink/SPSUpdate/wiki)
- 🚀 [Getting Started](https://github.com/luigilink/SPSUpdate/wiki/Getting-Started)
- ⚙️ [Configuration reference](https://github.com/luigilink/SPSUpdate/wiki/Configuration)
- 📖 [Usage reference](https://github.com/luigilink/SPSUpdate/wiki/Usage)
- 📝 [Changelog](CHANGELOG.md)
- 🤝 [Contributing](.github/CONTRIBUTING.md)
42 changes: 28 additions & 14 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,44 @@
# SPSUpdate - Release Notes

## [3.2.1] - 2026-06-11
## [4.0.0] - 2026-06-29

### Changed
This is a major modernization release that aligns SPSUpdate with the SPSWeather and
SPSUserSync projects. It is a **breaking** release: the package layout, the configuration
format and the credential storage all change. Review the migration notes below before
upgrading an existing deployment.

scripts/Modules/sps.util.psm1:
### Added

- `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.
- New `SPSUpdate.Common` PowerShell module (`src/Modules/SPSUpdate.Common`) with a manifest-driven version, a dot-sourcing loader, and a one-file-per-function layout split into `Public/` and `Private/`.
- DPAPI credential store: `Get-SPSSecret` / `Set-SPSSecret` persist the `InstallAccount` as an encrypted SecureString in `Config\secrets.psd1`, replacing the Windows Credential Manager module. `-Action Install` writes the secret and `-Action Uninstall` removes it.
- Tolerant configuration loader: optional keys fall back to safe defaults (`Binaries.ProductUpdate`, `Binaries.ShutdownServices` and `UpgradeContentDatabase` default to `$true`; `MountContentDatabase` and `SideBySideToken.Enable` default to `$false`).
- Repository scaffolding aligned with the other SPS* projects: `.editorconfig`, `.gitattributes` (UTF-8 BOM + CRLF for PowerShell files), `PSScriptAnalyzerSettings.psd1`, and an expanded `.gitignore`.
- Wiki: new `Release-Process` page and `_Sidebar.md` navigation.
- Offline install guide (`src/SPSUpdate_README.md`) shipped inside the release ZIP, rewritten for the psd1 config, the DPAPI secret store and the `Modules\` layout (no DSC).

scripts/SPSUpdate.ps1:
### Changed

- Bumped `$SPSUpdateVersion` to `3.2.1`.
- **BREAKING** — Project layout moved from `scripts/` to `src/`. The release ZIP now packages the *contents* of `src/`, so the archive extracts straight to `SPSUpdate.ps1`, `Config\` and `Modules\` (no wrapper folder).
- **BREAKING** — Environment configuration moved from JSON to a PowerShell data file (`*.psd1`), read with `Import-PowerShellDataFile`. A single documented template (`Config\CONTOSO-PROD.example.psd1`) lists every option with its possible values inline. Runtime/output files (the ContentDB inventory and logs) stay JSON by design.
- **BREAKING** — The config key `StoredCredential` is renamed to `CredentialKey` and now points at an entry in `Config\secrets.psd1` instead of a Windows Credential Manager target.
- `SPSUpdate.ps1` now imports `SPSUpdate.Common`, derives its version banner from the module manifest, and reads/writes the InstallAccount credential through `Get-SPSSecret`/`Set-SPSSecret`.
- `Add-SPSUpdateEvent` is now self-contained (version/user resolved from the module) and re-points a mis-mapped event source to the `SPSUpdate` log instead of returning silently.
- CI: `release.yml` zips the contents of `src/`; `pester.yml` triggers on `src/**`, `tests/**` and `PSScriptAnalyzerSettings.psd1`, and runs PSScriptAnalyzer against `src/SPSUpdate.ps1` and the `SPSUpdate.Common` module.
- `README.md` trimmed to a short overview with quick links; the wiki (`Home`, `Getting-Started`, `Configuration`, `Usage`) rewritten for the new layout, psd1 config and DPAPI secret store.

### Fixed

scripts/Modules/sps.util.psm1:
- `Invoke-SPSCommand` now fails fast when the CredSSP `New-PSSession` cannot be established (`-ErrorAction Stop` + a clear `throw`) instead of falling back to running the SharePoint scriptblock **locally** on the master server with no session — which previously made PSConfig and side-by-side operations silently target the wrong server. Added an `OpenTimeout` and clearer remote-failure error messages, aligned with the SPSWeather hardening.

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

### Tests
- **BREAKING** — The bundled third-party `credentialmanager` module (DLLs + manifest) and all `Get-/New-/Remove-StoredCredential` usage.
- The legacy `scripts/` tree, including the flat `util.psm1` / `sps.util.psm1` helpers and the per-farm JSON configs.

tests/Modules/sps.util.Tests.ps1:
### Migration from 3.x

- 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`.
1. Convert each JSON config to a `*.psd1` file (see `Config\CONTOSO-PROD.example.psd1`) and rename `StoredCredential` to `CredentialKey`.
2. Re-run `.\SPSUpdate.ps1 -ConfigFile '<farm>.psd1' -Action Install -InstallAccount (Get-Credential)` **as the service account** to store the credential in `Config\secrets.psd1` (the previous Credential Manager entry is no longer used).
3. Extract the new ZIP on each server; it unpacks to `SPSUpdate.ps1`, `Config\` and `Modules\` directly.

A full list of changes in each version can be found in the [change log](CHANGELOG.md)
Loading
Loading