Private/Core/Invoke-PostRunPlugins.ps1

<#
.SYNOPSIS
Discover and run the configured PostRun plugins with the run's RunResult object.

.DESCRIPTION
Iterates the Plugins array from the configuration in order, skipping every descriptor whose
Type is not PostRun. For each enabled descriptor it verifies <PluginsRoot>\<Function>.ps1
exists, dot-sources it, and confirms the function resolves — disabling the plugin with a Warn
if the file fails to load or the function is missing. Each plugin is invoked as
& <Function> -RunResult <RunResult>.

Called from the finally block of Invoke-IDBridge after every run (including failed and
ReadOnly runs — the RunResult carries Success/ReadOnly so plugins decide for themselves).
Every plugin invocation is isolated in its own try/catch: a failing PostRun plugin logs a
Warn and the next plugin still runs. This function never throws — it can never mask the
run's real outcome. Plugins read config and logs through the existing accessors
(Get-IDBridgeConfig, Get-IDBridgeLogs); the RunResult carries what those don't.

.PARAMETER RunResult
The RunResult object assembled by Invoke-IDBridge (SchemaVersion 1): outcome, timing, mode
flags, applied-work counts, threshold results, enriched source data, and the per-directory
change lists. SecureString values have already been scrubbed to $null.

.OUTPUTS
None. Side effects are whatever the plugins do (reports, webhooks, dashboards) plus log output.

.EXAMPLE
Invoke-PostRunPlugins -RunResult $runResult

.NOTES
   Created by: Sam Cattanach
   Modified: 2026-07-12
#>

function Invoke-PostRunPlugins {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [pscustomobject]$RunResult
    )

    #region Import Configuration
    # This runs inside the caller's finally block, so even a config failure must not throw.
    try { $IDConfig = Get-IDBridgeConfig } catch { return }
    #endregion Import Configuration

    foreach ($plugin in $IDConfig.Plugins) {
        if ($plugin.Type -ne "PostRun") { Continue }

        if ($plugin.Enabled -ne $true) {
            Write-Log -Message "Plugin: $($plugin.Function) is disabled in config. Skipping plugin." -Level Trace
            Continue
        }

        if (Test-Path "$($IDConfig.Paths.PluginsRoot)\$($plugin.Function).ps1" -PathType Leaf -ErrorAction SilentlyContinue) {
            try {
                . "$($IDConfig.Paths.PluginsRoot)\$($plugin.Function).ps1"
            }
            catch {
                Write-Log -Message "Plugin: $($plugin.Function) is enabled in config but failed to load. Disabling plugin. Error: $($_)" -Level Warn
                $plugin.Enabled = $false
                Continue
            }
        }

        if (-not (Get-Command $plugin.Function -ErrorAction SilentlyContinue)) {
            Write-Log -Message "Plugin: $($plugin.Function) is enabled in config but not found. Disabling plugin." -Level Warn
            $plugin.Enabled = $false
            Continue
        }

        #If the plugin is enabled and the function exists, run the plugin with the RunResult.
        #Each plugin is isolated: a failure is logged and the remaining plugins still run.
        try {
            Write-Log -Message "Running $($plugin.Type) Plugin: $($plugin.Function)" -Level Info
            & $plugin.Function -RunResult $RunResult
        }
        catch {
            Write-Log -Message "Plugin: PostRun: $($plugin.Function) failed (run unaffected): $($_)" -Level Warn
        }
    }
}