Publish-ToGallery.ps1

<#
.SYNOPSIS
Complete workflow to test and publish iFacto.AICodeReview module to PowerShell Gallery
 
.DESCRIPTION
This script provides a complete manual workflow for publishing the module:
1. Validates module structure
2. Runs all tests
3. Checks version isn't already published
4. Optionally updates version
5. Publishes to PowerShell Gallery
 
.PARAMETER NuGetApiKey
NuGet API key for PowerShell Gallery. Get from https://www.powershellgallery.com/account/apikeys
Can also be set via $env:NUGET_API_KEY
 
.PARAMETER UpdateVersion
Update the module version before publishing. Format: X.Y.Z (e.g., 0.2.0)
 
.PARAMETER SkipTests
Skip running Pester tests (not recommended)
 
.PARAMETER SkipValidation
Skip build validation (not recommended)
 
.PARAMETER WhatIf
Show what would be done without actually publishing
 
.PARAMETER Force
Skip confirmation prompts
 
.EXAMPLE
# First time setup - get your API key
$apiKey = Read-Host "Enter your PowerShell Gallery API Key" -AsSecureString
$env:NUGET_API_KEY = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($apiKey))
 
.EXAMPLE
# Publish current version
.\Publish-ToGallery.ps1
 
.EXAMPLE
# Update version and publish
.\Publish-ToGallery.ps1 -UpdateVersion "0.2.0"
 
.EXAMPLE
# Test run without publishing
.\Publish-ToGallery.ps1 -WhatIf
 
.EXAMPLE
# Force publish without prompts
.\Publish-ToGallery.ps1 -Force
 
.NOTES
Prerequisites:
- PowerShell Gallery API key from https://www.powershellgallery.com/account/apikeys
- Pester 5.x installed (Install-Module Pester -Force -SkipPublisherCheck -MinimumVersion 5.0)
- Git repository with no uncommitted changes (recommended)
#>

[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter()]
    [string]$NuGetApiKey = $env:NUGET_API_KEY,
    
    [Parameter()]
    [ValidatePattern('^\d+\.\d+\.\d+$')]
    [string]$UpdateVersion,
    
    [Parameter()]
    [switch]$SkipTests,
    
    [Parameter()]
    [switch]$SkipValidation,
    
    [Parameter()]
    [switch]$Force
)

$ErrorActionPreference = 'Stop'

# Module details
$moduleRoot = $PSScriptRoot
$moduleName = 'iFacto.AICodeReview'
$manifestPath = Join-Path $moduleRoot "$moduleName.psd1"

Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " PowerShell Gallery Publishing Workflow" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Module: $moduleName" -ForegroundColor White
Write-Host "Path: $moduleRoot" -ForegroundColor Gray
Write-Host ""

# =============================================================================
# PRE-FLIGHT CHECKS
# =============================================================================

Write-Host "Pre-flight Checks" -ForegroundColor Yellow
Write-Host "----------------" -ForegroundColor Yellow

# Check 1: API Key
if ([string]::IsNullOrWhiteSpace($NuGetApiKey)) {
    Write-Host "❌ NuGet API key not found" -ForegroundColor Red
    Write-Host ""
    Write-Host "To get your API key:" -ForegroundColor Cyan
    Write-Host "1. Go to https://www.powershellgallery.com/" -ForegroundColor White
    Write-Host "2. Sign in with your account" -ForegroundColor White
    Write-Host "3. Go to https://www.powershellgallery.com/account/apikeys" -ForegroundColor White
    Write-Host "4. Create a new API key with 'Push' permission" -ForegroundColor White
    Write-Host ""
    Write-Host "Then set it:" -ForegroundColor Cyan
    Write-Host "`$env:NUGET_API_KEY = 'your-api-key-here'" -ForegroundColor White
    Write-Host ""
    throw "NuGet API key required"
}
Write-Host " ✓ NuGet API key found" -ForegroundColor Green

