Releases: luigilink/SPSUserSync
Release list
v1.3.4
SPSUserSync - Release Notes
[1.3.4] - 2026-07-10
This is a performance release for SPSyncUserProfile.ps1, with no change in
behaviour: the same profiles are created and updated, and the same JSON files and
HTML report are produced.
On a large (~100k-user) farm, a full profile reconciliation took over an hour. A
transcript analysis showed the time was spent almost entirely in the per-user loop,
and that the loop rebuilt the User Profile service context and UserProfileManager
on every single user — the dominant cost, and the cause of a steady throughput
decay over the run (object churn / GC pressure).
Changed
- The
UserProfileManageris now built once and reused for the whole run
(#24), instead of being reconstructed for every user insideAdd-SPSUserProfile.
This removes the dominant per-user cost and the associated memory churn. - The per-user transcript output is condensed to a single status line —
[UPDATE] DOMAIN\user (changed: WorkEmail, Country),[CREATE],[INFO]or
[UNKNOWN_USER]— replacing the previous ~14-line before/after dump. On a
~100k-user run this cuts a multi-million-line, tens-of-MB transcript down to one
line per user and removes the corresponding synchronous I/O. (#24)
Added
- End-of-run timing summary in
SPSyncUserProfile.ps1: users processed, wall
time, average milliseconds per user, and the CREATE / UPDATE / INFO /
UNKNOWN_USER breakdown — so each run's performance is visible directly in the
log. (#24)
Upgrade note
No configuration change is required and the output files are unchanged. Simply
deploy the new version; the next SPSyncUserProfile.ps1 run is faster and its log
is far smaller, ending with the new timing summary.
v1.3.3
SPSUserSync - Release Notes
[1.3.3] - 2026-07-09
This release adds AD account status detection so departed employees are handled
correctly, and it works for every customer because it relies only on universal
Active Directory signals — never on a customer-specific leaver process.
Two kinds of departed accounts are now distinguished and handled:
- Disabled accounts (kept in AD,
userAccountControlbit0x2): detected and,
with the new opt-inSkipDisabledUsers, reported instead of being given a profile. - Deleted accounts (no longer in AD): already not provisioned, now explicitly
tagged so they can be told apart from an actionable configuration gap.
Added
- Account status on every record —
ConvertTo-SPSUserRecordreads
userAccountControland exposesAccountStatus(Active/Disabled/
NotFound) andEnabled;SPSyncUserInfoList.ps1writesAccountStatusinto the
JSON snapshot. Universal by design: no dedicated OU, naming convention, HR feed or
retention assumption. A resolved account with nouserAccountControl(some non-AD
LDAP directories) staysActive, exactly as before. (#21) SkipDisabledUsers(sync-settings.psd1, default$false) — when$true,
SPSyncUserProfile.ps1reportsDisabledaccounts as Not Added instead of creating
or updating their profile. Default preserves the previous behaviour. (#21)NotAddedReasonon each Not-Added entry —AD_NOT_FOUND,MISSING_ATTRIBUTES
orDISABLED— with a per-reason breakdown in the run summary, so an expected miss
(a departed account) is easy to tell apart from an actionable one (e.g. a forest
missing fromad-domains.psd1). (#21)- The
SPSyncUserInfoListHTML report gains an AD Status column and a
Disabled in AD card (with a note pointing atSkipDisabledUsers), so disabled —
departed-but-retained — accounts are visible before the profile sync runs. (#21)
Changed
- CI: bump the GitHub Actions that ran on the deprecated Node.js 20 runtime —
actions/checkoutv4→v7,actions/upload-artifactv4→v7,
softprops/action-gh-releasev2→v3. CI-only; the packaged module and scripts
are unchanged. (#22)
Upgrade note
SkipDisabledUsers acts on the AccountStatus written by SPSyncUserInfoList.ps1
1.3.3+. Regenerate the JSON snapshot after upgrading for the flag to take effect;
a pre-1.3.3 snapshot carries no status, so no user is skipped as disabled until then.
There is no behaviour change for a correctly-deployed farm when SkipDisabledUsers
is left at its default $false.
v1.3.2
SPSUserSync - Release Notes
[1.3.2] - 2026-07-08
This is a hardening release that makes a broken or mis-deployed secrets.psd1
fail fast and loud instead of silently degrading the result. It follows a
field case where a secrets.psd1 that could not be decoded on the User Profile
master server (DPAPI SecureStrings are bound to the machine and account that
created them) caused thousands of real users to be skipped as UNKNOWN_USER,
with the only trace being one Event-Log line per user.
There is no behaviour change for a correctly-deployed farm; the generated JSON
and the profile updates are unchanged.
Fixed
- AD configuration/secret errors are no longer mistaken for "user absent"
(#18).Get-SPSADConnectionandGet-SPSADUsernow distinguish a build-time
misconfiguration (missingLdapPath, missingCredentialKey, or an
undecodable/missing secret) — which throws a terminatingSPSADConfigError—
from a genuine lookup miss, which still returns$null. Previously every
failure, including an undecodable DPAPI secret, was logged only to the Event
Log and returned as$null, so a broken forest silently produced empty-name
records and, on the profile side,UNKNOWN_USERdowngrades. SPSyncUserProfile.ps1pre-flights the AD configuration (#18) once per
credential-mode forest present in the input JSON, before the user loop, and
stops withExit 1and an actionable message when a forest's secret cannot be
decoded on this server — instead of silently skipping every affected user.SPSyncUserInfoList.ps1fails the run loudly (#18) when a forest cannot be
resolved because of a configuration/secret error, naming the affected
forest(s), rather than writing a JSON with a whole forest blanked out.
Added
AuthenticationTypeper domain inad-domains.psd1(#20) — maps to
System.DirectoryServices.AuthenticationTypesand is passed as the LDAP bind
type, so a non-Active-Directory directory can be configured entirely from the
file:'None'for a plain simple bind,'SecureSocketsLayer'for LDAPS on
port 636, and so on. It defaults to'Secure'(integrated Kerberos/NTLM),
unchanged for existing AD forests, and is now honoured onDefault-mode
domains too (previously onlyCredential-mode). The value is case-insensitive
and may combine flags (e.g.'SecureSocketsLayer, ServerBind'); an unknown
value fails the run with the list of valid names instead of silently falling
back.ad-domains.example.psd1gains a documented non-AD directory example.Get-SPSADConnectionError(#18) — a fast, query-free pre-flight that decodes
each referenced forest's secret (without issuing an LDAP search) and returns
the forests that fail. It backs the newSPSyncUserProfile.ps1pre-flight and
is reusable in your own checks.
Changed
- Connectivity errors stay non-fatal (#18). An LDAP server that is not
operational or returns a referral (for example an external directory
reachable from an application farm but not from the UPA master) is deliberately
not treated as a configuration error: the affected login is logged and left
unresolved so the rest of the run continues, andTest-SPSUserSyncReadiness.ps1
flags the forest so you can fix the LDAP path/routing. Only deterministic,
fixable configuration/secret errors abort the run.
Upgrade notes
Drop-in replacement for 1.3.1 — no configuration change required. Before enabling
the scheduled tasks, run Test-SPSUserSyncReadiness.ps1 on each server
(application farms and the UPA master), signed in as the service account, to
confirm every forest's secret decodes and binds. With 1.3.2 a forest whose secret
cannot be decoded now stops the run explicitly (exit code 1) instead of silently
producing empty profiles.
v1.3.1
SPSUserSync - Release Notes
[1.3.1] - 2026-07-08
This is a hardening release for SPSyncUserInfoList.ps1. It does not change the
generated JSON for a correctly-permissioned account, but it makes a wrong
service account (or a missing Shell Admin) fail loudly and early instead of
silently. It follows a field case where the script was launched with an
under-privileged account and produced no output and no visible error at all.
Fixed
- No more silent ACCESS_DENIED runs (#16).
Get-SPSite -Limit Allno longer
uses-ErrorAction SilentlyContinue. When the running account cannot enumerate
the farm site collections, theACCESS_DENIED(E_ACCESSDENIED 0x80070005) is
now surfaced to the console/transcript as well as the Windows Event Log,
with an actionable message pointing at the Shell Admin / correct service
account. Previously the error reached only the Event Log, so the operator saw an
almost-empty screen while the script kept running and then emitted two confusing
secondary errors on a JSON file that was never written. - No more overwriting/copying an empty snapshot (#16). When zero users are
collected — almost always a rights problem rather than a genuinely empty farm —
the previous goodSPSyncUserInfoListUserList.jsonis left untouched, an
explicit error is raised, and the HTML report and the remote copy are skipped
(the script exits1) instead of pushing empty or stale data to the User
Profile farm.
Added
- Readiness site-collection enumeration check (#16).
Test-SPSUserSyncReadiness.ps1
now walksGet-SPSite -Limit Alland FAILs onACCESS_DENIED, pointing at
the Shell Admin / service account prerequisite. This is the exact permission
SPSyncUserInfoList.ps1depends on, and it plugs the gap left by the previous
Get-SPFarm-only check (which only proves config-database access, while the
real run reads every content database). Run the readiness check as the service
account before enabling the scheduled task to catch a wrong account up front. - Regression tests (#16) for
Get-SPSUniqueUsers: healthy farm writes the JSON
and reports success; zero users writes no JSON and raises an Error event;
ACCESS_DENIEDsurfaces an actionable Error and writes no JSON.
Upgrade notes
Drop-in replacement for 1.3.0 — no configuration change required. If you rely on a
scheduled task, make sure the account it runs under is a Shell Admin on every
content database (Add-SPShellAdmin); with 1.3.1 a wrong account now fails the run
explicitly (exit code 1) instead of silently producing nothing.
v1.3.0
SPSUserSync - Release Notes
[1.3.0] - 2026-06-28
This release adds optional parallel Active Directory resolution to
SPSyncUserInfoList.ps1, for large multi-forest farms where the per-user LDAP
round-trip dominates the runtime. It is opt-in and off by default, and the
generated JSON is byte-for-byte identical whether parallel resolution is on or
off (verified on a SharePoint Subscription Edition farm).
Added
- Parallel AD resolution (#14). With
ParallelADResolution = $true(new
setting, default$false), the unique user logins are resolved against AD
concurrently through a RunspacePool — Windows PowerShell 5.1 compatible, no
ForEach-Object -Parallelrequired.MaxParallelADQueriessets the degree of
parallelism (0 = auto from the CPU count). Off by default because on small
farms the per-runspace module-import overhead is not amortized. Two new public
helpers back it:Resolve-SPSADUserBatch(the RunspacePool resolver) and
Get-SPSThrottleLimit(CPU-based default). A measured ~8x speedup on a 40-user
mock with 100 ms LDAP latency. ConvertTo-SPSUserRecord(#14) — the singleGet-SPSADUser-> record
projection shared by both the sequential path and the parallel worker, which is
what guarantees the identical JSON.
Changed
SPSyncUserInfoList.ps1now resolves each unique login against AD exactly
once (previously once per web the user appeared in), by separating the
user-collection, AD-resolution and JSON-building passes. This also speeds up
the default sequential mode on farms where users span many sites. The
user-removal walk (Set-SPUser/Remove-SPUser) is unchanged and still runs
per web. (#14)
Upgrade notes
- No action required:
ParallelADResolutionandMaxParallelADQueriesare
optional and default to the previous (sequential) behavior. Add them to
sync-settings.psd1only when you want to enable parallel resolution.
A full list of changes in each version can be found in the change log
v1.2.1
SPSUserSync - Release Notes
[1.2.1] - 2026-06-28
A small follow-up to 1.2.0, with one important fix to the User Profile
reconciliation script plus a documentation/readiness improvement around UPA
permissions.
Fixed
SPSyncUserProfile.ps1processed no eligible users at all — the run
reported "N users do not meet Prerequisites", wrote no
SPSyncUserAddedInUSPListfile and logged no error, so it looked like a silent
no-op.Add-SPSUserProfile's mandatory-ResultCollectionparameter rejected
the emptyArrayListon the first loop iteration, and a script-scoped
Trap { Continue }swallowed the terminating error and abandoned the whole
batch. Added[AllowEmptyCollection()], wrapped the per-user call in
try/catch(one failing user is logged and the batch continues), and removed
the misleadingTrap. (#13)
Added
- The readiness check (
Test-SPSUserSyncReadiness.ps1) now verifies the current
account can read the User Profile Service Application — a non-destructive
profile-count read viaUserProfileManager, on the UPA master only. This
catches the missing Manage Profiles permission that otherwise makes
SPSyncUserProfile.ps1fail at runtime with "ProfileDBCacheServiceClient.GetUserData
threw exception: Access is denied." PASS reports the profile count; WARN points
to the permission / farm-account prerequisite when the read is denied. (#11)
Changed
- The prerequisites now document that the account running
SPSyncUserProfile.ps1
must be able to manage profiles on the UPA — either the farm account or an
account granted Administrator of the UPA with the Manage Profiles
permission. (#10)
A full list of changes in each version can be found in the change log
v1.2.0
SPSUserSync - Release Notes
[1.2.0] - 2026-06-28
This release makes the toolkit deployable and verifiable on a real SharePoint
server — including Subscription Edition — without the SharePoint Management
Shell, adds a pre-flight readiness check, and makes user removal opt-in so a sync
never prunes accounts from a live farm unless you ask it to.
Behavior change — user removal is now opt-in. On a claims-based farm,
earlier versions removed classic-format and system principals (e.g.
NT AUTHORITY\authenticated users) whenSet-SPUser -SyncFromADcould not
resolve them. SPSUserSync now reports those users and leaves them in place by
default. SetRemoveUnresolvableUsers = $trueinsync-settings.psd1to
restore the previous pruning behavior.
Added
- SharePoint command surface, edition-aware (#6) —
Import-SPSSharePointCommand
loads the snap-in on SharePoint 2013/2016/2019 and theSharePointServermodule
on Subscription Edition (which no longer ships the snap-in), so both scripts run
from a plainpowershell.exe(a scheduled task) instead of requiring the
SharePoint Management Shell.Get-SPSInstalledProductVersionbacks the detection. - Pre-flight readiness check (#7) —
Test-SPSUserSyncReadiness.ps1validates
Host, Module, Config, Secrets, AD, SharePoint, the Event Log and the master-VM
share, with colored PASS/WARN/FAIL/SKIP output and a 0/1 exit code. Read-only and
non-destructive.Test-SPSADConnectionbacks the AD section (binds, reads one
user, reports which key attributes are populated);-SkipNetwork/
-SkipSharePointallow a config-only pass from a workstation. - Unresolved-user audit in the HTML report (#5) — rows whose identity did not
resolve from AD (no display name, or a display name equal to the de-claimed
login) are highlighted in amber, counted in a new Unresolved card, and
explained by a legend pointing toRemoveUnresolvableUsers. The UserProfile
report highlightsUNKNOWN_USERrows the same way. RemoveUnresolvableUserssetting (default$false) in
sync-settings.example.psd1(#4).
Changed
- User removal is opt-in (#4) — see the behavior change above. The classic
system principalsNT AUTHORITY\*,BUILTIN\*andSHAREPOINT\*are now always
excluded from processing, so they are never written to the JSON nor removed
(previously only their claims forms were excluded). - Both
SPSyncUserInfoList.ps1andSPSyncUserProfile.ps1load the SharePoint
command surface viaImport-SPSSharePointCommandat startup (#6). - The release ZIP now contains the contents of
src/at its root, so the
archive extracts straight into the deployment folder with no manual move (#8).
Fixed
- The
Logsfolder (transcript, rotation logs, deleted-user snapshots, HTML
reports) is written next to the calling script again, instead of inside the
module folder where it was wiped on every module redeploy (#3).
A full list of changes in each version can be found in the change log
v1.1.0
SPSUserSync - Release Notes
[1.1.0] - 2026-06-26
Added
- JSON snapshot history and anomaly detection (
SPSyncUserInfoList.ps1):Backup-SPSJsonFile— archives the previousSPSyncUserInfoListUserList.jsontoLogs\history\with a timestamp before each regeneration.Compare-SPSJsonSnapshots— pure function returningCurrentCount,PreviousCount,Delta,DropPercent,ThresholdPercent,IsAnomalous.- The script now archives the previous snapshot, regenerates, then raises a Warning in the SPSUserSync Event Log when the user count drops by at least
JsonDropThresholdPercent(helps catch an unreachable AD forest or a bad exclusion before the UPA reconciliation runs). - History snapshots are rotated using the existing
Clear-SPSLogFolderwith-Extension '*.json'.
- New settings in
sync-settings.example.psd1(backward-compatible defaults applied when absent):JsonHistoryRetentionDays(default 90)JsonDropThresholdPercent(default 20)GenerateHtmlReport(default$true)
- Self-contained HTML reporting:
Export-SPSUserReport— generates a single dependency-free HTML report (no CDN, works offline) for either dataset via-ReportType UserInfoList|UserProfile. Summary cards plus an interactive table (live search, column sort, pagination) rendered by embedded vanilla JavaScript. All AD-sourced values are HTML-encoded and rendered viatextContent, so names/emails cannot inject markup.SPSyncUserInfoList.ps1writesSPSyncUserInfoListReport-*.html(total users, email coverage, top countries, top AD domains).SPSyncUserProfile.ps1writesSPSyncUserProfileReport-*.html(counts by Status: CREATE / UPDATE / INFO / UNKNOWN_USER).- Reports are rotated with the existing
Clear-SPSLogFolderusing-Extension '*.html', and can be disabled by settingGenerateHtmlReport = $false.
- Pester test suite under
tests/(39 tests, cross-platform):SPSUserSync.Common.Tests.ps1— module import, manifest validity, public/private surface, parameter contractsCompare-SPSJsonSnapshots.Tests.ps1— drop detection, threshold edges, growth, empty-previousBackup-SPSJsonFile.Tests.ps1— timestamped copy, copy-not-move, folder creation, missing source, content preservationExport-SPSUserReport.Tests.ps1— both report types, empty dataset, and an HTML-injection safety testPrivate.Tests.ps1—ConvertFrom-SPSUserLogin,ConvertTo-SPSHtmlEncoded,Get-SPSJsonRecordCountviaInModuleScope
.github/workflows/pester.yml— runs Pester and PSScriptAnalyzer on pull requests touchingsrc/,tests/or the analyzer settings.PSScriptAnalyzerSettings.psd1— analyzer configuration (Error+Warning, withPSUseSingularNounsdisabled for the deliberately pluralCompare-SPSJsonSnapshots/Get-SPSUniqueUsers)..gitattributesand.editorconfig— encode the project's text conventions.
Changed
- All PowerShell files (
*.ps1,*.psm1,*.psd1) are now stored as UTF-8 with BOM and checked out with CRLF, so Windows PowerShell 5.1 reads any non-ASCII content correctly instead of falling back to the ANSI code page. YAML, Markdown and JSON keep LF and no BOM. - Renamed two private report helpers to non-state-changing verbs (
New-SPSReportCard->Get-SPSReportCardHtml,New-SPSReportTopList->Get-SPSReportTopListHtml) to satisfy PSScriptAnalyzer.
A full list of changes in each version can be found in the change log
v1.0.0
SPSUserSync - Release Notes
[1.0.0] - 2026-06-26
Added
- README.md
- Add code_of_conduct.md badge
- Add CODE_OF_CONDUCT.md file
- Add Issue Templates files:
- 1_bug_report.yml
- 2_feature_request.yml
- 3_documentation_request.yml
- 4_improvement_request.yml
- config.yml
- Add RELEASE-NOTES.md file
- Add CHANGELOG.md file
- Add CONTRIBUTING.md file
- Add SECURITY.md file
- Wiki documentation (
wiki/Home.md,wiki/Getting-Started.md,wiki/Configuration.md,wiki/Usage.md,wiki/Release-Process.md,wiki/_Sidebar.md) auto-synced to the GitHub Wiki by the existingwiki.ymlworkflow. The_Sidebar.mdrenders the navigation on every wiki page;Release-Process.mddocuments the maintainer checklist for shipping a new version.
Changed
- .gitignore
- Add patterns for runtime logs and local configuration files
- Prevent accidental commit of credentials and secrets
- .github/ISSUE_TEMPLATE/1_bug_report.yml
- Align version and PowerShell dropdown options with the project
- README.md
- Add Requirements section (SharePoint Server 2016/2019/SE, PowerShell 5.1, Farm Admin, AD reachability, farm property bags)
- CODE_OF_CONDUCT.md
- Set the enforcement contact to the project maintainer's GitHub profile (@luigilink)
- README.md
- Remove obsolete reference to farm property bags (now in
config/sync-settings.psd1)
- Remove obsolete reference to farm property bags (now in
Added
- Configuration scaffolding under
src/config/(ad-domains, secrets, sync-settings —.example.psd1only is versioned) - PowerShell module
src/Modules/SPSUserSync.Common/with manifest, loader, and 7 public functions:Add-SPSUserSyncEvent(dedicated SPSUserSync Windows Event Log),Clear-SPSLogFolder(log rotation),Get-SPSADConnection(DirectorySearcher per domain),Get-SPSADUser/Test-SPSADUser(SP-to-AD lookup),Get-SPSSyncSetting(settings loader),Initialize-SPSScript(admin check, transcript, banner). Private helpers cover login parsing, config loading and SecureString DPAPI secret decryption.
Changed
- src/SPSyncUserInfoList.ps1 refactored to consume the new module:
- All 19 AD domains, bind credentials, master VM names, MySite URLs, user-exclusion patterns and claim prefix moved out of code into
src/config/*.psd1 - JSON output is now UTF-8 (fixes accents corruption)
- Errors funnel into the SPSUserSync Event Log instead of
*_errlog.xmldumps
- All 19 AD domains, bind credentials, master VM names, MySite URLs, user-exclusion patterns and claim prefix moved out of code into
- src/SPSyncUserProfile.ps1 refactored to consume the new module:
- Same module-driven cleanup as SPSyncUserInfoList.ps1
- Reads the input JSON explicitly as UTF-8
- UPA log retention configurable via
UpaLogRetentionDays
- Both scripts no longer carry their own version string. The single source of truth is the
ModuleVersionfield ofSPSUserSync.Common.psd1;Initialize-SPSScriptauto-detects it. - The SPSUserSync Event Log header records the SPSUserSync version and the calling script name on dedicated lines so operators can filter events by version or script directly from Event Viewer or SCOM.
Added
.github/workflows/release.yml— automated release: packagessrc/and publishes a GitHub Release withRELEASE-NOTES.mdas body, triggered by pushing av*tag.
Changed (post-commit polish)
- README.md slimmed down to badges + short description + Quick links. Detailed Requirements / Usage / Configuration content moved to the Wiki.
- README.md: fix Code of Conduct badge link casing (
CODE_OF_CONDUCT.md). - SECURITY.md: replace the obsolete
APP_CODE/ENV_NAMEfarm property bag reference with a broader description of the values that must never be committed.
A full list of changes in each version can be found in the change log