Public/Publish-PCRelease.ps1

<#
.SYNOPSIS
    Performs a full release: bump version, commit, tag, push, and create GitHub Release.
.DESCRIPTION
    Orchestrates the complete release workflow:
    1. Validates release readiness (Test-PCReleaseReady)
    2. Calculates next version (Get-PCNextVersion)
    3. Updates version in project file (Set-PCProjectVersion)
    4. Commits the version bump
    5. Creates annotated git tag
    6. Pushes commit and tag to remote
    7. Optionally creates a GitHub Release via gh CLI

    Requires: git, optionally gh (GitHub CLI) for GitHub Release creation.
.PARAMETER IncrementType
    Type of version increment: Major, Minor, or Patch.
.PARAMETER Version
    Explicit version to set (overrides IncrementType calculation).
.PARAMETER Path
    Project directory. Defaults to current directory.
.PARAMETER SkipTests
    Skip test validation during readiness check.
.PARAMETER CreateGitHubRelease
    Create a GitHub Release locally via gh CLI. Off by default — the CI release
    workflow is the intended sole authority for creating GitHub Releases after
    tests pass on the runner. Use only when releasing without CI.
.PARAMETER DryRun
    Show what would happen without making changes.
.PARAMETER Force
    Skip interactive confirmation prompts.
.EXAMPLE
    Publish-PCRelease -IncrementType Patch
    # Bumps patch version and releases
.EXAMPLE
    Publish-PCRelease -Version "2.0.0" -Force
    # Sets explicit version and releases without confirmation
.EXAMPLE
    Publish-PCRelease -IncrementType Minor -DryRun
    # Shows what would happen without making changes
#>

function Publish-PCRelease {
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Increment')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Increment')]
        [ValidateSet('Major', 'Minor', 'Patch')]
        [string]$IncrementType,

        [Parameter(Mandatory, ParameterSetName = 'Explicit')]
        [string]$Version,

        [Parameter()]
        [string]$Path = (Get-Location).Path,

        [switch]$SkipTests,
        [switch]$CreateGitHubRelease,
        [switch]$DryRun,
        [switch]$Force
    )

    Push-Location $Path
    try {
        # Step 1: Validate readiness
        Write-Host "Checking release readiness..." -ForegroundColor Cyan
        $readiness = Test-PCReleaseReady -Path $Path
        
        if (-not $readiness.Pass) {
            Write-Host "`nRelease validation FAILED:" -ForegroundColor Red
            foreach ($err in $readiness.Errors) {
                Write-Host " - $err" -ForegroundColor Red
            }
            throw "Release validation failed. Fix the issues above and retry."
        }
        Write-Host " All checks passed" -ForegroundColor Green

        # Step 2: Determine target version
        $currentInfo = Get-PCProjectVersion -Path $Path
        $currentVersion = $currentInfo.Version

        if ($PSCmdlet.ParameterSetName -eq 'Increment') {
            $newVersion = Get-PCNextVersion -IncrementType $IncrementType -CurrentVersion $currentVersion
        }
        else {
            $null = Get-SemanticVersion -Version $Version  # validate format
            $newVersion = $Version -replace '^v', ''
        }

        $tagName = "v$newVersion"

        # Check if tag already exists
        $existingTag = git tag -l $tagName 2>$null
        if ($existingTag) {
            throw "Tag '$tagName' already exists. Choose a different version."
        }

        # Step 3: Confirm
        Write-Host "`nRelease Plan:" -ForegroundColor Cyan
        Write-Host " Current version: $currentVersion"
        Write-Host " New version: $newVersion"
        Write-Host " Tag: $tagName"
        Write-Host " Project: $($currentInfo.ProjectType.FileName)"
        Write-Host ""

        if ($DryRun) {
            Write-Host "[DRY RUN] Would perform:" -ForegroundColor Yellow
            Write-Host " 1. Update version in $($currentInfo.ProjectType.FileName) to $newVersion"
            Write-Host " 2. Commit: 'Release $tagName'"
            Write-Host " 3. Tag: $tagName"
            Write-Host " 4. Push commit and tag to origin"
            Write-Host " 5. CI release workflow runs tests and publishes to PSGallery + creates GitHub Release"
            if ($CreateGitHubRelease) {
                Write-Host " * -CreateGitHubRelease: would also create a local GitHub Release (bypasses CI gate)"
            }
            return [PSCustomObject]@{
                Status  = 'DryRun'
                Version = $newVersion
                Tag     = $tagName
            }
        }

        if (-not $Force) {
            $confirm = Read-Host "Proceed with release $tagName? [y/N]"
            if ($confirm -notin @('y', 'Y', 'yes', 'Yes')) {
                Write-Host "Release cancelled." -ForegroundColor Yellow
                return
            }
        }

        # Step 4: Bump version
        Write-Host "`nUpdating version..." -ForegroundColor Cyan
        Set-PCProjectVersion -Version $newVersion -Path $Path
        Write-Host " Version set to $newVersion in $($currentInfo.ProjectType.FileName)" -ForegroundColor Green

        # Step 5: Commit
        Write-Host "Committing..." -ForegroundColor Cyan
        git add -A
        git commit -m "Release $tagName"
        if ($LASTEXITCODE -ne 0) { throw "Git commit failed" }
        Write-Host " Committed: Release $tagName" -ForegroundColor Green

        # Step 6: Tag
        Write-Host "Creating tag..." -ForegroundColor Cyan
        git tag -a $tagName -m "Release $newVersion"
        if ($LASTEXITCODE -ne 0) { throw "Git tag failed" }
        Write-Host " Tagged: $tagName" -ForegroundColor Green

        # Step 7: Push
        Write-Host "Pushing to remote..." -ForegroundColor Cyan
        git push origin HEAD
        if ($LASTEXITCODE -ne 0) { throw "Git push failed" }
        git push origin $tagName
        if ($LASTEXITCODE -ne 0) { throw "Git push tag failed" }
        Write-Host " Pushed commit and tag" -ForegroundColor Green

        # Step 8: GitHub Release (only when explicitly requested)
        $ghRelease = $null
        if ($CreateGitHubRelease) {
            Write-Host "Creating GitHub Release..." -ForegroundColor Cyan
            Write-Warning "Local GitHub Release creation bypasses CI stage gates. Prefer letting the release workflow create the release after tests pass on the runner."
            $ghAvailable = Get-Command gh -ErrorAction SilentlyContinue
            if ($ghAvailable) {
                $ghArgs = @('release', 'create', $tagName, '--title', "Release $newVersion", '--generate-notes')
                gh @ghArgs
                if ($LASTEXITCODE -eq 0) {
                    Write-Host " GitHub Release created" -ForegroundColor Green
                    $ghRelease = $true
                }
                else {
                    Write-Warning "GitHub Release creation failed (exit code: $LASTEXITCODE). Tag was pushed successfully — CI workflow will create the release if tests pass."
                }
            }
            else {
                Write-Warning "gh CLI not found. Tag was pushed — CI workflow will create the release if tests pass."
            }
        }
        else {
            Write-Host "GitHub Release will be created by CI after tests pass on the runner." -ForegroundColor Cyan
        }

        # Summary
        Write-Host "`nRelease $tagName complete!" -ForegroundColor Green
        
        return [PSCustomObject]@{
            Status        = 'Released'
            Version       = $newVersion
            Tag           = $tagName
            GitHubRelease = $ghRelease
            ProjectFile   = $currentInfo.ProjectType.FileName
        }
    }
    finally {
        Pop-Location
    }
}