Private/Invoke-IntuneDefinitionUpdate.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
    Updates App.json and Source\Install.json definition files with the latest
    version resolved via each definition's Application.Filter expression.
 
.DESCRIPTION
    For each valid definition row (DefinitionValid -eq 'Yes'), invokes
    Get-IntunePackageLatestVersion to resolve the current latest version and
    download URI from Evergreen. When a newer version or different filename is
    found the following fields are written back to disk:
 
    App.json:
      - PackageInformation.Version
      - PackageInformation.SetupFile (filename derived from resolved URI)
      - DetectionRule[*].VersionValue (File / Version detection rules)
      - DetectionRule[*].Value (Registry / VersionComparison detection rules)
 
    Source\Install.json (when present alongside App.json):
      - PackageInformation.Version
      - PackageInformation.SetupFile
 
    Definitions whose resolved version and derived filename match what is already
    stored are skipped (no file writes). All file operations are best-effort; a
    failure on one definition does not abort processing of the remaining rows.
 
.PARAMETER DefinitionRows
    Array of definition row objects as stored in $syncHash.IntuneDefinitionRows.
    Each row must have DefinitionValid, DefinitionPath, Name, and DefinitionObject.
 
.PARAMETER SyncHash
    Synchronized hashtable shared with the UI thread. Used by Write-UILog for
    thread-safe log output.
 
.OUTPUTS
    Array of PSCustomObject with:
        Name : string - display name of the definition
        DefinitionPath : string - full path to App.json
        OldVersion : string - version before update
        NewVersion : string - version after update (same as OldVersion when NoUpdateNeeded)
        NewSetupFile : string - SetupFile value written (empty when NoUpdateNeeded or failed)
        Succeeded : bool
        NoUpdateNeeded : bool - true when version and filename were already current
        Error : string - populated on failure
#>

