Public/Add-AzLocalPipelineVersionBanner.ps1

function Add-AzLocalPipelineVersionBanner {
    <#
    .SYNOPSIS
        Emits the module-version drift banner and step outputs that every
        AzLocal.UpdateManagement Step.*.yml install step needs.
 
    .DESCRIPTION
        Foundation cmdlet for the v0.8.5 thin-YAML refactor. Condenses the
        ~50-line install/drift/banner block that the v0.8.4 Step.*.yml
        files inline into ONE cmdlet call. The Step.*.yml install step
        becomes a three-liner:
 
            Install-Module AzLocal.UpdateManagement -Scope CurrentUser -Force -AllowClobber
            Import-Module AzLocal.UpdateManagement -Force
            Add-AzLocalPipelineVersionBanner -GeneratedAgainstVersion $env:GENERATED_AGAINST_MODULE_VERSION -PinnedVersion $env:REQUIRED_MODULE_VERSION
 
        The cmdlet:
          1. Resolves the INSTALLED module version (handling the
             multi-entry Get-Module quirk when nested modules are loaded)
          2. Looks up the LATEST version on PSGallery via Find-Module
             (unless -SkipPSGalleryQuery is set)
          3. Prints a "Module version summary" console block (identical
             shape to the v0.8.4 inline block so log greps keep working)
          4. Emits up to three drift annotations via
             Write-AzLocalPipelineWarning / Write-AzLocalPipelineNotice:
               - WARNING when installed < generated (YAML newer than module)
               - NOTICE when installed > generated (YAML older than module)
               - NOTICE when latest > installed (newer module on PSGallery)
          5. Appends a one-line banner to the rendered step summary via
             Add-AzLocalPipelineStepSummary
          6. Emits three step outputs via Set-AzLocalPipelineOutput:
               installed_module_version, generated_against_version,
               latest_on_psgallery
          7. Optionally returns a PSCustomObject describing the verdict
             when -PassThru is set (for test harnesses + downstream steps).
 
        Host detection (GitHub / AzureDevOps / Local) flows through the
        existing Phase 0 Private helpers - no per-host branching inside
        this cmdlet.
 
    .PARAMETER GeneratedAgainstVersion
        The module version this workflow YAML was generated against
        (typically '$env:GENERATED_AGAINST_MODULE_VERSION'). Required;
        must parse as a [version].
 
    .PARAMETER PinnedVersion
        The module version the operator pinned via the
        REQUIRED_MODULE_VERSION env var / workflow input. Empty / $null
        means "latest (fix-forward)". Used only for the human-readable
        "pin status" segment of the rendered banner.
 
    .PARAMETER ModuleName
        The PSGallery module to look up. Defaults to
        'AzLocal.UpdateManagement'. Parameterised so the cmdlet can be
        unit-tested against a synthetic module without touching the
        real Find-Module call.
 
    .PARAMETER PSGalleryRepositoryName
        The Find-Module -Repository value. Defaults to 'PSGallery'.
 
    .PARAMETER SkipPSGalleryQuery
        When set, the cmdlet does NOT call Find-Module. The "latest on
        PSGallery" segment of the banner renders as
        '(PSGallery lookup skipped)' and the step output
        'latest_on_psgallery' is the empty string. Use for offline
        runners or in tests.
 
    .PARAMETER SummaryFileName
        Forwarded to Add-AzLocalPipelineStepSummary. Defaults to
        'azlocal-version-banner.md'. Ignored on GitHub Actions (which
        uses a single $env:GITHUB_STEP_SUMMARY file managed by the
        runner).
 
    .PARAMETER PassThru
        When set, returns a PSCustomObject with InstalledVersion,
        GeneratedAgainstVersion, LatestOnPSGallery, PinStatus, Verdict.
 
    .OUTPUTS
        Nothing by default (side effects only). When -PassThru is set,
        returns a single PSCustomObject:
          InstalledVersion [version]
          GeneratedAgainstVersion [version]
          LatestOnPSGallery [version] or $null
          PinStatus [string] - 'pinned to vX.Y.Z' or 'latest (fix-forward)'
          Verdict [string] - one of:
                                       'in sync'
                                       'YAML newer than module - check REQUIRED_MODULE_VERSION'
                                       'YAML older than module - run Copy-AzLocalPipelineExample -Update'
                                       'newer module available on PSGallery'
 
    .EXAMPLE
        Add-AzLocalPipelineVersionBanner -GeneratedAgainstVersion '0.8.5'
 
        Default usage from a Step.*.yml install step. Emits drift
        annotations + banner + step outputs.
 
    .EXAMPLE
        $info = Add-AzLocalPipelineVersionBanner -GeneratedAgainstVersion '0.8.5' -PinnedVersion '0.8.5' -PassThru
        if ($info.Verdict -ne 'in sync') { ... }
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [ValidatePattern('^\d+\.\d+\.\d+(\.\d+)?$')]
        [string]$GeneratedAgainstVersion,

        [Parameter()]
        [AllowEmptyString()]
        [AllowNull()]
        [string]$PinnedVersion,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$ModuleName = 'AzLocal.UpdateManagement',

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$PSGalleryRepositoryName = 'PSGallery',

        [Parameter()]
        [switch]$SkipPSGalleryQuery,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$SummaryFileName = 'azlocal-version-banner.md',

        [Parameter()]
        [switch]$PassThru
    )

    $generated = [version]$GeneratedAgainstVersion

    $installedModule = Get-Module -Name $ModuleName | Sort-Object Version -Descending | Select-Object -First 1
    if (-not $installedModule) {
        throw "Add-AzLocalPipelineVersionBanner: module '$ModuleName' is not loaded. Call Import-Module before invoking this cmdlet."
    }
    $installed = [version]$installedModule.Version

    $latest = $null
    if (-not $SkipPSGalleryQuery) {
        try {
            $found = Find-Module -Name $ModuleName -Repository $PSGalleryRepositoryName -ErrorAction Stop
            if ($found -and $found.Version) {
                $latest = [version]$found.Version
            }
        } catch {
            Write-Verbose "Add-AzLocalPipelineVersionBanner: Find-Module lookup failed - $($_.Exception.Message)"
        }
    }

    $pinStatus = if ([string]::IsNullOrWhiteSpace($PinnedVersion)) { 'latest (fix-forward)' } else { "pinned to v$PinnedVersion" }
    $latestStr = if ($SkipPSGalleryQuery) { '(PSGallery lookup skipped)' } elseif ($latest) { "v$latest" } else { '(PSGallery lookup failed)' }
    $verdict   = if ($installed -lt $generated) { 'YAML newer than module - check REQUIRED_MODULE_VERSION' }
                 elseif ($installed -gt $generated) { 'YAML older than module - run Copy-AzLocalPipelineExample -Update' }
                 elseif ($latest -and ($latest -gt $installed)) { 'newer module available on PSGallery' }
                 else { 'in sync' }

    Write-Host ''
    Write-Host 'Module version summary'
    Write-Host " Installed on runner : $installed"
    Write-Host " YAML generated against : $generated"
    Write-Host " Latest on PSGallery : $(if ($SkipPSGalleryQuery) { '(skipped)' } elseif ($latest) { $latest } else { '(lookup failed - check network)' })"
    Write-Host " Pin status : $pinStatus"
    Write-Host " Verdict : $verdict"
    Write-Host ''

    if ($installed -lt $generated) {
        Write-AzLocalPipelineWarning `
            -Title "$ModuleName is older than workflow YAML expects" `
            -Message "$ModuleName v$installed is OLDER than the version this workflow YAML was generated against (v$generated). Cmdlets, parameters, or output schemas referenced by this YAML may not exist in v$installed. Set REQUIRED_MODULE_VERSION to v$generated, or refresh the YAML to match the installed module via 'Copy-AzLocalPipelineExample -Update'."
    }
    if ($installed -gt $generated) {
        Write-AzLocalPipelineNotice `
            -Title 'Workflow YAML may be stale' `
            -Message "Workflow YAML was generated against $ModuleName v$generated but the runner installed v$installed. Pipeline steps may have been improved in later releases - to refresh, re-run 'Copy-AzLocalPipelineExample -Update' (you will be prompted per file; add -Confirm:`$false to bypass). Pipeline YAMLs are under git so 'git diff' shows exactly what changed before commit."
    }
    if ($latest -and ($latest -gt $installed)) {
        Write-AzLocalPipelineNotice `
            -Title "Newer $ModuleName version available on PSGallery" `
            -Message "$ModuleName v$latest is available on PSGallery; this run installed v$installed. Review the module CHANGELOG before bumping REQUIRED_MODULE_VERSION (or clear the pin to install the latest automatically)."
    }

    $banner = "_Pipeline YAML v$generated | Module v$installed installed ($pinStatus) | PSGallery latest $latestStr | ${verdict}_`n"
    [void](Add-AzLocalPipelineStepSummary -Markdown $banner -SummaryFileName $SummaryFileName)

    Set-AzLocalPipelineOutput -Name 'installed_module_version'  -Value $installed.ToString()
    Set-AzLocalPipelineOutput -Name 'generated_against_version' -Value $generated.ToString()
    Set-AzLocalPipelineOutput -Name 'latest_on_psgallery'       -Value $(if ($latest) { $latest.ToString() } else { '' })

    if ($PassThru) {
        [PSCustomObject]@{
            InstalledVersion        = $installed
            GeneratedAgainstVersion = $generated
            LatestOnPSGallery       = $latest
            PinStatus               = $pinStatus
            Verdict                 = $verdict
        }
    }
}