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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
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.1.0] - 2026-07-15

### Added

- SPSCleanVersions.ps1
- Add `VersionPolicyMode` property selecting the version policy mechanism:
`Legacy` (default) keeps the per-library count-based `Set-PnPList` behaviour;
`AutoExpiration`, `ExpireAfter`, `NoExpiration` and `InheritFromTenant` drive
`Set-PnPSiteVersionPolicy` at the site level (the modern version-history model)
- Add `ExpireVersionsAfterDays` (for `ExpireAfter`, must be >= 30; `NoExpiration`
forces 0) with validation, addressing the customer request for version expiration
- Add `ApplyTo` (`New` / `Existing` / `Both`, default `Both`) mapping to
`-ApplyToNewDocumentLibraries` / `-ApplyToExistingDocumentLibraries`
- Add `Set-SiteVersionPolicy` helper that builds the `Set-PnPSiteVersionPolicy`
call per mode and honours `ShouldProcess` / `-WhatIf` / `DryRun`
- SPSCleanVersions.Tests.ps1
- Add tests for the site version policy modes, `ApplyTo` mapping, and a functional
context validating the `ExpireVersionsAfterDays` rules
- Wiki Documentation
- Document `VersionPolicyMode`, `ExpireVersionsAfterDays` and `ApplyTo`

## [3.0.0] - 2026-07-15

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ SPSCleanVersions is a PowerShell script tool to clean Version History in your Sh
* **Two Config Sources:** Pass configuration inline via `-InputJson` (ideal for Azure Automation Runbooks) or from a local JSON file via `-ConfigFile` (ideal for local execution and testing). Both share the same JSON schema and validation.
* **Simulation Mode:** Use `"DryRun": true` in the JSON (or `-WhatIf` locally) to preview changes safely.
* **Flexible Retention:** Define custom thresholds for major and minor versions.
* **Site Version Policy:** Set `"VersionPolicyMode"` to `AutoExpiration`, `ExpireAfter`, `NoExpiration` or `InheritFromTenant` to apply a site-level policy via `Set-PnPSiteVersionPolicy` — including version expiration (`ExpireVersionsAfterDays`) and applying to new and/or existing libraries. The default `Legacy` mode keeps the per-library count-based behaviour.
* **Multi-Site Processing:** Pass multiple Site Collection URLs in the `SiteUrls` JSON array to process them in a single execution.
* **Force Delete Old Versions:** Set `"ForceDeleteOldVersions": true` to trigger a batch delete job via `New-PnPSiteFileVersionBatchDeleteJob`. Requires delegated user context (automatically skipped in Azure Automation).
* **Azure Automation Ready:** Single string parameter avoids all runbook type limitations (arrays, switches, booleans).
Expand Down
32 changes: 12 additions & 20 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
# SPSCleanVersions - Release Notes

## [3.0.0] - 2026-07-15
## [3.1.0] - 2026-07-15

### Added

- SPSCleanVersions.ps1
- Add `-ConfigFile` parameter to load configuration from a local JSON file,
complementing the inline `-InputJson` string used by Azure Automation
Runbooks. Both input sources share the same JSON schema, parsing and
validation, so defaults and required-property checks stay identical
- `-InputJson` and `-ConfigFile` are mutually exclusive parameter sets, with
`InlineJson` as the default set
- Harden JSON parsing: trim the input, strip an accidental wrapping pair of
single quotes (a common copy/paste mistake from a command line), and reject
non-object JSON (string/scalar) with a clear, actionable message instead of
the misleading `SiteUrls is required` error
- Config/SPSCleanVersions.example.json
- Add example JSON configuration template
- Add `VersionPolicyMode` property selecting the version policy mechanism:
`Legacy` (default) keeps the per-library count-based `Set-PnPList` behaviour;
`AutoExpiration`, `ExpireAfter`, `NoExpiration` and `InheritFromTenant` drive
`Set-PnPSiteVersionPolicy` at the site level (the modern version-history model)
- Add `ExpireVersionsAfterDays` (for `ExpireAfter`, must be >= 30; `NoExpiration`
forces 0) with validation, addressing the customer request for version expiration
- Add `ApplyTo` (`New` / `Existing` / `Both`, default `Both`) mapping to
`-ApplyToNewDocumentLibraries` / `-ApplyToExistingDocumentLibraries`
- Add `Set-SiteVersionPolicy` helper that builds the `Set-PnPSiteVersionPolicy`
call per mode and honours `ShouldProcess` / `-WhatIf` / `DryRun`
- Wiki Documentation
- Document the file-based configuration workflow

