.temp_commit_revert/UserAdminModule-6383df066ad425f1fcb580c09aa46e3edd8eeea2/build/Publish-ToGallery.ps1

#requires -Version 5.1
<#
.SYNOPSIS
    Publishes UserAdminModule to the PowerShell Gallery.

.DESCRIPTION
    Creates a clean staging copy of the module (excluding build tools, CI config,
    and generated artefacts) and publishes it to the PowerShell Gallery using
    Publish-PSResource (Microsoft.PowerShell.PSResourceGet). PSResourceGet does not
    use dotnet pack internally and is the modern, reliable replacement for the
    broken Publish-Module / PowerShellGet v2 publish path.

    PSResourceGet is installed automatically if not present.

    Reference: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.psresourceget/publish-psresource

.PARAMETER ApiKey
    Your PowerShell Gallery NuGet API key. Create one at:
    https://www.powershellgallery.com → Sign in → API Keys → Create

.PARAMETER Repository
    The target repository. Defaults to 'PSGallery'.

.PARAMETER Validate
    Runs a pre-flight check without publishing. Builds the staging directory,
    verifies manifest integrity, checks no excluded folders leaked in, and
    confirms FunctionsToExport matches Public\ .ps1 files. No API key required.

.EXAMPLE
    .\build\Publish-ToGallery.ps1 -Validate

    Pre-flight validation — builds staging, verifies contents, prints pass/fail report.
    Safe to run at any time; no publish occurs and no API key is needed.

.EXAMPLE
    .\build\Publish-ToGallery.ps1 -Validate -Verbose

    Pre-flight validation with detailed staging output.

.EXAMPLE
    .\build\Publish-ToGallery.ps1 -ApiKey 'oy2abc...'

    Publishes the current version to the PowerShell Gallery.

.EXAMPLE
    .\build\Publish-ToGallery.ps1 -ApiKey 'oy2abc...' -WhatIf

    Dry run — shows what would be published without uploading.

.EXAMPLE
    .\build\Publish-ToGallery.ps1 -ApiKey 'oy2abc...' -Verbose

    Publishes with detailed output.

.NOTES
    Author: Luke Leigh
    Requires: Microsoft.PowerShell.PSResourceGet (installed automatically if missing)
    Tested on: PowerShell 5.1 and 7+

    Reference: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.psresourceget/publish-psresource

.LINK
    Publish-PSResource
    Test-ModuleManifest
#>

