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) If not specified and current version is already published, you will be prompted. .PARAMETER BumpVersion Automatically bump the version. Options: Major, Minor, Patch (default) Example: -BumpVersion Patch will increment 0.1.0 to 0.1.1 .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()] [ValidateSet('Major', 'Minor', 'Patch')] [string]$BumpVersion, [Parameter()] [switch]$SkipTests, [Parameter()] [switch]$SkipValidation, [Parameter()] [switch]$Force ) $ErrorActionPreference = 'Stop' # Load .env file if exists $envPath = Join-Path $PSScriptRoot '.env' if (Test-Path $envPath) { Write-Verbose "Loading environment variables from .env file" Get-Content $envPath | ForEach-Object { if ($_ -match '^\s*([^#][^=]+?)\s*=\s*(.+?)\s*$') { $key = $matches[1] $value = $matches[2] # Only set if not already set via parameter or environment if ([string]::IsNullOrEmpty($NuGetApiKey) -and $key -eq 'NUGET_API_KEY') { $NuGetApiKey = $value Write-Verbose "Loaded NUGET_API_KEY from .env file" } } } } # 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 store it in .env file (recommended):" -ForegroundColor Cyan Write-Host "1. Copy .env.example to .env" -ForegroundColor White Write-Host " Copy-Item .env.example .env" -ForegroundColor Gray Write-Host "2. Edit .env and add your key:" -ForegroundColor White Write-Host " NUGET_API_KEY=your-api-key-here" -ForegroundColor Gray Write-Host "" Write-Host "Or set environment variable:" -ForegroundColor Cyan Write-Host "`$env:NUGET_API_KEY = 'your-api-key-here'" -ForegroundColor Gray Write-Host "" Write-Host "Or pass as parameter:" -ForegroundColor Cyan Write-Host ".\Publish-ToGallery.ps1 -NuGetApiKey 'your-api-key'" -ForegroundColor Gray 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 # Function to bump version function Get-BumpedVersion { param( [string]$Version, [string]$BumpType = 'Patch' ) $parts = $Version.Split('.') $major = [int]$parts[0] $minor = [int]$parts[1] $patch = [int]$parts[2] switch ($BumpType) { 'Major' { return "$($major + 1).0.0" } 'Minor' { return "$major.$($minor + 1).0" } 'Patch' { return "$major.$minor.$($patch + 1)" } } } # Check if version already published first $isVersionPublished = $false try { $published = Find-Module -Name $moduleName -RequiredVersion $currentVersion -ErrorAction SilentlyContinue if ($published) { $isVersionPublished = $true Write-Host " ⚠️ Version $currentVersion is already published" -ForegroundColor Yellow } } catch { # Module doesn't exist yet - that's fine for first publish } # Determine new version $newVersion = $null if ($UpdateVersion) { # Explicit version provided $newVersion = $UpdateVersion Write-Host " New version (specified): $newVersion" -ForegroundColor Cyan } elseif ($BumpVersion) { # Auto-bump requested $newVersion = Get-BumpedVersion -Version $currentVersion -BumpType $BumpVersion Write-Host " New version ($BumpVersion bump): $newVersion" -ForegroundColor Cyan } elseif ($isVersionPublished -and -not $Force) { # Current version is published, prompt for version bump Write-Host "" Write-Host " Current version is already published. You need to bump the version." -ForegroundColor Yellow Write-Host "" Write-Host " Suggested versions:" -ForegroundColor Cyan Write-Host " [P] Patch: $(Get-BumpedVersion -Version $currentVersion -BumpType 'Patch') (bug fixes)" -ForegroundColor White Write-Host " [M] Minor: $(Get-BumpedVersion -Version $currentVersion -BumpType 'Minor') (new features)" -ForegroundColor White Write-Host " [J] Major: $(Get-BumpedVersion -Version $currentVersion -BumpType 'Major') (breaking changes)" -ForegroundColor White Write-Host " [C] Custom version" -ForegroundColor White Write-Host "" $choice = Read-Host "Select version bump (P/M/J/C) or Q to quit" switch ($choice.ToUpper()) { 'P' { $newVersion = Get-BumpedVersion -Version $currentVersion -BumpType 'Patch' } 'M' { $newVersion = Get-BumpedVersion -Version $currentVersion -BumpType 'Minor' } 'J' { $newVersion = Get-BumpedVersion -Version $currentVersion -BumpType 'Major' } 'C' { $newVersion = Read-Host "Enter version (X.Y.Z format)" if ($newVersion -notmatch '^\d+\.\d+\.\d+$') { throw "Invalid version format. Use X.Y.Z (e.g., 1.0.0)" } } 'Q' { throw "Publish cancelled by user" } default { throw "Invalid choice. Publish cancelled." } } Write-Host " Selected version: $newVersion" -ForegroundColor Cyan } # Update version if needed if ($newVersion -and $newVersion -ne $currentVersion) { if ($newVersion -eq $currentVersion) { Write-Host " ⚠️ New version same as current version" -ForegroundColor Yellow } if (-not $Force) { Write-Host "" $confirm = Read-Host "Update version from $currentVersion to $newVersion? (y/N)" if ($confirm -ne 'y') { throw "Version update cancelled by user" } } # Update manifest file Write-Host " Updating manifest..." -ForegroundColor Cyan $manifestContent = Get-Content $manifestPath -Raw $manifestContent = $manifestContent -replace "ModuleVersion\s*=\s*'[\d\.]+?'", "ModuleVersion = '$newVersion'" Set-Content -Path $manifestPath -Value $manifestContent -NoNewline Write-Host " ✓ Version updated to $newVersion" -ForegroundColor Green $currentVersion = $newVersion # Remind about changelog $changelogPath = Join-Path $moduleRoot 'CHANGELOG.md' if (Test-Path $changelogPath) { Write-Host " ℹ️ Don't forget to update CHANGELOG.md" -ForegroundColor Cyan } } # Final 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 or -BumpVersion 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 { # Handle folder naming requirement: Publish-Module requires folder name = module name $parentPath = Split-Path $moduleRoot -Parent $currentFolderName = Split-Path $moduleRoot -Leaf $publishPath = $moduleRoot if ($currentFolderName -ne $moduleName) { Write-Host " Folder name mismatch detected: '$currentFolderName' should be '$moduleName'" -ForegroundColor Yellow Write-Host " Creating properly named copy for publishing..." -ForegroundColor Cyan $tempPublishPath = Join-Path $parentPath $moduleName # Remove old temp copy if exists if (Test-Path $tempPublishPath) { Remove-Item $tempPublishPath -Recurse -Force } # Create properly named copy Copy-Item -Path $moduleRoot -Destination $tempPublishPath -Recurse -Force $publishPath = $tempPublishPath Write-Host " ✓ Created publishing copy" -ForegroundColor Green } # Publish module from parent directory Push-Location (Split-Path $publishPath -Parent) try { Publish-Module ` -Path ".\$moduleName" ` -NuGetApiKey $NuGetApiKey ` -Repository PSGallery ` -Verbose } finally { Pop-Location # Clean up temp copy if created if ($currentFolderName -ne $moduleName -and (Test-Path $tempPublishPath)) { Remove-Item $tempPublishPath -Recurse -Force Write-Host " ✓ Cleaned up temporary copy" -ForegroundColor Gray } } 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 } |