### Changed

- .gitignore
- Track only `Config/*.example.json`; ignore real configs and local run
artifacts (`Logs/`, `Results/`)
- Document `VersionPolicyMode`, `ExpireVersionsAfterDays` and `ApplyTo`

A full list of changes in each version can be found in the [change log](CHANGELOG.md)
195 changes: 152 additions & 43 deletions scripts/SPSCleanVersions.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<#PSScriptInfo
.VERSION 3.0.0
.VERSION 3.1.0

.GUID 7ecf4acd-17c4-4c50-be79-1fcf2b6611fe

Expand Down Expand Up @@ -51,6 +51,14 @@
- ClientId (string, optional) — Azure AD App Registration Client ID.
- ForceDeleteOldVersions (boolean, optional, default: false) — Trigger batch delete of old file versions.
- DryRun (boolean, optional, default: false) — Simulate changes without applying them.
- VersionPolicyMode (string, optional, default: 'Legacy') — Version policy mechanism.
'Legacy' keeps the per-library count-based Set-PnPList behaviour.
'AutoExpiration', 'ExpireAfter', 'NoExpiration' and 'InheritFromTenant'
drive Set-PnPSiteVersionPolicy at the site level (modern model).
- ExpireVersionsAfterDays (integer, optional, default: 0) — For 'ExpireAfter' (>= 30);
'NoExpiration' forces 0.
- ApplyTo (string, optional, default: 'Both') — 'New', 'Existing' or 'Both'
document libraries (site version policy modes only).

.PARAMETER ConfigFile
Path to a local JSON file containing the same configuration schema as -InputJson.
Expand All @@ -70,11 +78,15 @@
.\SPSCleanVersions.ps1 -ConfigFile '.\Config\contoso-PROD.json'
Loads all configuration from a local JSON file. Ideal for local execution and testing.

.EXAMPLE
.\SPSCleanVersions.ps1 -InputJson '{"SiteUrls":["https://contoso.sharepoint.com/sites/site1"],"VersionPolicyMode":"ExpireAfter","ExpireVersionsAfterDays":180,"KeepMajorVersions":100}'
Applies a site-level ExpireAfter version policy (versions expire after 180 days, 100 major versions) via Set-PnPSiteVersionPolicy.

.NOTES
FileName: SPSCleanVersions.ps1
Author: Jean-Cyril DROUHIN
Date: July 15, 2026
Version: 3.0.0
Version: 3.1.0

.LINK
https://spjc.fr/
Expand Down Expand Up @@ -155,6 +167,36 @@ if (-not $config.PSObject.Properties['SiteUrls'] -or
[string]$ClientId = if ($config.PSObject.Properties['ClientId']) { $config.ClientId } else { '' }
[bool]$ForceDeleteOldVersions = if ($config.PSObject.Properties['ForceDeleteOldVersions']) { $config.ForceDeleteOldVersions } else { $false }
[bool]$DryRun = if ($config.PSObject.Properties['DryRun']) { $config.DryRun } else { $false }

# Site version policy (Set-PnPSiteVersionPolicy) properties. VersionPolicyMode selects
# the mechanism: 'Legacy' keeps the per-library Set-PnPList behaviour; the other modes
# drive Set-PnPSiteVersionPolicy at the site level (the modern version-history model).
$validModes = @('Legacy', 'AutoExpiration', 'ExpireAfter', 'NoExpiration', 'InheritFromTenant')
[string]$VersionPolicyMode = if ($config.PSObject.Properties['VersionPolicyMode']) { [string]$config.VersionPolicyMode } else { 'Legacy' }
$matchedMode = $validModes | Where-Object { $_ -ieq $VersionPolicyMode }
if (-not $matchedMode) {
throw "Invalid 'VersionPolicyMode' value '$VersionPolicyMode'. Allowed values: $($validModes -join ', ')."
}
$VersionPolicyMode = $matchedMode

[int]$ExpireVersionsAfterDays = if ($config.PSObject.Properties['ExpireVersionsAfterDays']) { $config.ExpireVersionsAfterDays } else { 0 }

$validApplyTo = @('New', 'Existing', 'Both')
[string]$ApplyTo = if ($config.PSObject.Properties['ApplyTo']) { [string]$config.ApplyTo } else { 'Both' }
$matchedApplyTo = $validApplyTo | Where-Object { $_ -ieq $ApplyTo }
if (-not $matchedApplyTo) {
throw "Invalid 'ApplyTo' value '$ApplyTo'. Allowed values: $($validApplyTo -join ', ')."
}
$ApplyTo = $matchedApplyTo

# ExpireVersionsAfterDays must be 0 (NoExpiration) or >= 30 (ExpireAfter), per the
# Set-PnPSiteVersionPolicy contract. ExpireAfter additionally requires a value >= 30.
if ($ExpireVersionsAfterDays -ne 0 -and $ExpireVersionsAfterDays -lt 30) {
throw "'ExpireVersionsAfterDays' must be 0 (no expiration) or greater than or equal to 30."
}
if ($VersionPolicyMode -eq 'ExpireAfter' -and $ExpireVersionsAfterDays -lt 30) {
throw "VersionPolicyMode 'ExpireAfter' requires 'ExpireVersionsAfterDays' to be greater than or equal to 30."
}
#endregion

# When DryRun is specified, enable WhatIf mode so that ShouldProcess calls are simulated.
Expand Down Expand Up @@ -183,6 +225,58 @@ function Test-IsAzureAutomation {
)
}

