jackwener / jackwener/OpenCLI

[Bug]: extension update check is permanently silenced — failed GitHub fetch still refreshes the 24h cooldown

Open
#2,414 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
29.5k
Forks
2.9k
Avg merge
15h 36m
Merged PRs (30d)
70

Description

### Description

`checkForUpdateBackground()` writes `lastCheck` unconditionally but `latestExtensionVersion` only on success. Because `lastCheck` also gates the 24h re-check, a single failed GitHub Releases fetch stamps the cooldown *without* recording the extension version — and every subsequent run re-arms that cooldown before it can retry. The extension update notice then stays silent indefinitely.

`src/update-check.ts:199-204`:

```js
if (typeof data.version === 'string') {
const extVersion = await fetchLatestExtensionVersion(); // undefined on any non-2xx
const updates: Partial = { lastCheck: Date.now(), latestVersion: data.version };
if (extVersion) updates.latestExtensionVersion = extVersion; // conditional
writeCacheMerge(updates); // lastCheck: unconditional
}
```

The two fetches have very different reliability. `NPM_REGISTRY_URL` is a global CDN. `GITHUB_RELEASES_URL` is **unauthenticated** `api.github.com`, capped at 60 requests/hour **per IP** — shared with every other tool on that machine. When it returns 403, `fetchLatestExtensionVersion()` hits `if (!res.ok) return undefined` (line 172) and the key is skipped, but the cooldown is still refreshed.

Net effect: one 403 is enough to silence the feature, and it never self-heals, because the next run returns early at line 186 before reaching either fetch.

### Steps to Reproduce

Observed on a machine where the GitHub quota was already exhausted by unrelated tooling:

```bash
$ curl -s https://api.github.com/rate_limit -H 'User-Agent: opencli/1.8.7' \
| python3 -c "import sys,json;c=json.load(sys.stdin)['resources']['core'];print(c['limit'],c['remaining'])"
60 0

$ curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Accept: application/vnd.github+json' -H 'User-Agent: opencli/1.8.7' \
'https://api.github.com/repos/jackwener/OpenCLI/releases?per_page=20'
403
```

Resulting cache (`~/.opencli/update-check.json`) after months of daily use:

```json
{
"currentExtensionVersion": "1.0.23",
"extensionLastSeenAt": 1787847804948,
"lastCheck": 1787815161355,
"latestVersion": "1.8.7"
}
```

`latestVersion` (npm, CLI) is present. `latestExtensionVersion` (GitHub, extension) **has never been written**. `lastCheck` is recent, so `checkForUpdateBackground()` returns at line 186 on every invocation.

To reproduce deterministically without waiting for a real 403, make the GitHub call fail while the npm call succeeds — e.g. point `GITHUB_RELEASES_URL` at an endpoint returning 403/500, or run with the quota exhausted. `lastCheck` advances; `latestExtensionVersion` stays absent.

### Expected Behavior

A failed extension-version fetch should not consume the re-check window. Either:

1. Track the two fetches with separate timestamps (`lastCheck` for npm, e.g. `extLastCheck` for the extension), so each retries on its own schedule; or
2. Only advance `lastCheck` when *both* lookups succeed, letting the next run retry; or
3. Keep one timestamp but apply a short backoff when the extension lookup fails, instead of the full 24h.

Two robustness improvements worth folding in, since the current call is quota-fragile:

- Send a conditional request (`If-None-Match`/`ETag`) and cache the tag. 304s do not count against the rate limit, which removes the dominant failure mode rather than just retrying into it.
- `/releases?per_page=20` downloads 20 full release objects to extract one version string. `/releases/latest` is one object, and `extractLatestExtensionVersionFromReleases()` already falls back to the `ext-v*` tag pattern for extension-only releases.

### Impact

Both consumers of `latestExtensionVersion` are unreachable whenever the cache is in this state:

- `src/doctor.ts:209` — the "Extension update available" issue in `opencli doctor`
- `src/update-check.ts:108-118` — the exit-time update notice from `buildUpdateNotices()`

Concretely: on the machine above the loaded extension sat at **v1.0.21 (released 2026-06-28) while the CLI ran v1.8.7**, for roughly two months, and neither surface ever mentioned an update was available. The mismatch only came to light through manual `shasum` comparison of `dist/background.js` between the loaded directory and `~/.opencli/extension/opencli-extension`.

Note the release-asset parser itself is fine — verified against the live API, it correctly resolves `opencli-extension-v1.0.23.zip` from the `v1.8.7` release. The defect is purely in the cache bookkeeping.

### Environment

- OpenCLI: 1.8.7 (source verified at `90d5070`)
- Extension: 1.0.23
- OS: macOS 26.5 (Darwin 25.5.0, arm64)
- Node: v22 / npm global install

Contributor guide

Open the contributing guide

Research direction

Start in src/update-check.ts around lines 172, 186, and 199-204, tracing how the GitHub and npm fetch results update the cache and how lastCheck gates later runs. Reproduce a failed GitHub response with a successful npm response, then verify that the extension lookup can retry without losing the successful CLI version cache and that the update notices can receive latestExtensionVersion.

Written by the indexing model from the issue text.

Assessment

Tech stack
github, javascript
Domain
api, cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.