function Invoke-IntuneDefinitionUpdate {
    [CmdletBinding()]
    [OutputType([PSCustomObject[]])]
    param(
        [Parameter(Mandatory)]
        [PSCustomObject[]]$DefinitionRows,

        [Parameter(Mandatory)]
        [System.Collections.Hashtable]$SyncHash
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    $results = [System.Collections.Generic.List[object]]::new()

    foreach ($row in $DefinitionRows) {
        $name           = [string]$row.Name
        $definitionPath = [string]$row.DefinitionPath
        $oldVersion     = [string]$row.Version

        $resultBase = [PSCustomObject]@{
            Name           = $name
            DefinitionPath = $definitionPath
            OldVersion     = $oldVersion
            NewVersion     = $oldVersion
            NewSetupFile   = ''
            Succeeded      = $false
            NoUpdateNeeded = $false
            Error          = ''
        }

        if ([string]$row.DefinitionValid -ne 'Yes') {
            $resultBase.Error = 'Skipped: definition is not valid.'
            $results.Add($resultBase)
            continue
        }

        if ($null -eq $row.DefinitionObject) {
            $resultBase.Error = 'Skipped: definition object is null.'
            $results.Add($resultBase)
            continue
        }

        Write-Verbose "EvergreenUI: resolving latest version for '$name'..."
        Write-UILog -SyncHash $SyncHash -Message "Intune update: resolving '$name'..." -Level Info

        $latestResult = $null
        try {
            $latestResult = Get-IntunePackageLatestVersion -DefinitionObject $row.DefinitionObject -ErrorAction Stop
        }
        catch {
            $resultBase.Error = "Get-IntunePackageLatestVersion threw: $($_.Exception.Message)"
            Write-UILog -SyncHash $SyncHash -Message "Intune update: failed to resolve '$name' - $($_.Exception.Message)" -Level Warning
            $results.Add($resultBase)
            continue
        }

        if (-not $latestResult.Succeeded) {
            $resultBase.Error = $latestResult.Error
            Write-UILog -SyncHash $SyncHash -Message "Intune update: failed to resolve '$name' - $($latestResult.Error)" -Level Warning
            $results.Add($resultBase)
            continue
        }

        $newVersion = [string]$latestResult.Version

        # Derive new SetupFile from URI; fall back to existing value when URI is empty
        $newSetupFile = ''
        if (-not [string]::IsNullOrWhiteSpace($latestResult.URI)) {
            $rawFileName = [System.IO.Path]::GetFileName($latestResult.URI)
            if (-not [string]::IsNullOrWhiteSpace($rawFileName)) {
                $newSetupFile = [System.Uri]::UnescapeDataString($rawFileName)
            }
        }

        $currentSetupFile = ''
        if ($null -ne $row.DefinitionObject.PackageInformation) {
            $currentSetupFile = [string]$row.DefinitionObject.PackageInformation.SetupFile
        }

        if ([string]::IsNullOrWhiteSpace($newSetupFile)) {
            $newSetupFile = $currentSetupFile
        }

        # Skip if nothing changed
        if ($newVersion -eq $oldVersion -and $newSetupFile -eq $currentSetupFile) {
            $resultBase.Succeeded      = $true
            $resultBase.NoUpdateNeeded = $true
            $resultBase.NewVersion     = $newVersion
            Write-UILog -SyncHash $SyncHash -Message "Intune update: '$name' is already current ($oldVersion)." -Level Info
            $results.Add($resultBase)
            continue
        }

        # Update App.json
        try {
            $appJson = Get-Content -LiteralPath $definitionPath -Raw -ErrorAction Stop |
                ConvertFrom-Json -ErrorAction Stop

            $appJson.PackageInformation.Version   = $newVersion
            $appJson.PackageInformation.SetupFile = $newSetupFile

            # Update version values embedded in detection rules
            if ($null -ne $appJson.DetectionRule) {
                foreach ($rule in $appJson.DetectionRule) {
                    if ($null -eq $rule) {
                        continue
                    }
                    $ruleType   = [string]$rule.Type
                    $ruleMethod = [string]$rule.DetectionMethod
                    if ($ruleType -ieq 'File' -and $ruleMethod -ieq 'Version') {
                        $rule.VersionValue = $newVersion
                    }
                    elseif ($ruleType -ieq 'Registry' -and $ruleMethod -ieq 'VersionComparison') {
                        $rule.Value = $newVersion
                    }
                }
            }

            $appJson | ConvertTo-Json -Depth 10 |
                Set-Content -LiteralPath $definitionPath -Encoding UTF8 -ErrorAction Stop

            Write-UILog -SyncHash $SyncHash -Message "Intune update: '$name' App.json updated $oldVersion -> $newVersion (SetupFile: $newSetupFile)." -Level Info
        }
        catch {
            $resultBase.Error = "Failed to update App.json: $($_.Exception.Message)"
            Write-UILog -SyncHash $SyncHash -Message "Intune update: '$name' App.json write failed - $($_.Exception.Message)" -Level Error
            $results.Add($resultBase)
            continue
        }

        # Update Source\Install.json when present
        $installJsonPath = Join-Path -Path (Split-Path -Path $definitionPath -Parent) -ChildPath 'Source'
        $installJsonPath = Join-Path -Path $installJsonPath -ChildPath 'Install.json'
        if (Test-Path -LiteralPath $installJsonPath -PathType Leaf) {
            try {
                $installJson = Get-Content -LiteralPath $installJsonPath -Raw -ErrorAction Stop |
                    ConvertFrom-Json -ErrorAction Stop

                if ($null -ne $installJson.PackageInformation) {
                    $installJson.PackageInformation.Version   = $newVersion
                    $installJson.PackageInformation.SetupFile = $newSetupFile
                }

                $installJson | ConvertTo-Json -Depth 10 |
                    Set-Content -LiteralPath $installJsonPath -Encoding UTF8 -ErrorAction Stop

                Write-UILog -SyncHash $SyncHash -Message "Intune update: '$name' Install.json updated." -Level Info
            }
            catch {
                # best-effort - Install.json failure must not mark the definition update as failed
                Write-UILog -SyncHash $SyncHash -Message "Intune update: '$name' Install.json write failed (non-fatal) - $($_.Exception.Message)" -Level Warning
                Write-Verbose "EvergreenUI: Install.json update failed for '$name': $($_.Exception.Message)"
            }
        }

        $resultBase.Succeeded    = $true
        $resultBase.NewVersion   = $newVersion
        $resultBase.NewSetupFile = $newSetupFile
        $results.Add($resultBase)
    }

    return $results.ToArray()
}