function Set-SiteVersionPolicy {
<#
.SYNOPSIS
Applies a site-level version policy via Set-PnPSiteVersionPolicy according to the
requested mode, honouring ShouldProcess/WhatIf.
#>
[CmdletBinding(SupportsShouldProcess)]
param
(
[Parameter(Mandatory = $true)] [string] $SiteUrl,
[Parameter(Mandatory = $true)] [ValidateSet('AutoExpiration', 'ExpireAfter', 'NoExpiration', 'InheritFromTenant')] [string] $Mode,
[Parameter()] [int] $MajorVersions,
[Parameter()] [int] $MajorWithMinorVersions,
[Parameter()] [int] $ExpireAfterDays,
[Parameter()] [ValidateSet('New', 'Existing', 'Both')] [string] $ApplyTo = 'Both'
)

# Build the base parameter set for the requested mode.
$params = @{}
switch ($Mode) {
'InheritFromTenant' {
$params['InheritFromTenant'] = $true
}
'AutoExpiration' {
$params['EnableAutoExpirationVersionTrim'] = $true
}
'ExpireAfter' {
$params['EnableAutoExpirationVersionTrim'] = $false
$params['ExpireVersionsAfterDays'] = $ExpireAfterDays
$params['MajorVersions'] = $MajorVersions
if ($MajorWithMinorVersions -gt 0) { $params['MajorWithMinorVersions'] = $MajorWithMinorVersions }
}
'NoExpiration' {
$params['EnableAutoExpirationVersionTrim'] = $false
$params['ExpireVersionsAfterDays'] = 0
$params['MajorVersions'] = $MajorVersions
if ($MajorWithMinorVersions -gt 0) { $params['MajorWithMinorVersions'] = $MajorWithMinorVersions }
}
}

# Target new and/or existing document libraries. InheritFromTenant clears the site
# setting so new libraries follow the tenant; the existing-libraries request is
# still valid alongside it.
if ($ApplyTo -eq 'New' -or $ApplyTo -eq 'Both') { $params['ApplyToNewDocumentLibraries'] = $true }
if ($ApplyTo -eq 'Existing' -or $ApplyTo -eq 'Both') { $params['ApplyToExistingDocumentLibraries'] = $true }

if ($PSCmdlet.ShouldProcess($SiteUrl, "Set site version policy ($Mode, ApplyTo=$ApplyTo)")) {
Set-PnPSiteVersionPolicy @params -ErrorAction Stop
Write-Output "`tSite version policy applied: Mode=$Mode; ApplyTo=$ApplyTo"
}
}

