ReviewOS

pantry-pm/pantry

bun version resolution silently falls back to a stale bundled catalog when the unauthenticated GitHub API call fails

#242
Closed glennmichael123 opened this 8 days ago ·
8 days ago

Observed CI failure

A ^1.4.1 constraint on bun.sh is rejected as unsatisfiable, while ^1.4.0 resolves to 1.4.2 — a version that satisfies both ranges.

deps.yaml:

dependencies:
  craft-native.org: ^0.0.90
  bun.sh: ^1.4.1

Setup Pantry, runs-on: macos-26, action pinned at v0.11.45 (which downloads pantry CLI 0.11.74). Run 37620377338, job Desktop app:

Installing bun.com@latest via pantry SDK (defaulted to latest)
Installing craft-native.org@^0.0.90 via pantry SDK (from deps spec)
Installing bun.com@^1.4.1 via pantry SDK (from deps spec)
##[error]Required system package bun.sh@^1.4.1 failed to install: No version of bun.com satisfies "^1.4.1" (checked 143 versions, newest 1.4.0).

Changing only the constraint floor, nothing else, in run 37624061560:

Installing bun.com@^1.4.0 via pantry SDK (from deps spec)
Installing bun.com@1.4.2 via pantry SDK (from this action run)

1.4.2 satisfies ^1.4.1. The first run is wrong, and it is wrong in the direction that stops the job.

Note the line above the failure in both runs: bun.com@latest installs successfully in the same step. Only the ranged spec fails.

What the registry serves

https://registry.pantry.dev/binaries/bun.com/metadata.json — the endpoint publishedRegistryVersions fetches:

name: bun.com | latestVersion: 1.4.2 | updatedAt: 2026-10-04T05:06:55.422Z

8 versions:
  1.2.23   ['darwin-arm64', 'linux-arm64', 'linux-x86-64']
  1.3.11   ['darwin-arm64', 'linux-x86-64']
  1.3.12   ['darwin-arm64', 'linux-arm64', 'linux-x86-64']
  1.3.13   ['darwin-arm64', 'linux-arm64', 'linux-x86-64']
  1.3.14   ['darwin-arm64', 'linux-arm64', 'linux-x86-64']
  1.4.0    ['darwin-arm64', 'linux-arm64', 'linux-x86-64']
  1.4.1    ['darwin-arm64', 'linux-arm64', 'linux-x86-64']
  1.4.2    ['darwin-arm64', 'linux-arm64', 'linux-x86-64']

1.4.1 and 1.4.2 each carry a darwin-arm64 build, and latestVersion is 1.4.2. The error reports 143 versions with a newest of 1.4.0, so the range is being matched against a list that is neither this metadata nor its platform-filtered subset. /packages/bun.com/versions returns the same eight.

The darwin-arm64 tarballs resolve for all three versions:

GET binaries/bun.com/1.4.2/darwin-arm64/bun.com-1.4.2.tar.gz -> 302
GET binaries/bun.com/1.4.1/darwin-arm64/bun.com-1.4.1.tar.gz -> 302
GET binaries/bun.com/1.4.0/darwin-arm64/bun.com-1.4.0.tar.gz -> 302

Verified source path

packages/action/src/index.ts#L294-L297 states the contract: semver ranges "are passed through to installPackage, which resolves them against the package's known-version list." That known-version list is what disagrees with the registry metadata the same file fetches at L314-L331, and the range is resolved at L393.

The 143-entry count and the 1.4.0 ceiling both point at that list rather than at metadata.json.

What this is not

Each of these was checked and ruled out:

  • Not a missing platform build. Shown above: darwin-arm64 exists for 1.4.0, 1.4.1 and 1.4.2 in both the metadata and as fetchable tarballs.
  • Not the CLI. pantry 0.11.74, the exact build the action downloads, resolves this correctly on darwin-arm64 outside the action. bun.sh: ^1.4.1 installs 1.4.2, and so does the bun.com: ^1.4.1 alias the action resolves through. The defect is on the action/SDK path only.
  • Not a stale alias mapping. pantry info bun.sh lists bun.com as an alias; resolving either name directly works in the CLI.
  • Not transient. The failing run was re-run and failed identically. It reproduced across three separate runs over 30 minutes.
  • Not a lock pin. pantry.lock is gitignored in this repo, so CI resolves purely from the deps spec — which the log confirms (from deps spec).

Why it surfaced now

The same ^1.4.1 pin built successfully on 2026-09-07 (v0.3.1 shipped its signed DMG) and failed on 2026-10-07 with the pin unchanged.