# Check 2: Pester
try {
    $pesterModule = Get-Module -ListAvailable -Name Pester | Sort-Object Version -Descending | Select-Object -First 1
    if ($pesterModule.Version.Major -lt 5) {
        Write-Host " ⚠️ Pester version $($pesterModule.Version) found (5.x recommended)" -ForegroundColor Yellow
        Write-Host " Install with: Install-Module Pester -Force -SkipPublisherCheck -MinimumVersion 5.0" -ForegroundColor Gray
    } else {
        Write-Host " ✓ Pester $($pesterModule.Version) installed" -ForegroundColor Green
    }
} catch {
    Write-Host " ⚠️ Pester not installed (required for tests)" -ForegroundColor Yellow
}

# Check 3: Git status
try {
    $gitStatus = git status --porcelain 2>&1
    if ($gitStatus -and -not $Force) {
        Write-Host " ⚠️ Git repository has uncommitted changes" -ForegroundColor Yellow
        Write-Host " Consider committing changes before publishing" -ForegroundColor Gray
    } else {
        Write-Host " ✓ Git repository clean" -ForegroundColor Green
    }
} catch {
    Write-Host " ℹ️ Not a git repository" -ForegroundColor Cyan
}

# Check 4: Module manifest exists
if (-not (Test-Path $manifestPath)) {
    throw "Module manifest not found: $manifestPath"
}
Write-Host " ✓ Module manifest found" -ForegroundColor Green

Write-Host ""

# =============================================================================
# VERSION MANAGEMENT
# =============================================================================

Write-Host "Version Management" -ForegroundColor Yellow
Write-Host "------------------" -ForegroundColor Yellow

# Get current version
$manifest = Import-PowerShellDataFile -Path $manifestPath
$currentVersion = $manifest.ModuleVersion
Write-Host " Current version: $currentVersion" -ForegroundColor White

# Update version if requested
if ($UpdateVersion) {
    Write-Host " New version: $UpdateVersion" -ForegroundColor Cyan
    
    if ($UpdateVersion -eq $currentVersion) {
        Write-Host " ⚠️ New version same as current version" -ForegroundColor Yellow
    }
    
    if (-not $Force) {
        $confirm = Read-Host "Update version from $currentVersion to $UpdateVersion? (y/N)"
        if ($confirm -ne 'y') {
            throw "Version update cancelled by user"
        }
    }
    
    # Update manifest file
    $manifestContent = Get-Content $manifestPath -Raw
    $manifestContent = $manifestContent -replace "ModuleVersion\s*=\s*'[\d\.]+?'", "ModuleVersion = '$UpdateVersion'"
    Set-Content -Path $manifestPath -Value $manifestContent -NoNewline
    
    Write-Host " ✓ Version updated to $UpdateVersion" -ForegroundColor Green
    $currentVersion = $UpdateVersion
    
    # Update changelog
    $changelogPath = Join-Path $moduleRoot 'CHANGELOG.md'
    if (Test-Path $changelogPath) {
        Write-Host " ℹ️ Don't forget to update CHANGELOG.md" -ForegroundColor Cyan
    }
}

# Check if version already published
Write-Host ""
Write-Host "Checking PowerShell Gallery..." -ForegroundColor Yellow
try {
    $published = Find-Module -Name $moduleName -RequiredVersion $currentVersion -ErrorAction SilentlyContinue
    if ($published) {
        throw "Version $currentVersion is already published to PowerShell Gallery. Use -UpdateVersion to publish a new version."
    }
    Write-Host " ✓ Version $currentVersion not yet published" -ForegroundColor Green
} catch {
    if ($_.Exception.Message -like "*already published*") {
        throw
    }
    # Module doesn't exist yet - that's fine for first publish
    Write-Host " ✓ Module not found in gallery (first publish)" -ForegroundColor Green
}

Write-Host ""

# =============================================================================
# BUILD VALIDATION
# =============================================================================

