src/GitHubReleaseManagement.ps1
| # GitHub Release Management - Complete Smart Release Ecosystem function New-SmartRelease { <# .SYNOPSIS Creates a complete semantic release with Git tags and GitHub release using the proven Draft → Smart Tags → Publish strategy. .DESCRIPTION This is the main function for creating complete semantic releases. It combines Git tag creation with GitHub release management using a safe, proven workflow strategy: 1. 📦 Create GitHub Release as DRAFT (safe, reversible) 2. 🏷️ Create Smart Tags (only if Draft successful) 3. 🚀 Publish GitHub Release (only if Smart Tags successful) This strategy ensures that failed operations can be safely rolled back and provides comprehensive status reporting at each step. .PARAMETER TargetVersion The semantic version to create (e.g., "v1.2.3", "1.2.3"). .PARAMETER RepositoryPath Path to the Git repository. Defaults to current working directory. .PARAMETER ReleaseNotes Custom release notes content. .PARAMETER ReleaseNotesFile Path to a file containing release notes. .PARAMETER Force Force creation even if version already exists. .PARAMETER PushToRemote Push created tags to remote repository. .PARAMETER SkipGitHubRelease Only create Git tags, skip GitHub release creation. .EXAMPLE $result = New-SmartRelease -TargetVersion "v1.2.3" # Creates complete release with draft → tags → publish workflow .EXAMPLE $result = New-SmartRelease -TargetVersion "v2.0.0" -ReleaseNotes "Major release with breaking changes" Write-Host $result.GitHubSummary .OUTPUTS PSCustomObject with comprehensive release status including: - Git tag creation status - GitHub release creation status - Each step's success/failure - Rollback information - GitHub workflow integration data #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateScript({ if ($_ -match '^v?\d+\.\d+\.\d+(-(?:alpha|beta|rc|preview|pre)(?:\.\d+)?)?(\+[a-zA-Z0-9\-\.]+)?$') { $true } else { throw "TargetVersion '$_' is not a valid semantic version." } })] [string]$TargetVersion, [Parameter()] [string]$RepositoryPath = (Get-Location).Path, [Parameter()] [string]$ReleaseNotes, [Parameter()] [string]$ReleaseNotesFile, [Parameter()] [switch]$Force, [Parameter()] [switch]$PushToRemote, [Parameter()] [switch]$SkipGitHubRelease ) begin { $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() Write-SafeLog "INFO" "Starting smart release creation" "TargetVersion: $TargetVersion" # Normalize version $normalizedVersion = if ($TargetVersion.StartsWith('v')) { $TargetVersion } else { "v$TargetVersion" } $isPrerelease = $normalizedVersion -match '-(alpha|beta|rc|preview|pre)' # Initialize comprehensive result object $result = [PSCustomObject]@{ TargetVersion = $normalizedVersion BumpType = "Patch" # Will be determined Success = $false # Git Tag Results GitTagsResult = $null TagsCreated = @() TagsMovedFrom = @{} TagsStaticized = @() # GitHub Release Results GitHubReleaseResult = $null ReleaseId = $null ReleaseUrl = "" ReleaseDraftCreated = $false ReleasePublished = $false # Workflow Status StepResults = @{ DraftCreation = @{ Success = $false; Message = ""; Timestamp = $null } TagCreation = @{ Success = $false; Message = ""; Timestamp = $null } ReleasePublication = @{ Success = $false; Message = ""; Timestamp = $null } } # Standard fields Duration = $null ConflictsResolved = @() RollbackInfo = @{ TagsToDelete = @() TagsToRestore = @{} ReleaseToDelete = $null OriginalState = @{} } GitHubSummary = "" StepOutputs = @{} IsPrerelease = $isPrerelease PushToRemote = $PushToRemote.IsPresent Repository = $RepositoryPath } } process { try { Push-Location $RepositoryPath # Step 1: Create GitHub Draft Release (if not skipped) if (-not $SkipGitHubRelease) { Write-SafeLog "INFO" "Step 1: Creating GitHub draft release" "Version: $normalizedVersion" $result.StepResults.DraftCreation.Timestamp = Get-Date if ($PSCmdlet.ShouldProcess($normalizedVersion, "Create GitHub draft release")) { # Real execution $draftResult = New-GitHubDraftRelease -Version $normalizedVersion -ReleaseNotes $ReleaseNotes -ReleaseNotesFile $ReleaseNotesFile -RepositoryPath $RepositoryPath $result.GitHubReleaseResult = $draftResult if ($draftResult.Success) { $result.ReleaseDraftCreated = $true $result.ReleaseId = $draftResult.ReleaseId $result.ReleaseUrl = $draftResult.HtmlUrl $result.RollbackInfo.ReleaseToDelete = $draftResult.ReleaseId $result.StepResults.DraftCreation.Success = $true $result.StepResults.DraftCreation.Message = "Draft release created successfully" Write-SafeLog "INFO" "Draft release created successfully" "ReleaseId: $($draftResult.ReleaseId)" } else { throw "Failed to create draft release: $($draftResult.ErrorMessage)" } } else { # WhatIf simulation $mockDraftResult = [PSCustomObject]@{ Success = $true ReleaseId = "simulated-draft-id-12345" HtmlUrl = "https://github.com/owner/repo/releases/tag/$normalizedVersion" IsDraft = $true IsPrerelease = $isPrerelease ErrorMessage = "" } $result.GitHubReleaseResult = $mockDraftResult $result.ReleaseDraftCreated = $true $result.ReleaseId = $mockDraftResult.ReleaseId $result.ReleaseUrl = $mockDraftResult.HtmlUrl $result.RollbackInfo.ReleaseToDelete = $mockDraftResult.ReleaseId $result.StepResults.DraftCreation.Success = $true $result.StepResults.DraftCreation.Message = "Draft release would be created successfully (WhatIf)" Write-SafeLog "INFO" "Draft release simulation successful" "ReleaseId: $($mockDraftResult.ReleaseId) (WhatIf)" } } else { $result.StepResults.DraftCreation.Success = $true $result.StepResults.DraftCreation.Message = "Skipped (SkipGitHubRelease specified)" } # Step 2: Create Smart Tags (only if draft successful or skipped) if ($result.StepResults.DraftCreation.Success) { Write-SafeLog "INFO" "Step 2: Creating smart tags" "Version: $normalizedVersion" $result.StepResults.TagCreation.Timestamp = Get-Date if ($WhatIfPreference) { # WhatIf simulation for tag creation $mockTagResult = [PSCustomObject]@{ Success = $true TagsCreated = @("$normalizedVersion", "latest", "v0.1") TagsMovedFrom = @{ "latest" = "v0.1.0"; "v0.1" = "v0.1.0" } TagsStaticized = @() BumpType = "Patch" ErrorMessage = "" } $result.GitTagsResult = $mockTagResult $result.TagsCreated = $mockTagResult.TagsCreated $result.TagsMovedFrom = $mockTagResult.TagsMovedFrom $result.TagsStaticized = $mockTagResult.TagsStaticized $result.BumpType = $mockTagResult.BumpType $result.RollbackInfo.TagsToDelete = $mockTagResult.TagsCreated $result.RollbackInfo.TagsToRestore = $mockTagResult.TagsMovedFrom $result.StepResults.TagCreation.Success = $true $result.StepResults.TagCreation.Message = "Smart tags would be created successfully (WhatIf)" Write-SafeLog "INFO" "Smart tags simulation successful" "Tags: $($mockTagResult.TagsCreated -join ', ') (WhatIf)" } else { # Real execution $tagResult = New-SemanticReleaseTags -TargetVersion $normalizedVersion -RepositoryPath $RepositoryPath -Force:$Force -PushToRemote:$PushToRemote $result.GitTagsResult = $tagResult if ($tagResult.Success) { $result.TagsCreated = $tagResult.TagsCreated $result.TagsMovedFrom = $tagResult.TagsMovedFrom $result.TagsStaticized = $tagResult.TagsStaticized $result.BumpType = $tagResult.BumpType $result.RollbackInfo.TagsToDelete = $tagResult.TagsCreated $result.RollbackInfo.TagsToRestore = $tagResult.TagsMovedFrom $result.StepResults.TagCreation.Success = $true $result.StepResults.TagCreation.Message = "Smart tags created successfully" Write-SafeLog "INFO" "Smart tags created successfully" "Tags: $($tagResult.TagsCreated -join ', ')" } else { throw "Failed to create smart tags: $($tagResult.ErrorMessage)" } } } # Step 3: Publish GitHub Release (only if tags successful and release exists) if ($result.StepResults.TagCreation.Success -and $result.ReleaseDraftCreated) { Write-SafeLog "INFO" "Step 3: Publishing GitHub release" "ReleaseId: $($result.ReleaseId)" $result.StepResults.ReleasePublication.Timestamp = Get-Date if ($PSCmdlet.ShouldProcess($result.ReleaseId, "Publish GitHub release")) { # Real execution $publishResult = Publish-GitHubRelease -ReleaseId $result.ReleaseId -MarkAsLatest:(-not $isPrerelease) -RepositoryPath $RepositoryPath if ($publishResult.Success) { $result.ReleasePublished = $true $result.StepResults.ReleasePublication.Success = $true $result.StepResults.ReleasePublication.Message = "Release published successfully" $result.RollbackInfo.ReleaseToDelete = $null # Don't delete published releases Write-SafeLog "INFO" "Release published successfully" "ReleaseId: $($result.ReleaseId)" } else { $result.ConflictsResolved += "Release publication failed but draft and tags exist: $($publishResult.ErrorMessage)" $result.StepResults.ReleasePublication.Message = "Publication failed: $($publishResult.ErrorMessage)" } } else { # WhatIf simulation $result.ReleasePublished = $true $result.StepResults.ReleasePublication.Success = $true $result.StepResults.ReleasePublication.Message = "Release would be published successfully (WhatIf)" $result.RollbackInfo.ReleaseToDelete = $null # Don't delete published releases in simulation Write-SafeLog "INFO" "Release publication simulation successful" "ReleaseId: $($result.ReleaseId) (WhatIf)" } } # Determine overall success $result.Success = $result.StepResults.TagCreation.Success -and ($SkipGitHubRelease -or $result.StepResults.ReleasePublication.Success) $stopwatch.Stop() $result.Duration = $stopwatch.Elapsed # Generate GitHub Summary and Step Outputs $result.GitHubSummary = New-SmartReleaseStepSummary -Result $result $result.StepOutputs = ConvertTo-SmartReleaseStepOutputs -Result $result Write-SafeLog "INFO" "Smart release completed" "Success: $($result.Success), Duration: $($result.Duration.TotalSeconds)s" } catch { $stopwatch.Stop() $result.Duration = $stopwatch.Elapsed $result.Success = $false $result.GitHubSummary = "❌ **Smart Release Failed**`n`nError: $($_.Exception.Message)`n`nSee rollback information for cleanup steps." # Rollback on failure if ($result.RollbackInfo.ReleaseToDelete) { Write-SafeLog "WARN" "Rolling back: Deleting draft release" "ReleaseId: $($result.RollbackInfo.ReleaseToDelete)" try { Remove-GitHubRelease -ReleaseId $result.RollbackInfo.ReleaseToDelete -RepositoryPath $RepositoryPath $result.ConflictsResolved += "Rolled back: Deleted draft release $($result.RollbackInfo.ReleaseToDelete)" } catch { $result.ConflictsResolved += "Rollback failed: Could not delete draft release $($result.RollbackInfo.ReleaseToDelete)" } } Write-SafeLog "ERROR" "Smart release failed" "Error: $($_.Exception.Message)" throw } finally { Pop-Location } } end { return $result } } function New-GitHubDraftRelease { <# .SYNOPSIS Creates a GitHub Release as draft using GitHub CLI with comprehensive error handling. .DESCRIPTION Creates a GitHub release in draft mode as the first step of the proven release strategy. Draft releases are safe and reversible, allowing for validation before publication. .PARAMETER Version The semantic version for the release. .PARAMETER RepositoryPath Path to the Git repository. .PARAMETER ReleaseNotes Custom release notes content. .PARAMETER ReleaseNotesFile Path to a file containing release notes. .PARAMETER Title Custom release title. Defaults to version-based title. .EXAMPLE $result = New-GitHubDraftRelease -Version "v1.2.3" #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Version, [Parameter()] [string]$RepositoryPath = (Get-Location).Path, [Parameter()] [string]$ReleaseNotes, [Parameter()] [string]$ReleaseNotesFile, [Parameter()] [string]$Title ) begin { Write-SafeLog "INFO" "Creating GitHub draft release" "Version: $Version" $result = [PSCustomObject]@{ Version = $Version Success = $false ReleaseId = $null HtmlUrl = "" IsDraft = $true IsPrerelease = $Version -match '-(alpha|beta|rc|preview|pre)' Title = $Title ErrorMessage = "" CreatedAt = $null GitHubSummary = "" StepOutputs = @{} } } process { try { Push-Location $RepositoryPath # Validate GitHub CLI $ghVersion = gh version 2>$null if ($LASTEXITCODE -ne 0) { throw "GitHub CLI (gh) is not available. Please install GitHub CLI and authenticate." } # Prepare release title if (-not $Title) { $Title = if ($result.IsPrerelease) { "🧪 Prerelease $Version" } else { "🚀 Release $Version" } } $result.Title = $Title # Prepare release notes $notesArgs = @() if ($ReleaseNotesFile -and (Test-Path $ReleaseNotesFile)) { $notesArgs += "--notes-file", $ReleaseNotesFile } elseif ($ReleaseNotes) { $notesArgs += "--notes", $ReleaseNotes } else { $notesArgs += "--generate-notes" } # Create draft release $ghArgs = @( "release", "create", $Version "--title", $Title "--draft" ) + $notesArgs if ($result.IsPrerelease) { $ghArgs += "--prerelease" } Write-SafeLog "DEBUG" "Executing GitHub CLI" "Command: gh $($ghArgs -join ' ')" $output = & gh @ghArgs 2>&1 if ($LASTEXITCODE -eq 0) { # Extract release URL from output $result.HtmlUrl = $output | Where-Object { $_ -match "https://github.com/.+/releases/" } | Select-Object -First 1 if (-not $result.HtmlUrl) { $result.HtmlUrl = $output -join "`n" } # Get release details $releaseDetails = gh release view $Version --json id,htmlUrl,isDraft,createdAt 2>$null | ConvertFrom-Json if ($releaseDetails) { $result.ReleaseId = $releaseDetails.id $result.HtmlUrl = $releaseDetails.htmlUrl $result.IsDraft = $releaseDetails.isDraft $result.CreatedAt = $releaseDetails.createdAt } $result.Success = $true Write-SafeLog "INFO" "Draft release created successfully" "ReleaseId: $($result.ReleaseId), URL: $($result.HtmlUrl)" } else { $result.ErrorMessage = $output -join "`n" throw "GitHub CLI failed: $($result.ErrorMessage)" } # Generate outputs $result.GitHubSummary = "✅ **Draft Release Created**: [$Version]($($result.HtmlUrl))" $result.StepOutputs = @{ "draft-success" = "true" "release-id" = $result.ReleaseId "release-url" = $result.HtmlUrl "is-draft" = $result.IsDraft.ToString().ToLower() "is-prerelease" = $result.IsPrerelease.ToString().ToLower() } } catch { $result.Success = $false $result.ErrorMessage = $_.Exception.Message $result.GitHubSummary = "❌ **Draft Release Failed**: $($_.Exception.Message)" $result.StepOutputs = @{ "draft-success" = "false" "error-message" = $_.Exception.Message } Write-SafeLog "ERROR" "Failed to create draft release" "Error: $($_.Exception.Message)" throw } finally { Pop-Location } } end { return $result } } function Publish-GitHubRelease { <# .SYNOPSIS Publishes a GitHub draft release using GitHub CLI. .PARAMETER ReleaseId The GitHub release ID to publish. .PARAMETER RepositoryPath Path to the Git repository. .PARAMETER MarkAsLatest Mark this release as the latest release. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ReleaseId, [Parameter()] [string]$RepositoryPath = (Get-Location).Path, [Parameter()] [switch]$MarkAsLatest ) begin { Write-SafeLog "INFO" "Publishing GitHub release" "ReleaseId: $ReleaseId" $result = [PSCustomObject]@{ ReleaseId = $ReleaseId Success = $false Published = $false MarkedAsLatest = $false PublishedAt = $null ErrorMessage = "" } } process { try { Push-Location $RepositoryPath # Get current release info $releaseInfo = gh release view $ReleaseId --json tagName 2>$null | ConvertFrom-Json if (-not $releaseInfo) { throw "Release with ID $ReleaseId not found" } $tagName = $releaseInfo.tagName # Publish release if ($MarkAsLatest) { gh release edit $tagName --draft=false --latest } else { gh release edit $tagName --draft=false } if ($LASTEXITCODE -eq 0) { $result.Success = $true $result.Published = $true $result.MarkedAsLatest = $MarkAsLatest.IsPresent $result.PublishedAt = Get-Date Write-SafeLog "INFO" "Release published successfully" "ReleaseId: $ReleaseId, Latest: $($MarkAsLatest.IsPresent)" } else { throw "Failed to publish release" } } catch { $result.Success = $false $result.ErrorMessage = $_.Exception.Message Write-SafeLog "ERROR" "Failed to publish release" "Error: $($_.Exception.Message)" throw } finally { Pop-Location } } end { return $result } } function Remove-GitHubRelease { <# .SYNOPSIS Removes a GitHub release using GitHub CLI. .PARAMETER ReleaseId The GitHub release ID to remove. .PARAMETER RepositoryPath Path to the Git repository. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$ReleaseId, [Parameter()] [string]$RepositoryPath = (Get-Location).Path ) try { Push-Location $RepositoryPath # Get release tag name $releaseInfo = gh release view $ReleaseId --json tagName 2>$null | ConvertFrom-Json if ($releaseInfo) { gh release delete $releaseInfo.tagName --yes if ($LASTEXITCODE -eq 0) { Write-SafeLog "INFO" "Release deleted successfully" "ReleaseId: $ReleaseId" return $true } } Write-SafeLog "WARN" "Failed to delete release" "ReleaseId: $ReleaseId" return $false } catch { Write-SafeLog "ERROR" "Error deleting release" "ReleaseId: $ReleaseId, Error: $($_.Exception.Message)" return $false } finally { Pop-Location } } # Helper functions for Smart Release workflow function New-SmartReleaseStepSummary { <# .SYNOPSIS Generates comprehensive GitHub step summary for Smart Release operations. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject]$Result ) $statusIcon = if ($Result.Success) { "✅" } else { "❌" } $summary = @" ### $statusIcon **Smart Release Results** | Property | Value | |----------|-------| | **Target Version** | ``$($Result.TargetVersion)`` | | **Bump Type** | $($Result.BumpType) | | **Overall Success** | $(if($Result.Success){"✅ Yes"}else{"❌ No"}) | | **Duration** | $([math]::Round($Result.Duration.TotalSeconds, 2))s | | **Is Prerelease** | $(if($Result.IsPrerelease){"🧪 Yes"}else{"🚀 No"}) | #### 🏗️ **Workflow Steps** | Step | Status | Timestamp | Message | |------|--------|-----------|---------| $(foreach($step in $Result.StepResults.GetEnumerator()) { $status = if($step.Value.Success) {"✅"} else {"❌"} $timestamp = if($step.Value.Timestamp) {$step.Value.Timestamp.ToString("HH:mm:ss")} else {"-"} "| $($step.Key) | $status | $timestamp | $($step.Value.Message) |" }) #### 🏷️ **Git Tags Created** $(if ($Result.TagsCreated.Count -gt 0) { ($Result.TagsCreated | ForEach-Object { "- ``$_``" }) -join "`n" } else { "- *No tags created*" }) #### 🔄 **Tag Movements** $(if ($Result.TagsMovedFrom.Count -gt 0) { ($Result.TagsMovedFrom.GetEnumerator() | ForEach-Object { "- 🔄 ``$($_.Key)``: ``$($_.Value)`` → ``$($Result.TargetVersion)``" }) -join "`n" } else { "- *No tag movements*" }) $(if ($Result.ReleaseUrl) { @" #### 🚀 **GitHub Release** - **Status**: $(if($Result.ReleasePublished){"✅ Published"}elseif($Result.ReleaseDraftCreated){"📝 Draft Created"}else{"❌ Failed"}) - **URL**: [$($Result.TargetVersion)]($($Result.ReleaseUrl)) - **Release ID**: ``$($Result.ReleaseId)`` "@ }) $(if ($Result.ConflictsResolved.Count -gt 0) { @" #### ⚠️ **Issues Resolved** $(($Result.ConflictsResolved | ForEach-Object { "- ⚠️ $_" }) -join "`n") "@ }) <details> <summary>🔧 <strong>Technical Details</strong></summary> **Repository:** ``$($Result.Repository)`` **Operation:** Smart Release (Draft → Tags → Publish) **Timestamp:** $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC') **Push to Remote:** $(if($Result.PushToRemote){"✅ Yes"}else{"❌ No"}) $(if ($Result.RollbackInfo.TagsToDelete.Count -gt 0 -or $Result.RollbackInfo.ReleaseToDelete) { @" **Rollback Info:** $(if($Result.RollbackInfo.TagsToDelete.Count -gt 0){"- Tags to clean up: $($Result.RollbackInfo.TagsToDelete -join ', ')"}) $(if($Result.RollbackInfo.ReleaseToDelete){"- Release to clean up: $($Result.RollbackInfo.ReleaseToDelete)"}) - Original state preserved: ✅ "@ }) </details> "@ return $summary } function ConvertTo-SmartReleaseStepOutputs { <# .SYNOPSIS Converts Smart Release results to GitHub Actions step outputs. #> [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline)] [PSCustomObject]$Result ) $outputs = @{ "success" = $Result.Success.ToString().ToLower() "target-version" = $Result.TargetVersion "bump-type" = $Result.BumpType.ToLower() "is-prerelease" = $Result.IsPrerelease.ToString().ToLower() "duration-seconds" = [math]::Round($Result.Duration.TotalSeconds, 2) # Git tag outputs "tags-created" = ($Result.TagsCreated -join ',') "tags-created-count" = $Result.TagsCreated.Count # GitHub release outputs "release-draft-created" = $Result.ReleaseDraftCreated.ToString().ToLower() "release-published" = $Result.ReleasePublished.ToString().ToLower() "release-id" = $Result.ReleaseId "release-url" = $Result.ReleaseUrl # Workflow step statuses "draft-step-success" = $Result.StepResults.DraftCreation.Success.ToString().ToLower() "tags-step-success" = $Result.StepResults.TagCreation.Success.ToString().ToLower() "publish-step-success" = $Result.StepResults.ReleasePublication.Success.ToString().ToLower() } # Add optional outputs if ($Result.TagsMovedFrom.Count -gt 0) { $outputs["tags-moved"] = ($Result.TagsMovedFrom.Keys -join ',') $outputs["tags-moved-count"] = $Result.TagsMovedFrom.Count } if ($Result.ConflictsResolved.Count -gt 0) { $outputs["issues-resolved"] = $Result.ConflictsResolved.Count $outputs["has-issues"] = "true" } else { $outputs["has-issues"] = "false" } return $outputs } |