Public/Update-Evergreen.ps1

function Update-Evergreen {
    <#
        .SYNOPSIS
            Download and synchronize Evergreen Apps and Manifests from a separate GitHub repository.

        .DESCRIPTION
            Enables separation of the core Evergreen module from app-specific code and manifests.
            Downloads the latest versions of /Apps and /Manifests from a specified GitHub repository to a user-writable location (no admin required).
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.SwitchParameter] $Force
    )

    begin {
        # Sync folders to check for expected structure
        $SyncFolders = @((Join-Path -Path $Script:AppsPath -ChildPath 'Apps'), (Join-Path -Path $Script:AppsPath -ChildPath 'Manifests'))

        try {
            # Get the latest version from the remote repository
            Write-Message -Message "Checking for latest Evergreen apps release."
            $Url = "https://api.github.com/repos/$($script:resourceStrings.Repositories.Apps.Repo)/releases/latest"
            $EvergreenAppsRelease = Get-GitHubRepoRelease -Uri $Url -Filter "\.zip$|\.csv"
            $EvergreenAppsZip = $EvergreenAppsRelease | Where-Object { $_.Type -eq "zip" }
            $EvergreenAppsCsv = $EvergreenAppsRelease | Where-Object { $_.Type -eq "csv" }
            Write-Message -Message "Latest Evergreen apps release: $($EvergreenAppsZip.Version)"
        }
        catch {
            $EvergreenAppsRelease = $null
        }

        # Check whether the AppsPath exists and create it if not
        if (Test-Path -Path $script:AppsPath -PathType "Container") {

            $DoHashCheck = $false
            foreach ($folder in $SyncFolders) {
                if (Test-Path -Path $folder -PathType "Container") {
                    $DoHashCheck = $true
                }
            }

            if ($DoHashCheck) {
                try {
                    # Get remote SHA256 hashes from CSV which will be attached to the latest release
                    Write-Message -Message "Downloading hash file: $($EvergreenAppsCsv.Uri)."
                    $Sha256Csv = $EvergreenAppsCsv | Save-EvergreenApp -LiteralPath $script:AppsPath -Force
                    if ($Sha256Csv) {
                        $FileHash = (Get-FileHash -Path $Sha256Csv -Algorithm "SHA256").Hash.ToLower()
                        if ($FileHash -ne $EvergreenAppsCsv.Sha256.ToLower()) {
                            throw "SHA256 mismatch for downloaded hash file."
                        }
                        else {
                            Write-Message -Message "Downloaded hash file passed hash validation." -MessageType "Pass"
                        }
                    }
                    $RemoteFileShas = $Sha256Csv | Get-Content | ConvertFrom-Csv
                }
                catch {
                    Write-Warning -Message "Failed to retrieve or parse SHA256 hash file: $_"
                }

                # Check if the local files match the expected SHA256 hashes
                Write-Message -Message "Validating local cache against SHA256 hashes."
                $HashMismatch = $false
                foreach ($File in $RemoteFileShas) {
                    $FilePath = Join-Path -Path $script:AppsPath -ChildPath $File.file_path
                    if (Test-Path -Path $FilePath) {
                        $LocalHash = (Get-FileHash -Path $FilePath -Algorithm "SHA256").Hash.ToLower()
                        if ($LocalHash -ne $File.sha256.ToLower()) {
                            Write-Message -Message "SHA256 mismatch for file: '$($FilePath)'." -MessageType "Fail"
                            $HashMismatch = $true
                        }
                    }
                }
                if ($HashMismatch) {
                    Write-Message -Message "SHA256 mismatch found. Recommend running 'Update-Evergreen -Force'." -MessageType "Warning"
                }
                else {
                    Write-Message -Message "Local cache passed hash validation." -MessageType "Pass"
                }
            }
        }
        else {
            # Create the AppsPath directory
            New-Item -Path $script:AppsPath -ItemType "Directory" -Force | Out-Null
        }
    }

    process {
        try {
            # Read the local version file
            $LocalVersion = (Get-Content -Path $script:VersionFile -Raw -ErrorAction "Stop").Trim()
            Write-Message -Message "Local cache version: $LocalVersion"
        }
        catch {
            $LocalVersion = $null
        }

        $DoUpdate = $false
        if ($null -eq $EvergreenAppsRelease) {
            throw "Could not retrieve remote version information. Please check your internet connection or the repository URL."
        }
        elseif ($null -eq $LocalVersion) {
            Write-Message -Message "Unable to find Evergreen apps cached version. Downloading latest release."
            $DoUpdate = $true
        }
        elseif ([System.Version]$EvergreenAppsZip.Version -gt [System.Version]$LocalVersion) {
            Write-Message -Message "Evergreen apps cache is out of date. Downloading latest release."
            $DoUpdate = $true
        }
        elseif ([System.Version]$EvergreenAppsZip.Version -le [System.Version]$LocalVersion) {
            Write-Message -Message "Local cache matches release version. Evergreen apps are up to date."
            $DoUpdate = $false
            if ($Force) {
                Write-Message -Message "Forcing update due to -Force parameter."
            }
            else {
                Write-Message -Message "Use 'Update-Evergreen -Force' to force a full re-sync."
            }
        }
        else {
            Write-Message -Message "Unable to validate local Evergreen apps cached version. Downloading latest release."
            $DoUpdate = $true
        }

        # Check local expected directories exist
        foreach ($folder in $SyncFolders) {
            if (-not (Test-Path -Path $folder -PathType "Container")) {
                Write-Message -Message "'$folder' does not exist. Will perform full sync."
                $DoUpdate = $true
            }
        }

        # If -Force or no local copy or commit mismatch, do a full download
        if ($Force -or $DoUpdate) {
            Write-Message -Message "Performing full sync from remote repository."

            Write-Message -Message "Downloading Evergreen apps release: $($EvergreenAppsZip.Uri)."
            $ZipFile = Save-EvergreenApp -InputObject $EvergreenAppsZip -LiteralPath $script:AppsPath -Force
            if (Test-Path -Path $ZipFile -PathType "Leaf") {
                Write-Verbose -Message "Downloaded Evergreen apps release to $ZipFile."

                $ZipFileHash = (Get-FileHash -Path $ZipFile -Algorithm "SHA256").Hash.ToLower()
                if ($EvergreenAppsZip.Sha256.ToLower() -ne $ZipFileHash) {
                    throw "SHA256 mismatch for downloaded release zip file."
                }
                else {
                    Write-Message -Message "Downloaded release zip file passed hash validation." -MessageType "Pass"
                }

                Write-Verbose -Message "Extracting Evergreen apps release from $ZipFile."
                $ExtractPath = Join-Path -Path $script:AppsPath -ChildPath "_extracted"
                if (Test-Path -Path $ExtractPath) { Remove-Item -Path $ExtractPath -Recurse -Force -ErrorAction "SilentlyContinue" }
                $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue
                Expand-Archive -Path $ZipFile -DestinationPath $ExtractPath -Force
                Remove-Item -Path $ZipFile -Force -ErrorAction "SilentlyContinue"

                Write-Message -Message "Validating extracted files against SHA256 hashes."
                $DoReplace = $true
                foreach ($File in $RemoteFileShas) {
                    $FilePath = Join-Path -Path $ExtractPath -ChildPath $File.file_path
                    if (Test-Path -Path $FilePath) {
                        $LocalHash = (Get-FileHash -Path $FilePath -Algorithm "SHA256").Hash.ToLower()
                        if ($LocalHash -ne $File.sha256.ToLower()) {
                            Write-Warning -Message "SHA256 mismatch for file '$($File.file_path)'." -MessageType "Fail"
                            $DoReplace = $false
                        }
                        else {
                            Write-Verbose -Message "[$(Get-Symbol -Symbol "Tick")] File '$($File.file_path)' hash matches expected value."
                        }
                    }
                    else {
                        Write-Warning -Message "Expected file '$($File.file_path)' not found in extracted release." -MessageType "Fail"
                        $DoReplace = $false
                    }
                }

                if ($DoReplace) {
                    Write-Message -Message "Extracted files passed hash validation." -MessageType "Pass"
                    Write-Message -Message "Synchronizing Evergreen apps and manifests to $script:AppsPath."
                    # Remove existing Apps and Manifests directories
                    $LocalAppsPath = Join-Path -Path $script:AppsPath -ChildPath "Apps"
                    $LocalManifestsPath = Join-Path -Path $script:AppsPath -ChildPath "Manifests"
                    if (Test-Path -Path $LocalAppsPath) { Remove-Item -Path $LocalAppsPath -Recurse -Force -ErrorAction "SilentlyContinue" }
                    if (Test-Path -Path $LocalManifestsPath) { Remove-Item -Path $LocalManifestsPath -Recurse -Force -ErrorAction "SilentlyContinue" }

                    # Move extracted contents to the correct locations
                    Move-Item -Path (Join-Path -Path $ExtractPath -ChildPath "Apps") -Destination $script:AppsPath
                    Move-Item -Path (Join-Path -Path $ExtractPath -ChildPath "Manifests") -Destination $script:AppsPath

                    if ($EvergreenAppsZip) { Set-Content -Path $script:VersionFile -Value $EvergreenAppsZip.Version -Encoding "UTF8" -Force }
                    Write-Message -Message "Update complete."
                }
                else {
                    Write-Warning -Message "Some files did not match expected SHA256 hashes. Evergreen apps and manifests were not updated."
                }

                # Clean up the extracted files
                Remove-Item -Path $ExtractPath -Recurse -Force -ErrorAction "SilentlyContinue"
            }
            else {
                throw "Failed to download Evergreen apps release. The file does not exist at $ZipFile."
            }
        }
    }
}