[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Publish')]
param(
    [Parameter(ParameterSetName = 'Publish', Mandatory, HelpMessage = 'PowerShell Gallery NuGet API key.')]
    [ValidateNotNullOrEmpty()]
    [string]$ApiKey,

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

    [Parameter(ParameterSetName = 'Validate', Mandatory)]
    [switch]$Validate
)

trap {
    Write-Error "Publish-ToGallery failed: $_"
    break
}

# ── Ensure PSResourceGet is available ─────────────────────────────────────────
# PSResourceGet (Publish-PSResource) does not use dotnet pack internally,
# avoiding the PowerShellGet v2 bug where dotnet.exe pack fails with exit
# code -2147450735 on newer .NET SDKs.
if (-not (Get-Module -Name Microsoft.PowerShell.PSResourceGet -ListAvailable)) {
    Write-Information 'Microsoft.PowerShell.PSResourceGet not found — installing...' -InformationAction Continue
    Install-Module -Name Microsoft.PowerShell.PSResourceGet -Force -AllowClobber -Scope CurrentUser
}
Import-Module -Name Microsoft.PowerShell.PSResourceGet -ErrorAction Stop

# ── Resolve paths ─────────────────────────────────────────────────────────────
$repoRoot     = Split-Path $PSScriptRoot -Parent
$manifestPath = Join-Path $repoRoot 'UserAdminModule.psd1'

if (-not (Test-Path $manifestPath)) {
    Write-Error "Module manifest not found at: $($manifestPath)"
    return
}

# ── Validate manifest ─────────────────────────────────────────────────────────
Write-Verbose "Validating manifest: $($manifestPath)"
$manifest = Test-ModuleManifest -Path $manifestPath -ErrorAction Stop
$prerelease = $manifest.PrivateData.PSData.Prerelease
$fullVersion = if ($prerelease) { "$($manifest.Version)-$($prerelease)" } else { $manifest.Version }

Write-Verbose "Module: $($manifest.Name)"
Write-Verbose "Version: $($fullVersion)"
Write-Verbose "Repo: $($Repository)"

# ── Build staging directory ───────────────────────────────────────────────────
# Staging folder must be named 'UserAdminModule' so Publish-Module picks up the
# correct module name from the directory name.
# Use GetFullPath to expand any 8.3 short names (e.g. LUKELE~1) — dotnet pack fails on 8.3 paths
$stagingBase = Join-Path ([System.IO.Path]::GetFullPath($env:TEMP)) "UAM-publish-$(Get-Date -Format 'yyyyMMddHHmmss')"
$stagingPath = Join-Path $stagingBase 'UserAdminModule'
New-Item -Path $stagingPath -ItemType Directory -Force | Out-Null
Write-Verbose "Staging directory: $($stagingPath)"

# Folders excluded from the PSGallery package
$excludedDirs = [System.Collections.Generic.HashSet[string]]::new(
    [System.StringComparer]::OrdinalIgnoreCase
)
@('.github', 'build', '.vscode', 'docs') | ForEach-Object { $null = $excludedDirs.Add($_) }

# File name patterns excluded from the PSGallery package
$excludedPatterns = @(
    '*.Tests.ps1',
    'FunctionIndex.json',
    'FunctionIndex.md',
    'ValidationReport.json',
    'UserAdminModule-Diagram.md',
    'SYNTAX-VALIDATION-REPORT.md',
    '*.png',
    '*.svg',
    '.gitignore'
)

function Test-ExcludedFile {
    param([System.IO.FileInfo]$File)
    foreach ($pattern in $excludedPatterns) {
        if ($File.Name -like $pattern) { return $true }
    }
    return $false
}

# ── Copy module contents to staging ──────────────────────────────────────────
Get-ChildItem -Path $repoRoot -ErrorAction SilentlyContinue | ForEach-Object {
    if ($_.PSIsContainer) {
        if (-not $excludedDirs.Contains($_.Name)) {
            Copy-Item -Path $_.FullName -Destination $stagingPath -Recurse -Force
        }
    }
    else {
        if (-not (Test-ExcludedFile -File $_)) {
            Copy-Item -Path $_.FullName -Destination $stagingPath -Force
        }
    }
}

# Remove Tests folders from any submodule copies
Get-ChildItem -Path $stagingPath -Recurse -Directory -Filter 'Tests' -ErrorAction SilentlyContinue |
    Remove-Item -Recurse -Force -ErrorAction SilentlyContinue

Write-Verbose "Staging complete. Contents:"
Get-ChildItem -Path $stagingPath | ForEach-Object { Write-Verbose " $($_.Name)" }

# ── Validate (pre-flight check — no publish) ──────────────────────────────────
if ($Validate) {
    $pass   = $true
    $report = [System.Collections.Generic.List[string]]::new()

    $report.Add(" [OK ] Manifest valid: $($manifest.Name) v$($fullVersion)")

    $leaked = Get-ChildItem -Path $stagingPath -Directory |
        Where-Object { $excludedDirs.Contains($_.Name) }
    if ($leaked) {
        $pass = $false
        foreach ($l in $leaked) { $report.Add(" [FAIL] Excluded dir present in staging: $($l.Name)") }
    }
    else {
        $report.Add(' [OK ] No excluded directories present in staging')
    }

    $stagedManifest = Join-Path $stagingPath 'UserAdminModule.psd1'
    if (Test-Path $stagedManifest) {
        $report.Add(' [OK ] Module manifest present in staging')
    }
    else {
        $pass = $false
        $report.Add(' [FAIL] Module manifest NOT found in staging')
    }

    $exportedFns = @($manifest.ExportedFunctions.Keys | Sort-Object)
    $publicPs1   = @(Get-ChildItem -Path (Join-Path $stagingPath 'Public') -Filter '*.ps1' -ErrorAction SilentlyContinue |
        Select-Object -ExpandProperty BaseName | Sort-Object)
    $missing = @($exportedFns | Where-Object { $_ -notin $publicPs1 })
    $extra   = @($publicPs1   | Where-Object { $_ -notin $exportedFns })
    if ($missing.Count -gt 0) {
        $pass = $false
        foreach ($m in $missing) { $report.Add(" [FAIL] In FunctionsToExport but no .ps1 found: $($m)") }
    }
    if ($extra.Count -gt 0) {
        foreach ($e in $extra) { $report.Add(" [WARN] .ps1 exists but not in FunctionsToExport: $($e)") }
    }
    if ($missing.Count -eq 0 -and $extra.Count -eq 0) {
        $report.Add(" [OK ] FunctionsToExport matches Public\ .ps1 files ($($exportedFns.Count) functions)")
    }

    if ($stagingPath -match '~\d') {
        $pass = $false
        $report.Add(' [FAIL] Staging path contains 8.3 short name — dotnet pack will fail')
        $report.Add(" Path: $($stagingPath)")
    }
    else {
        $report.Add(' [OK ] Staging path is a full long path')
    }

    Write-Information '' -InformationAction Continue
    Write-Information "Pre-flight Validation — UserAdminModule v$($fullVersion)" -InformationAction Continue
    Write-Information ('-' * 60) -InformationAction Continue
    foreach ($line in $report) { Write-Information $line -InformationAction Continue }
    Write-Information ('-' * 60) -InformationAction Continue
    if ($pass) {
        Write-Information ' RESULT: PASS — safe to publish' -InformationAction Continue
    }
    else {
        Write-Information ' RESULT: FAIL — resolve issues above before publishing' -InformationAction Continue
    }
    Write-Information '' -InformationAction Continue

    Remove-Item -Path $stagingBase -Recurse -Force -ErrorAction SilentlyContinue
    return
}

# ── Publish ───────────────────────────────────────────────────────────────────
$publishParams = @{
    Path        = $stagingPath
    ApiKey      = $ApiKey
    Repository  = $Repository
    ErrorAction = 'Stop'
}
if ($PSCmdlet.ShouldProcess("UserAdminModule v$($fullVersion)", "Publish to $($Repository)")) {
    Write-Information "Publishing UserAdminModule v$($fullVersion) to $($Repository)..." -InformationAction Continue
    Publish-PSResource @publishParams
    Write-Information 'Published successfully. View at: https://www.powershellgallery.com/packages/UserAdminModule' -InformationAction Continue
}

# ── Cleanup ───────────────────────────────────────────────────────────────────
Remove-Item -Path $stagingBase -Recurse -Force -ErrorAction SilentlyContinue
Write-Verbose 'Staging directory cleaned up.'