DanGough / Nevergreen

This module is an alternative to Evergreen, and allows you to find the latest version and download URL for various Windows apps. Evergreen uses API queries to obtain its data whereas this module is more focussed on web scraping. This is more prone to breaking when websites are changed, hence the name.
The Unlicense
71 stars 16 forks source link

[Enhancement] Microsoft SSMS #65

Open AScott-WWF opened 4 weeks ago

AScott-WWF commented 4 weeks ago

The current Get-MicrosoftSSMS.ps1 script:

$URL64 = (Resolve-Uri -Uri 'https://aka.ms/ssmsfullsetup').Uri
$Version = Get-Version -Uri 'https://docs.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms' -Pattern 'Release number: ((?:\d+\.)+\d+)'
New-NevergreenApp -Name 'Microsoft SSMS' -Version $Version -Uri $URL64 -Architecture 'x64' -Type 'Exe'

only returns the latest new full version in 'English (United States)' language, but does not return any earlier versions. Earlier versions are sometimes still required as Microsoft may still update earlier versions to fix security issues. Earlier versions are required to work with specific components of certain SQL server releases (SSIS for example requires v18.x)

Therefore I have re-written the Get-MicrosoftSSMS.ps1 script to webscrape the release notes page, to return all current released versions.

# Get-SSMS.ps1

# Define AppName
$AppName = "Microsoft SQL Server Management Studio"

# Main script to fetch and process links
$ReleaseURL = "https://learn.microsoft.com/en-us/sql/ssms/release-notes-ssms?view=sql-server-ver16"  # Replace with the URL of the web page you want to process

Write-Verbose "Obtaining $($AppName) Release Versions...`n=============================================================================`n"
# Fetch the HTML content of the webpage
$htmlContent = Invoke-WebRequest -Uri $ReleaseUrl

# Define the RegEx pattern to match the links and capture the version number and BaseURI
$pattern = '<a[^>]*href="([^"]*)"[^>]*>\s*Download SSMS ([\d.]+)\s*</a>'

$SearchCount = ([regex]::Matches($htmlContent.RawContent, $pattern)).Count

Write-Verbose "$($AppName) Release Versions found: $($SearchCount)`n-------------------------------------------------------------------------`n"

do {
    # Use RegEx to find matches
    foreach ($patternmatch in [regex]::Matches($htmlContent.Content, $pattern)) {
        # Extract the URI and version number
        $ThisVersionUri = $patternmatch.Groups[1].Value
        [version]$ThisVersion = $patternmatch.Groups[2].Value
        $BaseVersion = $($ThisVersion.Major)

        # Regular expression to match the release information
        $regex = "<li>Release number: $($ThisVersion)</li>`n<li>Build number: (.*?)</li>`n<li>Release date: (.*?)</li>"

        $BuildCount = ([regex]::Matches($htmlContent.RawContent, $regex)).Count
        foreach ($regexmatch in [regex]::Matches($htmlContent.RawContent, $regex)) {
            #$ReleaseVersion = $regexmatch.Groups[1].Value
            [version]$BuildNumber = $regexmatch.Groups[1].Value
            $ReleaseDate = $regexmatch.Groups[2].Value

            do {
                $AppVersions = @(
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Chinese (Simplified)'; Pattern = '&clcid=0x804'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Chinese (Traditional)'; Pattern = '&clcid=0x404'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'English'; Pattern = '&clcid=0x409'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'French'; Pattern = '&clcid=0x40c'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'German'; Pattern = '&clcid=0x407'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Italian'; Pattern = '&clcid=0x410'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Japanese'; Pattern = '&clcid=0x411'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Korean'; Pattern = '&clcid=0x412'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Portuguese (Brazil)'; Pattern = '&clcid=0x416'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Russian'; Pattern = '&clcid=0x419'}
                    @{Version = $ThisVersion; BuildNumber = $BuildNumber; ReleaseDate = $ReleaseDate; Language = 'Spanish'; Pattern = '&clcid=0x40a'}
                )

                foreach ($AppVersion in $AppVersions) {
                    if ($ThisVersionUri) {
                        #Build each link with Language specific versions
                        $URL = (Resolve-Uri -Uri "$($ThisVersionUri)$($AppVersion.Pattern)").Uri
                        New-NevergreenApp -Name $AppName -Ring $BaseVersion -Version $AppVersion.Version -BuildNumber $AppVersion.BuildNumber -ReleaseDate $AppVersion.ReleaseDate -Uri $URL -Architecture 'x64' -Language $AppVersion.Language -Type 'Exe'
                        #break
                    }
                }
                $BuildCount--
            } until ($BuildCount -eq 0)
        }
        $SearchCount--
    }
} until ($SearchCount -eq 0)

