lib/rules/releases/duplicate_release/duplicate_release.ps1

#############################################################################
# Rule: duplicate_release
# Category: releases
# Priority: 9
#############################################################################

$Rule_DuplicateRelease = [ValidationRule]@{
    Name = "duplicate_release"
    Description = "Delete duplicate draft releases that point to the same tag as another release"
    Priority = 9
    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 @()
        }
        
        $duplicatesToDelete = @()
        
        # Group releases by tag name (only patch versions)
        $patchReleases = $State.Releases | Where-Object {
            -not $_.IsIgnored -and $_.TagName -match '^v\d+\.\d+\.\d+$'
        }
        
        $releasesByTag = $patchReleases | Group-Object -Property TagName
        
        foreach ($group in $releasesByTag) {
            if ($group.Count -gt 1) {
                # Multiple releases for the same tag - find duplicates to delete
                $releases = $group.Group
                
                # Sort releases to determine which one to keep:
                # 1. Prefer published (non-draft) over draft
                # 2. Prefer immutable over mutable
                # 3. Keep the one with the lowest ID (oldest)
                $sortedReleases = $releases | Sort-Object -Property @(
                    @{ Expression = { -not $_.IsDraft }; Descending = $true }
                    @{ Expression = { $_.IsImmutable }; Descending = $true }
                    @{ Expression = { $_.Id }; Ascending = $true }
                )
                
                # Keep the first one (best candidate), mark others as duplicates
                # The best candidate is selected by the sort order above
                $duplicates = $sortedReleases | Select-Object -Skip 1
                
                foreach ($duplicate in $duplicates) {
                    # Only delete draft duplicates - immutable/published releases cannot be deleted
                    if ($duplicate.IsDraft) {
                        $duplicatesToDelete += $duplicate
                    }
                }
            }
        }
        
        return $duplicatesToDelete
    }
    
    Check = { param([ReleaseInfo]$ReleaseInfo, [RepositoryState]$State, [hashtable]$Config)
        # If we got here from Condition, the release is a duplicate that should be deleted
        return $false
    }
    
    CreateIssue = { param([ReleaseInfo]$ReleaseInfo, [RepositoryState]$State, [hashtable]$Config)
        $version = $ReleaseInfo.TagName
        $severity = if ($Config.'check-releases' -eq 'warning') { 'warning' } else { 'error' }
        
        $issue = [ValidationIssue]::new(
            "duplicate_release",
            $severity,
            "Duplicate draft release found for $version (release ID: $($ReleaseInfo.Id)) - will be deleted"
        )
        $issue.Version = $version
        
        # DeleteReleaseAction constructor parameters are (tagName, releaseId)
        $issue.RemediationAction = [DeleteReleaseAction]::new($version, $ReleaseInfo.Id)
        
        return $issue
    }
}

# Export the rule
$Rule_DuplicateRelease