if (-not $SkipValidation) {
    Write-Host "Build Validation" -ForegroundColor Yellow
    Write-Host "----------------" -ForegroundColor Yellow
    
    $buildScript = Join-Path $moduleRoot 'Build\Build-Module.ps1'
    if (Test-Path $buildScript) {
        try {
            & $buildScript -SkipTests:$SkipTests
            Write-Host ""
        } catch {
            throw "Build validation failed: $_"
        }
    } else {
        Write-Host " ⚠️ Build script not found, performing basic validation..." -ForegroundColor Yellow
        
        # Basic validation
        $requiredFolders = @('Public', 'Private', 'Rules')
        foreach ($folder in $requiredFolders) {
            $folderPath = Join-Path $moduleRoot $folder
            if (-not (Test-Path $folderPath)) {
                throw "Required folder missing: $folder"
            }
        }
        Write-Host " ✓ Basic structure validation passed" -ForegroundColor Green
        Write-Host ""
    }
} else {
    Write-Host "⚠️ Skipping build validation (not recommended)" -ForegroundColor Yellow
    Write-Host ""
}

# =============================================================================
# SUMMARY & CONFIRMATION
# =============================================================================

Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Ready to Publish" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "Module: $moduleName" -ForegroundColor White
Write-Host "Version: $currentVersion" -ForegroundColor White
Write-Host "Destination: PowerShell Gallery" -ForegroundColor White
Write-Host ""

if ($WhatIfPreference) {
    Write-Host "WhatIf: Would publish module to PowerShell Gallery" -ForegroundColor Yellow
    Write-Host ""
    Write-Host "✓ All checks passed - ready to publish" -ForegroundColor Green
    exit 0
}

if (-not $Force) {
    Write-Host "This will publish the module to PowerShell Gallery where it will be publicly available." -ForegroundColor Yellow
    Write-Host ""
    $confirm = Read-Host "Continue with publish? (y/N)"
    if ($confirm -ne 'y') {
        Write-Host ""
        Write-Host "Publish cancelled by user" -ForegroundColor Yellow
        exit 0
    }
}

# =============================================================================
# PUBLISH
# =============================================================================

Write-Host ""
Write-Host "Publishing to PowerShell Gallery..." -ForegroundColor Cyan
Write-Host ""

try {
    # Publish module
    Publish-Module `
        -Path $moduleRoot `
        -NuGetApiKey $NuGetApiKey `
        -Repository PSGallery `
        -Verbose
    
    Write-Host ""
    Write-Host "========================================" -ForegroundColor Green
    Write-Host "✓ Successfully Published!" -ForegroundColor Green
    Write-Host "========================================" -ForegroundColor Green
    Write-Host ""
    Write-Host "Module: $moduleName" -ForegroundColor White
    Write-Host "Version: $currentVersion" -ForegroundColor White
    Write-Host ""
    Write-Host "View at: https://www.powershellgallery.com/packages/$moduleName/$currentVersion" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Install with:" -ForegroundColor Yellow
    Write-Host " Install-Module $moduleName" -ForegroundColor White
    Write-Host ""
    Write-Host "Note: It may take a few minutes for the module to be searchable in the gallery." -ForegroundColor Gray
    Write-Host ""
    
    # Tag in git if possible
    if (-not (git rev-parse --git-dir 2>$null)) {
        Write-Host "Next steps:" -ForegroundColor Yellow
        Write-Host "1. Commit the version change: git commit -am 'Release v$currentVersion'" -ForegroundColor White
        Write-Host "2. Create a tag: git tag v$currentVersion" -ForegroundColor White
        Write-Host "3. Push changes: git push && git push --tags" -ForegroundColor White
        Write-Host ""
    }
    
} catch {
    Write-Host ""
    Write-Host "========================================" -ForegroundColor Red
    Write-Host "❌ Publish Failed" -ForegroundColor Red
    Write-Host "========================================" -ForegroundColor Red
    Write-Host ""
    Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host ""
    
    if ($_.Exception.Message -like "*conflict*") {
        Write-Host "This usually means the version is already published." -ForegroundColor Yellow
        Write-Host "Use -UpdateVersion to publish a new version." -ForegroundColor Yellow
    }
    
    Write-Host ""
    throw
}