Modules/Private/Main/Test-AZSCModuleUpdate.ps1

<#
.SYNOPSIS
Checks PSGallery for a newer AzureScout release than the one currently loaded and
surfaces it to the caller (AB#369).

.DESCRIPTION
Called once from AzureScout.psm1 on module import, right after the module's own
functions are dot-sourced. This is the auto-UPDATE counterpart to the auto-INSTALL
dependency bootstrap at the top of AzureScout.psm1 -- same "never block import"
contract, same non-fatal try/catch style.

Never throws: every external call (Get-Module, Find-Module, Update-Module) is
reached only inside a try/catch, so an offline host, an unreachable PSGallery, or a
throttled/misconfigured repository degrades to a Write-Verbose and nothing else.
Import always proceeds regardless of what this function does.

Notify-only by default. A real reinstall is only attempted if the caller has
explicitly opted in via $env:AZURESCOUT_AUTO_UPDATE, and that opt-in is itself
ignored whenever a CI/automation signal is detected -- a pinned automation run
must never be silently moved onto a different module version mid-run. Absent the
opt-in, a newer version is only ever surfaced via Write-Warning with the exact
command to run.

.PARAMETER ManifestPath
Path to the AzureScout.psd1 manifest used to determine the currently-loaded
version when Get-Module can't yet see the module (e.g. mid-import). Defaults to
the manifest that ships alongside this module.

.NOTES
Throttle design: at most once every 24 hours, tracked via a small marker file in
the machine temp directory (not an in-memory/session flag). AzureScout is
typically re-imported once per new pwsh process (a fresh terminal session), so an
in-memory-only guard would not actually throttle anything across the common
"open a terminal, import the module" workflow -- only a file-based marker
persists across processes and avoids a PSGallery round-trip on every session.

CI/automation guard: any of the common CI environment variables (CI, TF_BUILD --
Azure DevOps, GITHUB_ACTIONS -- GitHub Actions) skip the entire check, not just
the reinstall. This keeps pinned/automation runs fast and deterministic -- no
surprise network call, no surprise notice, no chance of an unattended reinstall.
Opt out of the check entirely (interactively too) with $env:AZURESCOUT_SKIP_UPDATE_CHECK.

.COMPONENT
This PowerShell Module is part of Azure Scout (AZSC)

.NOTES
Version: 1.0.0
First Release Date: 23rd Jul, 2026
Work item: AB#369
#>

function Test-AZSCModuleUpdate {
    [CmdletBinding()]
    param(
        [string]$ManifestPath = (Join-Path $PSScriptRoot '..\..\..\AzureScout.psd1')
    )

    # Opt-out: skip the check entirely, no network call at all.
    if ($env:AZURESCOUT_SKIP_UPDATE_CHECK) {
        Write-Verbose '[AzureScout] Update check skipped ($env:AZURESCOUT_SKIP_UPDATE_CHECK set).'
        return
    }

    # CI/automation guard -- a pinned automation run should never spend time on, or be
    # surprised by, this check (and must never be auto-updated out from under it).
    $_isCi = [bool]($env:CI -or $env:TF_BUILD -or $env:GITHUB_ACTIONS)
    if ($_isCi) {
        Write-Verbose '[AzureScout] Update check skipped (CI/automation environment detected).'
        return
    }

    try {
        # Throttle: once per 24h, tracked in a marker file (see .NOTES for rationale).
        $_throttleFile = Join-Path ([System.IO.Path]::GetTempPath()) 'azurescout-update-check.txt'
        if (Test-Path -Path $_throttleFile -ErrorAction SilentlyContinue) {
            $_lastCheckRaw = Get-Content -Path $_throttleFile -Raw -ErrorAction SilentlyContinue
            $_lastCheck = [datetime]::MinValue
            if ($_lastCheckRaw -and [DateTime]::TryParse($_lastCheckRaw.Trim(), [ref]$_lastCheck)) {
                if (((Get-Date) - $_lastCheck).TotalHours -lt 24) {
                    Write-Verbose '[AzureScout] Update check throttled (checked within the last 24 hours).'
                    return
                }
            }
        }
        Set-Content -Path $_throttleFile -Value (Get-Date -Format 'o') -ErrorAction SilentlyContinue

        # Locally-loaded version: prefer an already-imported module instance, else fall
        # back to reading the manifest directly (Get-Module can't see this module yet
        # while AzureScout.psm1 is still mid-import).
        $_localVersion = (Get-Module -Name AzureScout -ErrorAction SilentlyContinue |
            Sort-Object -Property Version -Descending |
            Select-Object -First 1).Version
        if (-not $_localVersion) {
            $_localVersion = (Test-ModuleManifest -Path $ManifestPath -ErrorAction Stop).Version
        }

        $_galleryModule = Find-Module -Name AzureScout -Repository PSGallery -ErrorAction Stop
        $_galleryVersion = [Version]$_galleryModule.Version

        if ($_galleryVersion -gt $_localVersion) {
            if ($env:AZURESCOUT_AUTO_UPDATE) {
                Write-Warning "[AzureScout] Newer version $_galleryVersion found (you have $_localVersion) -- updating (`$env:AZURESCOUT_AUTO_UPDATE is set)..."
                try {
                    Update-Module -Name AzureScout -Force -ErrorAction Stop
                    Write-Warning "[AzureScout] Updated to $_galleryVersion. Restart your PowerShell session to load it."
                } catch {
                    Write-Warning "[AzureScout] Auto-update to $_galleryVersion failed: $_. Run 'Update-Module AzureScout' manually."
                }
            } else {
                Write-Warning "[AzureScout] A newer version is available: $_galleryVersion (you have $_localVersion). Run 'Update-Module AzureScout' to update, or set `$env:AZURESCOUT_AUTO_UPDATE = '1' to update automatically on import."
            }
        } else {
            Write-Verbose "[AzureScout] Already at the latest version ($_localVersion)."
        }
    } catch {
        Write-Verbose "[AzureScout] Update check skipped: $_"
    }
}