This modified script has also required the modification of the New-NeverGreenApp.ps1 script to specifically handle the BuildNumber and ReleaseDate parameters (To give similar functionality as Evergreen), so here is my updated version:

function New-NevergreenApp {
    <#
    .SYNOPSIS
        Returns a PSCustomObject to output.

    .DESCRIPTION
        Returns a PSCustomObject to output.

    .NOTES
        Site: https://packageology.com
        Author: Dan Gough
        Twitter: @packageologist

    .LINK
        https://github.com/DanGough/Nevergreen

    .PARAMETER Name
        Mandatory. The name of the application.

    .PARAMETER Uri
        Mandatory. The download URI for the application.

    .PARAMETER Version
        Mandatory. The application version.

    .PARAMETER Architecture
        Mandatory. Must match x86, x64, ARM32, ARM64 or Multi.

    .PARAMETER Type
        Mandatory. Must match Msi, Msp, Exe, Zip, Msix, AppX, AppV, 7z if supplied.

    .PARAMETER Language
        Optional. The language of the application installer, e.g. 'en'.

    .PARAMETER Ring
        Optional. The deployment ring, e.g. 'General', 'Preview'.

    .PARAMETER Channel
        Optional. The channel, e.g. 'Enterprise'.

    .PARAMETER Platform
        Optional. The platform, e.g. 'Windows Server'.

    .PARAMETER BuildNumber
        Optional. The Build Version (used where available - example SSMS).

    .PARAMETER ReleaseDate
        Optional. The Date the version was released.

    .PARAMETER MD5
        Optional. The MD5 hash of the file.

    .PARAMETER SHA256
        Optional. The SHA256 hash of the file.

    .EXAMPLE
        New-NevergreenApp -Uri 'http://somewhere.com/something.exe' -Version '1.0' -Architecture 'x64' -Type 'Exe'

        Description:
        Outputs a PSCustomObject with the chosen properties.
    #>
    [CmdletBinding(SupportsShouldProcess = $False)]
    param (
        [Parameter(
            Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Name,
        [Parameter(
            Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Version,
        [Parameter(
            Mandatory = $true)]
        [ValidatePattern('^(http|https)://')]
        [Alias('Url')]
        [String] $Uri,
        [Parameter(
            Mandatory = $true)]
        [ValidateSet('x86', 'x64', 'ARM32', 'ARM64', 'Multi')]
        [String] $Architecture,
        [Parameter(
            Mandatory = $true)]
        [ValidateSet('Msi', 'Msp', 'Exe', 'Zip', 'Msix', 'AppX', 'AppV', '7z')]
        [String] $Type,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $Language,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $Ring,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $Channel,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $Platform,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $BuildNumber,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $ReleaseDate,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $MD5,
        [Parameter(
            Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [String] $SHA256
    )

    $Output = [PSCustomObject]@{
        Name    = $Name
    }

    if ($Ring) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Ring' -Value $Ring
    }

    if ($Channel) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Channel' -Value $Channel
    }

    if ($Language) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Language' -Value $Language
    }

    if ($BuildNumber) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'BuildNumber' -Value $BuildNumber
    }

    if ($ReleaseDate) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'ReleaseDate' -Value $ReleaseDate
    }

    if ($Platform) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Platform' -Value $Platform
    }

    if ($Architecture) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Architecture' -Value $Architecture
    }

    if ($Type) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Type' -Value $Type
    }

    Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Version' -Value $Version
    Add-Member -InputObject $Output -MemberType NoteProperty -Name 'Uri' -Value $Uri

    if ($MD5) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'MD5' -Value $MD5
    }

    if ($SHA256) {
        Add-Member -InputObject $Output -MemberType NoteProperty -Name 'SHA256' -Value $SHA256
    }

    $Output
}
DanGough commented 4 weeks ago

Thanks, I can take a look and add to the next release - I have a few apps to fix and a couple of PRs to review, which I should get around to doing some time this weekend!