Functions/Update-ModuleVersion.ps1

Function Update-ModuleVersion {
    <#
        .SYNOPSIS
            Updates the version number of a PowerShell module based on detected changes
 
        .DESCRIPTION
            This function analyzes a PowerShell module's fingerprint (commands and parameters) to determine if the version should be incremented.
            It detects breaking changes (major version), new features (minor version), or bug fixes (patch version) and updates the module manifest accordingly.
 
        .PARAMETER ModulePath
            The path to the module directory or manifest file that should be updated
 
        .PARAMETER Ask
            When specified, displays what version update would occur without making any changes
 
        .PARAMETER Patch
            Forces a patch version increment (0.0.X) regardless of detected changes
 
        .EXAMPLE
            Update-ModuleVersion -ModulePath "C:\Modules\MyModule"
 
            Analyzes the module and updates the version based on detected changes
 
        .EXAMPLE
            Update-ModuleVersion -ModulePath "C:\Modules\MyModule" -Ask
 
            Shows what version update would occur without making changes
 
        .EXAMPLE
            Update-ModuleVersion -ModulePath "C:\Modules\MyModule" -Patch -Confirm:$false
 
            Forces a patch version update without confirmation
 
    #>


    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'High')]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$ModulePath,

        [Switch]$Ask,
        [Switch]$Patch
    )

    BEGIN{
        Write-Verbose "#################################################################"
        Write-Verbose "Beginning $($MyInvocation.MyCommand.Name) on $($ENV:ComputerName) @ $(Get-Date -Format 'yyyy.MM.dd HH:mm:ss')"
        Write-Verbose "#################################################################"
    }

    PROCESS{
        TRY{
            # Ensure ModulePath is a directory
            IF((Get-Item $ModulePath).PSIsContainer -ne $True) {
                $ModulePath = (Get-Item $ModulePath).DirectoryName
            }
            $ModuleName = $ModulePath.TrimEnd('\').Split('\')[-1]
            $ManifestPath = Get-ChildItem -Path $ModulePath -Filter "$ModuleName.psd1" -Recurse -ErrorAction Stop | Select-Object -ExpandProperty FullName

            # Check if this function is part of the module being updated
            $CurrentModule = (Get-Command -Name $MyInvocation.MyCommand.Name).Module.Name
            IF($CurrentModule -eq $ModuleName) {
                Write-Verbose "This function is part of the module $ModuleName. Skipping module unloading."
            }
            else {
                Write-Verbose ("Importing {0}" -f $ModuleName)
                Import-Module -Name $ManifestPath -Force
                $CommandList = Get-Command -Module $ModuleName
                Write-Verbose ("Removing {0}" -f $ModuleName)
                Remove-Module -Name $ModuleName -Force
            }

            Write-Output 'Calculating fingerprint'
            $Fingerprint = @()

            # Calculate fingerprint for commands and parameters
            ForEach ($Command in $CommandList) {
                ForEach ($Parameter in $Command.Parameters.Keys) {
                    $Fingerprint += '{0}:{1}' -f $Command.Name, $Command.Parameters[$Parameter].Name
                    $Command.Parameters[$Parameter].Aliases | ForEach-Object {
                        $Fingerprint += '{0}:{1}' -f $Command.Name, $_
                    }
                }
            }

            ## There's a to do here to figure out a way to check for changes to txt files
            <# # #Calculate fingerprint for .txt files
            $TextFiles = Get-ChildItem -Path $ModulePath -Filter '*.txt' -Recurse -File -ErrorAction Continue
            ForEach ($File in $TextFiles) {
                $FileContent = Get-Content -Path $File.FullName -Raw
                $FileHash = [System.BitConverter]::ToString((New-Object System.Security.Cryptography.SHA256Managed).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($FileContent))).Replace("-", "")
                $txtFingerprint += '{0}:{1}' -f $File.Name, $FileHash
            } #>


            $Manifest = Import-PowerShellDataFile -Path $ManifestPath
            [version]$Version = $Manifest.ModuleVersion

            $MinorFeature = 0
            $MajorFeature = 0
            $VersionType = $Null

            IF($Patch) {
                $VersionType = 'Patch'
                [version]$NewVersion = "{0}.{1}.{2}" -f $Version.Major, $Version.Minor, ($Version.Build + 1)
            }
            ELSEIF([string]::IsNullOrEmpty($Fingerprint)) {
                $VersionType = 'Patch'
                [version]$NewVersion = "{0}.{1}.{2}" -f $Version.Major, $Version.Minor, ($Version.Build + 1)
            }
            else {
                # .NET alternative: $FingerprintPath = [System.IO.Path]::Combine($ModulePath, 'fingerprint')
                # .NET alternative: $OldFingerprint = IF([System.IO.File]::Exists($FingerprintPath)) { [System.IO.File]::ReadAllLines($FingerprintPath) }
                $OldFingerprint = IF(Test-Path -Path (Join-Path $ModulePath 'fingerprint')) { Get-Content -Path (Join-Path $ModulePath 'fingerprint') }
                else {
                    Write-Verbose "No Fingerprint found, saving current fingerprint"
                    $Fingerprint
                }

                IF(Compare-Object -ReferenceObject $OldFingerprint -DifferenceObject $Fingerprint) {
                    Write-Output 'Detecting new features'
                    $Fingerprint | Where-Object { $_ -notin $OldFingerprint } | ForEach-Object { $MinorFeature++ }
                    IF($MinorFeature -ge 1) {
                        $VersionType = 'Minor'
                        [version]$NewVersion = "{0}.{1}.{2}" -f $Version.Major, ($Version.Minor + 1), 0
                    }

                    Write-Output 'Detecting breaking changes'
                    $OldFingerprint | Where-Object { $_ -notin $Fingerprint } | ForEach-Object { $MajorFeature++ }
                    IF($MajorFeature -ge 1) {
                        $VersionType = 'Major'
                        [version]$NewVersion = "{0}.{1}.{2}" -f ($Version.Major + 1), 0, 0
                    }
                }

                IF($PSCmdlet.ShouldProcess("Fingerprint will be saved")) {
                    # .NET alternative: [System.IO.File]::WriteAllLines($FingerprintPath, $Fingerprint)
                    Set-Content -Path (Join-Path $ModulePath 'fingerprint') -Value $Fingerprint
                }
            }

            IF($Ask) {
                Write-Output "$(Join-Path $ModulePath "$ModuleName.psd1") would have been updated by $VersionType"
            }
            ELSEIF($VersionType) {
                IF($PSCmdlet.ShouldProcess("$ModulePath\$ModuleName.psd1 will be updated by $VersionType")) {
                    Update-ModuleManifest -Path $ManifestPath -ModuleVersion $NewVersion
                }
            }
        }
        CATCH{
            Write-Error "An error occurred: $_"
        }
    }
    END{
        IF($Version -ne $NewVersion) {
            Write-Output "Module $ModuleName Updated from $Version to $NewVersion"
        }
    }
}