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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.2.4] - 2026-06-29

### Fixed

- `Add-SPSSheduledTask` no longer warns and skips when the task already exists:
it now registers the task in create-or-update mode (logon type 6) so `-Install`
reliably creates or refreshes the SharePoint task. Registration failures now
`throw` (with arguments and exception detail) instead of being swallowed by
`Write-Error`, so a failed install is visible (#30).
- `Get-SPSSecret` / `Set-SPSSecret` are now exported by the module. The entry
script called them directly, but they lived in `Private/`, so the scheduled run
could not resolve the stored credential and fell back to an interactive
credential prompt. Exporting them fixes the unattended run (#32).
- `-Install` / `-Uninstall` write a dedicated entry to the SPSWeather Event Log
(EventID 1003 / 1002) and print an explicit success line, so the installation
outcome is traceable in Event Viewer.

### Added

- `Add-SPSSheduledTask` gains an optional `-Description` parameter for the task
registration metadata.

## [2.2.3] - 2026-06-29

### Changed
Expand Down
15 changes: 15 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# SPSWeather - Release Notes

## [2.2.4] - 2026-06-29

### Fixed

- The scheduled task is now created reliably on -Install: Add-SPSSheduledTask
registers in create-or-update mode instead of skipping when a task exists, and
surfaces registration failures (throw) instead of silently swallowing them.
- Unattended runs no longer prompt for credentials: Get-SPSSecret/Set-SPSSecret
are exported, so the script resolves the DPAPI-stored credential instead of
prompting.
- -Install / -Uninstall now log to the SPSWeather Event Log and print an explicit
success line, so the install outcome is visible in Event Viewer.

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

## [2.2.3] - 2026-06-29

### Changed
Expand Down
100 changes: 49 additions & 51 deletions src/Modules/SPSWeather.Common/Public/Add-SPSSheduledTask.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
[System.String]
$TaskName, # Name of the scheduled task to be added

[Parameter()]
[System.String]
$Description = 'SPSWeather daily health-check', # Task description

[Parameter()]
[System.String]
$TaskPath = 'SharePoint' # Path of the task folder
Expand Down Expand Up @@ -40,57 +44,51 @@
Write-Output "Successfully created task folder '$TaskPath'"
}

# Retrieve the scheduled task
$getScheduledTask = $TaskFolder.GetTasks(0) | Where-Object -FilterScript {
$_.Name -eq $TaskName
}

if ($getScheduledTask) {
Write-Warning -Message 'Scheduled Task already exists - skipping.' # Task already exists
# Create or update the task (no skip): adopt the SPSUpdate pattern so an
# existing task is refreshed rather than silently skipped.
Write-Output '--------------------------------------------------------------'
Write-Output "Adding or updating '$TaskName' script in Task Scheduler Service ..."

# Get credentials for Task Schedule
$TaskAuthor = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name # Author of the task
$TaskUser = $UserName # Username for task registration
$TaskUserPwd = $Password # Password for task registration

# Add a new Task Schedule
$TaskSchd = $TaskSvc.NewTask(0)
$TaskSchd.RegistrationInfo.Description = "$($Description)" # Task description
$TaskSchd.RegistrationInfo.Author = $TaskAuthor # Task author
$TaskSchd.Principal.RunLevel = 1 # Task run level (1 = Highest)

# Task Schedule - Modify Settings Section
$TaskSettings = $TaskSchd.Settings
$TaskSettings.AllowDemandStart = $true
$TaskSettings.Enabled = $true
$TaskSettings.Hidden = $false
$TaskSettings.StartWhenAvailable = $true

# Task Schedule - Trigger Section: daily at 06:00
$TaskTriggers = $TaskSchd.Triggers
$TaskTrigger1 = $TaskTriggers.Create(2) # 2 = Daily trigger
$TaskTrigger1.StartBoundary = $TaskDate + 'T06:00:00' # Start time
$TaskTrigger1.DaysInterval = 1 # Interval of 1 day
$TaskTrigger1.Enabled = $true

# Define the task action
$TaskAction = $TaskSchd.Actions.Create(0) # 0 = Executable action
$TaskAction.Path = $TaskCmd # Path to the executable
$TaskAction.Arguments = $ActionArguments # Arguments for the executable

try {
# Register/update the task (6 = create or update, 1 = TASK_LOGON_PASSWORD)
[void]$TaskFolder.RegisterTaskDefinition($TaskName, $TaskSchd, 6, $TaskUser, $TaskUserPwd, 1)
Write-Output "Successfully added or updated '$TaskName' script in Task Scheduler Service"
}
else {
Write-Output '--------------------------------------------------------------'
Write-Output "Adding '$TaskName' script in Task Scheduler Service ..."

# Get credentials for Task Schedule
$TaskAuthor = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name # Author of the task
$TaskUser = $UserName # Username for task registration
$TaskUserPwd = $Password # Password for task registration

# Add a new Task Schedule
$TaskSchd = $TaskSvc.NewTask(0)
$TaskSchd.RegistrationInfo.Description = "$($TaskName) Task - Start at 6:00 daily" # Task description
$TaskSchd.RegistrationInfo.Author = $TaskAuthor # Task author
$TaskSchd.Principal.RunLevel = 1 # Task run level (1 = Highest)

# Task Schedule - Modify Settings Section
$TaskSettings = $TaskSchd.Settings
$TaskSettings.AllowDemandStart = $true
$TaskSettings.Enabled = $true
$TaskSettings.Hidden = $false
$TaskSettings.StartWhenAvailable = $true

# Task Schedule - Trigger Section
$TaskTriggers = $TaskSchd.Triggers

# Add Trigger Type 2 OnSchedule Daily Start at 6:00 AM
$TaskTrigger1 = $TaskTriggers.Create(2) # 2 = Daily trigger
$TaskTrigger1.StartBoundary = $TaskDate + 'T06:00:00' # Start time
$TaskTrigger1.DaysInterval = 1 # Interval of 1 day
$TaskTrigger1.Enabled = $true

# Define the task action
$TaskAction = $TaskSchd.Actions.Create(0) # 0 = Executable action
$TaskAction.Path = $TaskCmd # Path to the executable
$TaskAction.Arguments = $ActionArguments # Arguments for the executable

try {
# Register the task
$TaskFolder.RegisterTaskDefinition($TaskName, $TaskSchd, 6, $TaskUser, $TaskUserPwd, 1)
Write-Output "Successfully added '$TaskName' script in Task Scheduler Service"
}
catch {
Write-Error -Message $_ # Handle any errors during task registration
}
catch {
throw @"
An error occurred while adding/updating the scheduled task: $($TaskName)
ActionArguments: $($ActionArguments)
Exception: $($_.Exception.Message)
"@
}
}
4 changes: 3 additions & 1 deletion src/Modules/SPSWeather.Common/SPSWeather.Common.psd1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@{
RootModule = 'SPSWeather.Common.psm1'
ModuleVersion = '2.2.3'
ModuleVersion = '2.2.4'
GUID = 'c39bd612-8520-4e65-9037-80060894d654'
Author = 'Jean-Cyril DROUHIN'
CompanyName = 'luigilink'
Expand All @@ -23,6 +23,7 @@
'Get-SPSSearchEntCrawlLogs'
'Get-SPSSearchEntCrawlStatus'
'Get-SPSSearchEntTopology'
'Get-SPSSecret'
'Get-SPSServer'
'Get-SPSSiteHttpStatus'
'Get-SPSSolutionStatus'
Expand All @@ -42,6 +43,7 @@
'Join-HtmlBodyFromPSo'
'Remove-SPSSheduledTask'
'Resolve-SPSSqlAlias'
'Set-SPSSecret'
)

CmdletsToExport = @()
Expand Down
6 changes: 6 additions & 0 deletions src/SPSWeather.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ else {

# Remove the stored secret from secrets.psd1 (if present)
Set-SPSSecret -CredentialKey $envCfg.CredentialKey -ConfigPath $pathConfigFolder -Remove

Add-SPSWeatherEvent -Message "SPSWeather scheduled task '$spWeatherTaskName' removed on $env:COMPUTERNAME." -EntryType 'Information' -EventID 1002
Write-Output "SPSWeather uninstalled: task '$spWeatherTaskName' and secret '$($envCfg.CredentialKey)' removed."
}
elseif ($Install) {
# Persist the service credential as a DPAPI-encrypted SecureString in
Expand All @@ -200,6 +203,9 @@ else {

# Add SPSWeather script in a new scheduled Task
Add-SPSSheduledTask -ExecuteAsCredential $InstallAccount -TaskName $spWeatherTaskName -ActionArguments "-Execution Bypass $($scriptRootPath)\SPSWeather.ps1 -ConfigFile $($ConfigFile) -EnableSMTP"

Add-SPSWeatherEvent -Message "SPSWeather scheduled task '$spWeatherTaskName' installed/updated for $Application/$Environment on $env:COMPUTERNAME." -EntryType 'Information' -EventID 1003
Write-Output "SPSWeather installed: task '$spWeatherTaskName' created/updated and secret '$($envCfg.CredentialKey)' stored."
}
else {
# Initialize Security
Expand Down
8 changes: 3 additions & 5 deletions src/Test-SPSWeatherReadiness.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,13 @@ if ($null -ne $cfg -and $cfg.Contains('CredentialKey') -and $cfg.CredentialKey)
Add-CheckResult -Section 'Secrets' -Name 'secrets.psd1' -Status 'FAIL' -Detail "Not found at $secretsPath. Run SPSWeather.ps1 -Install as the service account."
}
else {
# Get-SPSSecret is private (not exported), so call it inside the module's
# session state where private functions are visible.
$module = Get-Module -Name SPSWeather.Common
if ($null -eq $module) {
# Get-SPSSecret is exported by the module; call it directly.
if ($null -eq (Get-Module -Name SPSWeather.Common)) {
Add-CheckResult -Section 'Secrets' -Name 'Get-SPSSecret' -Status 'SKIP' -Detail 'Module not loaded; cannot validate the secret'
}
else {
try {
$cred = & $module { param($k, $p) Get-SPSSecret -CredentialKey $k -ConfigPath $p -ErrorAction Stop } $cfg.CredentialKey $configFolder
$cred = Get-SPSSecret -CredentialKey $cfg.CredentialKey -ConfigPath $configFolder -ErrorAction Stop
if ($null -ne $cred -and $cred.GetNetworkCredential().Password.Length -gt 0) {
Add-CheckResult -Section 'Secrets' -Name "Credential '$($cfg.CredentialKey)'" -Status 'PASS' -Detail "DPAPI decrypt OK (user: $($cred.UserName))"
}
Expand Down
15 changes: 15 additions & 0 deletions tests/SPSWeather.Common.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Describe 'SPSWeather.Common module' {
'Get-SPSSearchEntCrawlLogs'
'Get-SPSSearchEntCrawlStatus'
'Get-SPSSearchEntTopology'
'Get-SPSSecret'
'Get-SPSServer'
'Get-SPSSiteHttpStatus'
'Get-SPSSolutionStatus'
Expand All @@ -73,6 +74,7 @@ Describe 'SPSWeather.Common module' {
'Join-HtmlBodyFromPSo'
'Remove-SPSSheduledTask'
'Resolve-SPSSqlAlias'
'Set-SPSSecret'
)
$actual = (Get-Command -Module SPSWeather.Common).Name | Sort-Object
$actual | Should -Be ($expected | Sort-Object)
Expand Down Expand Up @@ -135,6 +137,19 @@ Describe 'Public function contracts' {
$param.Attributes.Where{ $_.TypeId.Name -eq 'ParameterAttribute' }[0].Mandatory | Should -BeTrue
}

It 'Add-SPSSheduledTask exposes an optional -Description parameter' {
$param = (Get-Command -Name Add-SPSSheduledTask -Module SPSWeather.Common).Parameters['Description']
$param | Should -Not -BeNullOrEmpty
$param.Attributes.Where{ $_.TypeId.Name -eq 'ParameterAttribute' }[0].Mandatory | Should -BeFalse
}

It 'Add-SPSSheduledTask creates or updates the task (mode 6, no silent skip)' {
$src = (Get-Command -Name Add-SPSSheduledTask -Module SPSWeather.Common).Definition
$src | Should -Match 'RegisterTaskDefinition\([^)]*6,'
$src | Should -Not -Match 'already exists - skipping'
$src | Should -Match 'throw'
}

It 'Get-SPSVersion requires a mandatory -Server' {
$param = (Get-Command -Name Get-SPSVersion -Module SPSWeather.Common).Parameters['Server']
$param | Should -Not -BeNullOrEmpty
Expand Down
Loading