foreach ($SiteUrl in $SiteUrls) {
Write-Output "Processing Site: $SiteUrl"

Expand All @@ -202,50 +296,65 @@ foreach ($SiteUrl in $SiteUrls) {
Connect-PnPOnline -Url $SiteUrl -Interactive -ClientId $ClientId
}

# Get all Lists in the Site
Write-Output "Retrieving lists from $SiteUrl..."
$allLists = Get-PnPList
$targetLists = $allLists | Where-Object {
$_.Hidden -eq $false -and
$_.EnableVersioning -eq $true -and
$_.RootFolder.ServerRelativeUrl -notlike "*_catalogs*" -and
$_.RootFolder.ServerRelativeUrl -notlike "*/SiteAssets*" -and
$_.RootFolder.ServerRelativeUrl -notlike "*/SitePages*" -and
$_.RootFolder.ServerRelativeUrl -notlike "*/Style Library*" -and
$_.BaseTemplate -eq 101 # Document Libraries only
}
foreach ($list in $targetLists) {
$minorDesired = ($KeepMinorVersions -gt 0)
$changeNeeded = ($list.MajorVersionLimit -ne $KeepMajorVersions) -or
($list.EnableMinorVersions -ne $minorDesired) -or
($minorDesired -and ($list.MajorWithMinorVersionsLimit -ne $KeepMinorVersions)) -or
(-not $minorDesired -and ($list.MajorWithMinorVersionsLimit -ne 0))

if ($changeNeeded) {
if ($PSCmdlet.ShouldProcess($list.Title, "Set versioning policy")) {
$p = @{
Identity = "$($list.Title)"
EnableVersioning = $true
MajorVersions = $KeepMajorVersions
}
if ($minorDesired) {
$p.EnableMinorVersions = $true
$p.MinorVersions = $KeepMinorVersions
}
else {
$p.EnableMinorVersions = $false
}
try {
Set-PnPList @p -ErrorAction Stop
Write-Output "`t$($list.Title) -> Major=$KeepMajorVersions; MinorEnabled=$minorDesired; MinorLimit=$KeepMinorVersions"
}
catch {
Write-Warning "`tFAILED $($list.Title): $($_.Exception.Message)"
if ($VersionPolicyMode -eq 'Legacy') {
# --- Legacy mode: per-library count-based limits via Set-PnPList ---
# Get all Lists in the Site
Write-Output "Retrieving lists from $SiteUrl..."
$allLists = Get-PnPList
$targetLists = $allLists | Where-Object {
$_.Hidden -eq $false -and
$_.EnableVersioning -eq $true -and
$_.RootFolder.ServerRelativeUrl -notlike "*_catalogs*" -and
$_.RootFolder.ServerRelativeUrl -notlike "*/SiteAssets*" -and
$_.RootFolder.ServerRelativeUrl -notlike "*/SitePages*" -and
$_.RootFolder.ServerRelativeUrl -notlike "*/Style Library*" -and
$_.BaseTemplate -eq 101 # Document Libraries only
}
foreach ($list in $targetLists) {
$minorDesired = ($KeepMinorVersions -gt 0)
$changeNeeded = ($list.MajorVersionLimit -ne $KeepMajorVersions) -or
($list.EnableMinorVersions -ne $minorDesired) -or
($minorDesired -and ($list.MajorWithMinorVersionsLimit -ne $KeepMinorVersions)) -or
(-not $minorDesired -and ($list.MajorWithMinorVersionsLimit -ne 0))

if ($changeNeeded) {
if ($PSCmdlet.ShouldProcess($list.Title, "Set versioning policy")) {
$p = @{
Identity = "$($list.Title)"
EnableVersioning = $true
MajorVersions = $KeepMajorVersions
}
if ($minorDesired) {
$p.EnableMinorVersions = $true
$p.MinorVersions = $KeepMinorVersions
}
else {
$p.EnableMinorVersions = $false
}
try {
Set-PnPList @p -ErrorAction Stop
Write-Output "`t$($list.Title) -> Major=$KeepMajorVersions; MinorEnabled=$minorDesired; MinorLimit=$KeepMinorVersions"
}
catch {
Write-Warning "`tFAILED $($list.Title): $($_.Exception.Message)"
}
}
}
else {
Write-Output "`t$($list.Title) already compliant"
}
}
else {
Write-Output "`t$($list.Title) already compliant"
}
else {
# --- Site version policy mode: Set-PnPSiteVersionPolicy at the site level ---
Write-Output "Applying site version policy on $SiteUrl (Mode=$VersionPolicyMode)..."
try {
Set-SiteVersionPolicy -SiteUrl $SiteUrl -Mode $VersionPolicyMode `
-MajorVersions $KeepMajorVersions -MajorWithMinorVersions $KeepMinorVersions `
-ExpireAfterDays $ExpireVersionsAfterDays -ApplyTo $ApplyTo
}
catch {
Write-Warning "`tFAILED to apply site version policy on ${SiteUrl}: $($_.Exception.Message)"
}
}

Expand Down
Loading