From 14032523a06ff7adc0f42600d44a0ef8c1bdd376 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Sat, 19 Sep 2026 15:29:55 +0100 Subject: [PATCH] Take the latest version from the index, and prove a folder is a version Microsoft.Office could not be installed at all. manifests/m/Microsoft/Office contains "2010", which is not a release but the Microsoft.Office.2010.* package namespace. Sorting numerically ranked 2010 above 16.0.20228.20124, resolution looked for an installer manifest inside it, and the failure surfaced to the operator as "not found in GitHub OR Custom Catalog" for an application that is published. Paired with a detection service this does not fail once, it loops: detection correctly reports not installed, the install policy fails, and the pair repeats every scan. Observed continuously on STAR-S01003. Two changes. The version now comes from the detection index, which carries winget's own packages.latest_version. That is authoritative rather than inferred, and it removes one of the two API calls per resolve. Where no index entry exists, the directory listing is still used, but a folder is no longer assumed to be a version because it sorts highest. The candidates are walked in order until one actually contains an installer manifest, bounded to five probes because each is an API call. The error now names every candidate tried instead of only the first. The earlier ^\d guard is kept but was never sufficient: it rejects "x86" and "Canary", and cannot reject "2010". Verified against the live repository: Microsoft.Office resolves to 16.0.20228.20124, Discord.Discord to 1.0.9258, Google.Chrome to 153.0.8010.53, Valve.Steam to 2.10.91.91 and Git.Git to 2.55.0.3. --- Private/Resolve-GitHubManifest.ps1 | 68 +++++++++++++++++++++++------- Public/Get-GitHubInstaller.ps1 | 14 +++++- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/Private/Resolve-GitHubManifest.ps1 b/Private/Resolve-GitHubManifest.ps1 index 1ad21e8..746c709 100644 --- a/Private/Resolve-GitHubManifest.ps1 +++ b/Private/Resolve-GitHubManifest.ps1 @@ -37,7 +37,12 @@ function Resolve-GitHubManifest { param ( [Parameter(Mandatory=$true)][string]$Id, [Parameter(Mandatory=$true)][string]$SysArch, - [Parameter(Mandatory=$true)][hashtable]$Headers + [Parameter(Mandatory=$true)][hashtable]$Headers, + + # winget's own latest_version for this package, from the detection + # index. Authoritative, so it skips the directory listing entirely - + # which is both correct and one fewer API call. + [string]$KnownVersion ) # 2. Construct API Path @@ -45,24 +50,55 @@ function Resolve-GitHubManifest { $FirstChar = $Id.Substring(0,1).ToLower() $BaseApi = "https://api.github.com/repos/microsoft/winget-pkgs/contents/manifests/$FirstChar/$IdPath" - # 3. Get Version (Latest) [API call 1 of 2] - $VersionsResponse = Invoke-RestMethod -Uri $BaseApi -Method Get -Headers $Headers -ErrorAction Stop + # 3. Candidate version folders, best first. + # + # A package folder can contain entries that are not versions at all. Some + # are architectures or channels (Discord carries x86, arm64, Canary, PTB). + # Worse, some are nested PACKAGE namespaces that look exactly like a + # version: manifests/m/Microsoft/Office contains "2010", which is the + # Microsoft.Office.2010.* family, not a release. Sorting numerically put + # 2010 above 16.0.20228.20124, resolution then looked for an installer + # manifest inside it and threw, and Install-TecharyApp reported + # "not found in GitHub OR Custom Catalog" for an app that is published. + # + # So a folder is only a version if it actually contains an installer + # manifest. Walk the candidates until one does. + $Candidates = @() + + if ($KnownVersion) { + $Candidates += $KnownVersion + } + else { + $VersionsResponse = Invoke-RestMethod -Uri $BaseApi -Method Get -Headers $Headers -ErrorAction Stop + $Candidates = @($VersionsResponse | + Where-Object { $_.type -eq "dir" } | + Select-Object *, @{N='SortKey'; E={ Get-ManifestVersionKey -Name $_.name }} | + Where-Object { $null -ne $_.SortKey } | + Sort-Object SortKey -Descending | + Select-Object -ExpandProperty name) + + if ($Candidates.Count -eq 0) { throw "Could not determine a valid version folder for '$Id'." } + } + + # 4. Get Manifest + $LatestVersion = $null + $InstallerFile = $null + $Tried = New-Object System.Collections.Generic.List[string] - $LatestVersionObj = $VersionsResponse | - Where-Object { $_.type -eq "dir" } | - Select-Object *, @{N='SortKey'; E={ Get-ManifestVersionKey -Name $_.name }} | - Where-Object { $null -ne $_.SortKey } | - Sort-Object SortKey -Descending | - Select-Object -First 1 + # Bounded: each probe is an API call, and that allowance is scarce. + foreach ($Candidate in ($Candidates | Select-Object -First 5)) { + $Tried.Add($Candidate) + try { + $VersionFiles = Invoke-RestMethod -Uri "$BaseApi/$Candidate" -Method Get -Headers $Headers -ErrorAction Stop + } catch { continue } - if (-not $LatestVersionObj) { throw "Could not determine a valid version folder for '$Id'." } - $LatestVersion = $LatestVersionObj.Name + $File = $VersionFiles | Where-Object { $_.name -like "*.installer.yaml" } | Select-Object -First 1 + if ($File) { $LatestVersion = $Candidate; $InstallerFile = $File; break } + } - # 4. Get Manifest [API call 2 of 2] - $VersionPath = "$BaseApi/$LatestVersion" - $VersionFiles = Invoke-RestMethod -Uri $VersionPath -Method Get -Headers $Headers -ErrorAction Stop - $InstallerFile = $VersionFiles | Where-Object { $_.name -like "*.installer.yaml" } | Select-Object -First 1 - if (-not $InstallerFile) { throw "No installer YAML found for '$Id' $LatestVersion." } + if (-not $InstallerFile) { + throw ("No installer YAML found for '{0}'. Tried: {1}." -f $Id, ($Tried -join ', ')) + } # Served from raw.githubusercontent.com, which is CDN-backed and not # subject to the API rate limit, so no credentials are sent here. diff --git a/Public/Get-GitHubInstaller.ps1 b/Public/Get-GitHubInstaller.ps1 index 85f424f..e097e1a 100644 --- a/Public/Get-GitHubInstaller.ps1 +++ b/Public/Get-GitHubInstaller.ps1 @@ -65,8 +65,20 @@ function Get-GitHubInstaller { } if (-not $Meta) { + # winget's own latest_version for this package. Authoritative, and it + # avoids re-deriving the version from a directory listing that also + # contains architectures, channels and nested package namespaces. + $KnownVersion = $null try { - $Meta = Resolve-GitHubManifest -Id $Id -SysArch $SysArch -Headers $Headers + $Entry = Get-DetectionEntry -Id $Id + if ($Entry -and $Entry.Version) { + $KnownVersion = $Entry.Version + Write-PackagerLog -Message "Index gives $Id latest version $KnownVersion; skipping version discovery." + } + } catch { } + + try { + $Meta = Resolve-GitHubManifest -Id $Id -SysArch $SysArch -Headers $Headers -KnownVersion $KnownVersion try { if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir -Force -ErrorAction Stop | Out-Null }