#234, closed 2026-10-03, correctly changed a failed required-Bun install from core.warning + continue into a hard failure. That looks like what turned this from a silent wrong-path into a stopped job. The resolution defect is the older one; #234 made it visible. Raising it here rather than there, since #234's change is right and reverting it would re-hide this.

#216 fixed a closely related shape — a constraint parsed so that every candidate was excluded, with "select the newest matching concrete registry version without falling back outside the requested range" in its contract. This looks like the same family on a stable (non-prerelease) range.

Expected

  • A semver range must resolve against the versions the registry publishes for the runner's platform. ^1.4.1 and ^1.4.0 must both select 1.4.2 given the metadata above.
  • A range that cannot be satisfied must report the candidate list it actually consulted. "checked 143 versions, newest 1.4.0" named neither the source nor a set matching any registry endpoint, which is what made this take a full afternoon to localise.
  • Cover the case where the known-version list is staler than the registry metadata: a range above the stale ceiling but satisfiable from live metadata must resolve, not fail.

Not asking for the bun.sh ceiling to be bumped by hand, or for #234 to be loosened. The ask is that the ranged path consult the same versions the live-metadata path already does.

Workaround

Lower the floor to whatever the stale list's ceiling allows — bun.sh: ^1.4.0 here. It then installs 1.4.2 anyway, which is the clearest statement of the bug.

8 days ago

Correcting my own report. I had the symptom right and the cause wrong, and the title is misleading. The range matcher is fine; nothing is wrong with satisfiesInstallerConstraint.

Actual mechanism

143 in the error is the fingerprint. It is exactly the length of the bundled catalog at the tag this workflow pins the action to:

tagpackages/ts-pantry/src/packages/buncom.tsnewesthas 1.4.1has 1.4.2
v0.11.45 (our pin)143 versions1.4.0nono
v0.11.74145 versions1.4.2yesyes

So candidates contained the bundled list and nothing else. Any live entry would have changed that count.

fetchLiveVersions is the reason it was empty:

if (isBunDomain(domain)) {
  const resp = await fetchJSON('https://api.github.com/repos/oven-sh/bun/releases?per_page=100').catch(() => null)
  const releases = (resp as Array<{ tag_name?: string }> | null) || []
  return releases.map(r => (r.tag_name || '').replace(/^bun-v/, '')).filter(Boolean)
}

Unauthenticated api.github.com, 60 requests/hour/IP, and GitHub Actions runners share egress addresses. .catch(() => null) turns a 403 into [], which is indistinguishable from "upstream published nothing". resolveVersionConstraint then unions that empty list with the stale bundled one and throws a message naming the constraint.

Called directly just now, that endpoint returns 100 releases, newest 1.4.2, including 1.4.1. So both ^1.4.1 and ^1.4.0 resolve to 1.4.2 whenever the call succeeds — which is exactly what the passing run shows (Installing bun.com@1.4.2), and why only the ^1.4.1 floor appeared to be the trigger: ^1.4.0 still matches 1.4.0 in the stale list, so it survives the degraded path while ^1.4.1 cannot.

Three corrections to the issue above

  1. The title is wrong. No satisfiable range is being rejected by the matcher. A pin above the bundled ceiling is rejected when the live list is empty. Retitle to something like "bun version resolution silently falls back to a stale bundled catalog when the unauthenticated GitHub API call fails".
  2. "What the registry serves" is the wrong comparison for this package. bun.com never consults registry.pantry.dev during constraint resolution — the bun branch returns before reaching registryVersions. That section is accurate about the registry and irrelevant to the bug.
  3. "Not transient" is wrong. It is rate-limit dependent. My three failures landed inside one exhausted hour, which I misread as determinism.

Expected, restated

  • Don't let a failed live-version lookup present as an unsatisfiable constraint. [] from a 403 and [] from "nothing published" need to be distinguishable, the way publishedRegistryVersions already separates undefined from empty for the lock-pin path, with the comment "A registry hiccup must never turn into a wrong version." Same principle, other code path.
  • Authenticate that call inside the action, where GITHUB_TOKEN is available, or consult the registry metadata as a fallback. Either removes the shared-IP rate limit as a failure mode.
  • When resolution does fail, say which sources were consulted and whether any errored. "checked 143 versions, newest 1.4.0" sent me to the wrong conclusion twice.

Dropping our floor to ^1.4.0 only widened the window; it still silently installs 1.4.0 instead of 1.4.2 whenever the lookup fails. Pinning the action to a tag whose bundled catalog is current is the better consumer-side workaround, and that it works at all is itself the argument for not depending on the bundled snapshot.

Sign in to comment on this issue.