lib/rules/releases/patch_release_required/patch_release_required.ps1

#############################################################################
# Rule: patch_release_required
# Category: releases
# Priority: 10
#############################################################################

# Load shared release helpers
. "$PSScriptRoot/../ReleaseRulesHelper.ps1"

$Rule_PatchReleaseRequired = [ValidationRule]@{
    Name = "patch_release_required"
    Description = "Patch versions must have GitHub Releases when check-releases is enabled"
    Priority = 10
    Category = "releases"
    
    Condition = { param([RepositoryState]$State, [hashtable]$Config)
        # Only apply when check-releases is enabled
        $checkReleases = $Config.'check-releases'
        if ($checkReleases -ne 'error' -and $checkReleases -ne 'warning') {
            return @()
        }
        
        # Track versions we've already added to avoid duplicates
        $seenVersions = @{}
        $results = @()
        
        # Get all patch versions from both tags and branches
        $allPatches = ($State.Tags + $State.Branches) | Where-Object { $_.IsPatch }
        
        # 1. Find existing patch tags without releases
        $existingPatchesWithoutRelease = $allPatches | Where-Object {
            $version = $_.Version
            
            # Skip ignored versions
            if ($_.IsIgnored) {
                return $false
            }
            
            # Check if release exists
            $release = $State.Releases | Where-Object { $_.TagName -eq $version }
            return $null -eq $release
        }
        
        foreach ($patch in $existingPatchesWithoutRelease) {
            if (-not $seenVersions.ContainsKey($patch.Version)) {
                $seenVersions[$patch.Version] = $true
                $results += $patch
            }
        }
        
        # 2. Find expected patch versions from floating versions (e.g., v1 exists but v1.0.0 doesn't)
        $floatingVersions = ($State.Tags + $State.Branches) | Where-Object { 
            -not $_.IsPatch -and $_.Version -ne 'latest' 
        }
        
        foreach ($floatingRef in $floatingVersions) {
            $version = $floatingRef.Version
            
            # Skip ignored versions
            if ($floatingRef.IsIgnored) {
                continue
            }
            
            # Determine expected patch version
            $expectedPatchVersion = $null
            if ($floatingRef.IsMajor) {
                # For v1, expect v1.0.0
                $expectedPatchVersion = "v$($floatingRef.Major).0.0"
            } elseif ($floatingRef.IsMinor) {
                # For v1.0, expect v1.0.0
                $expectedPatchVersion = "v$($floatingRef.Major).$($floatingRef.Minor).0"
            }
            
            if ($expectedPatchVersion) {
                # Skip if we've already added this version (from existing patch or another floating)
                if ($seenVersions.ContainsKey($expectedPatchVersion)) {
                    continue
                }
                
                # Check if this patch version already exists
                $existingPatch = $allPatches | Where-Object { $_.Version -eq $expectedPatchVersion }
                
                # Check if release exists for this expected version
                $release = $State.Releases | Where-Object { $_.TagName -eq $expectedPatchVersion }
                
                # If patch doesn't exist OR patch exists but release doesn't, create expected entry
                if ($null -eq $existingPatch -or $null -eq $release) {
                    # Check if there's already a draft release that just needs publishing
                    $draftRelease = $State.Releases | Where-Object { 
                        $_.TagName -eq $expectedPatchVersion -and $_.IsDraft 
                    }
                    
                    # Skip if draft exists (publish action will handle it)
                    if ($null -eq $draftRelease) {
                        # Create a synthetic VersionRef for the expected patch
                        # Use a dummy ref path since this version doesn't exist yet
                        $syntheticRef = [VersionRef]::new($expectedPatchVersion, "refs/tags/$expectedPatchVersion", $floatingRef.Sha, "tag")
                        $seenVersions[$expectedPatchVersion] = $true
                        $results += $syntheticRef
                    }
                }
            }
        }
        
        return $results
    }
    
    Check = { param([VersionRef]$VersionRef, [RepositoryState]$State, [hashtable]$Config)
        # If we got here from Condition, the release is missing
        return $false
    }
    
    CreateIssue = { param([VersionRef]$VersionRef, [RepositoryState]$State, [hashtable]$Config)
        $version = $VersionRef.Version
        $severity = if ($Config.'check-releases' -eq 'warning') { 'warning' } else { 'error' }
        
        # Determine if we should auto-publish (make immutable)
        $checkImmutability = $Config.'check-release-immutability'
        $shouldAutoPublish = ($checkImmutability -eq 'error' -or $checkImmutability -eq 'warning')
        
        $issue = [ValidationIssue]::new(
            "missing_release",
            $severity,
            "Release required for patch version $version"
        )
        $issue.Version = $version
        
        # CreateReleaseAction constructor: tagName, isDraft, autoPublish, targetSha
        # isDraft should be opposite of shouldAutoPublish
        $isDraft = -not $shouldAutoPublish
        $action = [CreateReleaseAction]::new($version, $isDraft, $shouldAutoPublish, $VersionRef.Sha)
        
        # Determine if this release should become "latest"
        # Only set MakeLatest=false explicitly if it should NOT be latest
        # to prevent overwriting a correct latest release
        $shouldBeLatest = Test-ShouldBeLatestRelease -State $State -Version $version
        if (-not $shouldBeLatest) {
            $action.MakeLatest = $false
        }
        
        $issue.RemediationAction = $action
        
        return $issue
    }
}

# Export the rule
$Rule_PatchReleaseRequired