lib/rules/releases/ReleaseRulesHelper.ps1

#############################################################################
# ReleaseRulesHelper.ps1 - Shared helper functions for release rules
#############################################################################

<#
.SYNOPSIS
    Determines if a release should be marked as "latest" when created or published.
 
.DESCRIPTION
    Checks if the given version should become the "latest" release based on:
    1. It must be a valid patch version (vX.Y.Z)
    2. It must not be a prerelease (checked via ReleaseInfo if available)
    3. It must be the highest non-prerelease, non-ignored patch version
 
.PARAMETER State
    The RepositoryState object containing all releases and tags.
 
.PARAMETER Version
    The version string to check (e.g., "v1.0.0").
 
.PARAMETER ReleaseInfo
    Optional ReleaseInfo object for the release being created/published.
    Used to check prerelease status.
 
.OUTPUTS
    $true if the release should become latest, $false otherwise.
#>

function Test-ShouldBeLatestRelease {
    param(
        [Parameter(Mandatory)]
        [RepositoryState]$State,
        [Parameter(Mandatory)]
        [string]$Version,
        [Parameter(Mandatory = $false)]
        [ReleaseInfo]$ReleaseInfo = $null
    )
    
    # Must be a valid patch version
    if ($Version -notmatch '^v(\d+)\.(\d+)\.(\d+)$') {
        return $false
    }
    
    $targetMajor = [int]$Matches[1]
    $targetMinor = [int]$Matches[2]
    $targetPatch = [int]$Matches[3]
    
    # If this release is a prerelease, it should NOT become latest
    if ($ReleaseInfo -and $ReleaseInfo.IsPrerelease) {
        return $false
    }
    
    # Find all existing published, non-prerelease, non-ignored patch releases
    $eligibleReleases = $State.Releases | Where-Object {
        -not $_.IsDraft -and
        -not $_.IsPrerelease -and
        -not $_.IsIgnored -and
        $_.TagName -match '^v(\d+)\.(\d+)\.(\d+)$'
    }
    
    # Parse versions and find the highest existing release
    $highestExisting = $null
    $highestMajor = -1
    $highestMinor = -1
    $highestPatch = -1
    
    foreach ($release in $eligibleReleases) {
        if ($release.TagName -match '^v(\d+)\.(\d+)\.(\d+)$') {
            $major = [int]$Matches[1]
            $minor = [int]$Matches[2]
            $patch = [int]$Matches[3]
            
            if ($major -gt $highestMajor -or
                ($major -eq $highestMajor -and $minor -gt $highestMinor) -or
                ($major -eq $highestMajor -and $minor -eq $highestMinor -and $patch -gt $highestPatch)) {
                $highestMajor = $major
                $highestMinor = $minor
                $highestPatch = $patch
                $highestExisting = $release
            }
        }
    }
    
    # Compare target version against highest existing
    # If target is higher, it should become latest
    if ($null -eq $highestExisting) {
        # No existing eligible releases, this should become latest
        return $true
    }
    
    if ($targetMajor -gt $highestMajor -or
        ($targetMajor -eq $highestMajor -and $targetMinor -gt $highestMinor) -or
        ($targetMajor -eq $highestMajor -and $targetMinor -eq $highestMinor -and $targetPatch -gt $highestPatch)) {
        # Target is higher than any existing release
        return $true
    }
    
    # Target is not the highest, should NOT become latest
    return $false
}

<#
.SYNOPSIS
    Gets the IDs of duplicate releases that should be deleted.
 
.DESCRIPTION
    Identifies duplicate releases (multiple releases for the same patch version tag)
    and returns the IDs of releases that should be deleted. The "best" release is kept
    based on these criteria (in order):
    1. Published releases are preferred over drafts
    2. Immutable releases are preferred over mutable
    3. Older releases (lower ID) are preferred over newer
 
.PARAMETER State
    The RepositoryState object containing all releases.
 
.OUTPUTS
    An array of release IDs that are duplicates and should be deleted.
#>

function Get-DuplicateReleaseId {
    param(
        [Parameter(Mandatory)]
        [RepositoryState]$State
    )
    
    $duplicateReleaseIds = @()
    
    # Get all patch releases (not ignored)
    $patchReleases = $State.Releases | Where-Object {
        -not $_.IsIgnored -and $_.TagName -match '^v\d+\.\d+\.\d+$'
    }
    
    # Group by tag name to find duplicates
    $releasesByTag = $patchReleases | Group-Object -Property TagName
    
    foreach ($group in $releasesByTag) {
        if ($group.Count -gt 1) {
            $releases = $group.Group
            
            # Sort to find which release 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 }
            )
            
            # Mark all but the first as duplicates
            $duplicates = $sortedReleases | Select-Object -Skip 1
            $duplicateReleaseIds += $duplicates.Id
        }
    }
    
    return $duplicateReleaseIds
}

<#
.SYNOPSIS
    Gets duplicate draft releases that can be deleted.
 
.DESCRIPTION
    Returns the ReleaseInfo objects for duplicate draft releases that should be deleted.
    Only draft releases are returned since published/immutable releases cannot be deleted.
 
.PARAMETER State
    The RepositoryState object containing all releases.
 
.OUTPUTS
    An array of ReleaseInfo objects for duplicate drafts that should be deleted.
#>

function Get-DuplicateDraftRelease {
    param(
        [Parameter(Mandatory)]
        [RepositoryState]$State
    )
    
    $duplicatesToDelete = @()
    
    # Get all patch releases (not ignored)
    $patchReleases = $State.Releases | Where-Object {
        -not $_.IsIgnored -and $_.TagName -match '^v\d+\.\d+\.\d+$'
    }
    
    # Group by tag name to find duplicates
    $releasesByTag = $patchReleases | Group-Object -Property TagName
    
    foreach ($group in $releasesByTag) {
        if ($group.Count -gt 1) {
            $releases = $group.Group
            
            # Sort to find which release to keep
            $sortedReleases = $releases | Sort-Object -Property @(
                @{ Expression = { -not $_.IsDraft }; Descending = $true }
                @{ Expression = { $_.IsImmutable }; Descending = $true }
                @{ Expression = { $_.Id }; Ascending = $true }
            )
            
            # Get duplicates (all but the first), but only drafts can be deleted
            $duplicates = $sortedReleases | Select-Object -Skip 1 | Where-Object { $_.IsDraft }
            $duplicatesToDelete += $duplicates
        }
    }
    
    return $duplicatesToDelete
}