diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2cfdc..28c7dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 95edc9b..9fab8ab 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -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 diff --git a/src/Modules/SPSWeather.Common/Public/Add-SPSSheduledTask.ps1 b/src/Modules/SPSWeather.Common/Public/Add-SPSSheduledTask.ps1 index 9495d01..7509651 100644 --- a/src/Modules/SPSWeather.Common/Public/Add-SPSSheduledTask.ps1 +++ b/src/Modules/SPSWeather.Common/Public/Add-SPSSheduledTask.ps1 @@ -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 @@ -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) +"@ } } diff --git a/src/Modules/SPSWeather.Common/Private/Get-SPSSecret.ps1 b/src/Modules/SPSWeather.Common/Public/Get-SPSSecret.ps1 similarity index 100% rename from src/Modules/SPSWeather.Common/Private/Get-SPSSecret.ps1 rename to src/Modules/SPSWeather.Common/Public/Get-SPSSecret.ps1 diff --git a/src/Modules/SPSWeather.Common/Private/Set-SPSSecret.ps1 b/src/Modules/SPSWeather.Common/Public/Set-SPSSecret.ps1 similarity index 100% rename from src/Modules/SPSWeather.Common/Private/Set-SPSSecret.ps1 rename to src/Modules/SPSWeather.Common/Public/Set-SPSSecret.ps1 diff --git a/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 b/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 index 37fde1d..af8e525 100644 --- a/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 +++ b/src/Modules/SPSWeather.Common/SPSWeather.Common.psd1 @@ -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' @@ -23,6 +23,7 @@ 'Get-SPSSearchEntCrawlLogs' 'Get-SPSSearchEntCrawlStatus' 'Get-SPSSearchEntTopology' + 'Get-SPSSecret' 'Get-SPSServer' 'Get-SPSSiteHttpStatus' 'Get-SPSSolutionStatus' @@ -42,6 +43,7 @@ 'Join-HtmlBodyFromPSo' 'Remove-SPSSheduledTask' 'Resolve-SPSSqlAlias' + 'Set-SPSSecret' ) CmdletsToExport = @() diff --git a/src/SPSWeather.ps1 b/src/SPSWeather.ps1 index 1498fe8..8c718ce 100644 --- a/src/SPSWeather.ps1 +++ b/src/SPSWeather.ps1 @@ -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 @@ -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 diff --git a/src/Test-SPSWeatherReadiness.ps1 b/src/Test-SPSWeatherReadiness.ps1 index 1d07c29..fb03ccc 100644 --- a/src/Test-SPSWeatherReadiness.ps1 +++ b/src/Test-SPSWeatherReadiness.ps1 @@ -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))" } diff --git a/tests/SPSWeather.Common.Tests.ps1 b/tests/SPSWeather.Common.Tests.ps1 index 7644822..c34cdbc 100644 --- a/tests/SPSWeather.Common.Tests.ps1 +++ b/tests/SPSWeather.Common.Tests.ps1 @@ -54,6 +54,7 @@ Describe 'SPSWeather.Common module' { 'Get-SPSSearchEntCrawlLogs' 'Get-SPSSearchEntCrawlStatus' 'Get-SPSSearchEntTopology' + 'Get-SPSSecret' 'Get-SPSServer' 'Get-SPSSiteHttpStatus' 'Get-SPSSolutionStatus' @@ -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) @@ -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