update-check gates on main:VERSION but /gstack-upgrade installs main HEAD — installs sit silently 31 commits behind between releases (incl. security fixes)
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## Summary
`gstack-update-check` decides "is there an upgrade?" by string-comparing `main:VERSION` against the local `VERSION` file, but `/gstack-upgrade` applies the upgrade with `git reset --hard origin/main`. Those are two different clocks:
- **The check** reads a *release marker* that contributors are explicitly told not to touch.
- **The upgrade** installs *all of `main`*.
Between releases, `main` accumulates commits while `main:VERSION` stays pinned at the last released value. Every installed client on that release is told "up to date" and has no supported path to those commits — including security fixes.
This is not the stale-CDN problem already handled by the SHA-pinned raw URL at `bin/gstack-update-check:183-202`. That fix works correctly here: the SHA-pinned fetch accurately returns `main`'s VERSION. The version it accurately returns just isn't a function of `main`'s content.
## Reproduction (observed today, 2026-07-28)
Global git install at `~/.claude/skills/gstack`, sitting on the `v1.60.1.0` release commit `7c9df1c`:
```
$ ~/.claude/skills/gstack/bin/gstack-update-check --force
(no output — i.e. UP_TO_DATE)
```
Meanwhile:
```
$ git fetch origin && git rev-list --count HEAD..origin/main
31
$ git rev-parse --short HEAD origin/main
7c9df1c
a325940
$ git show origin/main:VERSION
1.60.1.0 # identical to local → line 220 returns UP_TO_DATE
```
`main`'s tip is `a325940` (2026-07-14); the last VERSION bump is `7c9df1c` (2026-07-09). So `main` has been 31 commits ahead of its own VERSION for ~2 weeks, and no client can see it.
## Why `main:VERSION` is stale (and why this will recur)
Commit `f155481` bumped VERSION/CHANGELOG/package.json to `1.60.2.0`. Commit `c7e182b` then reverted all three, with this rationale in its message:
> Revert VERSION, CHANGELOG, and package.json bumps — leave release bookkeeping to maintainers / `/ship`, matching community PR practice.
**That commit is correct.** Community PRs shouldn't self-assign version numbers. Which is exactly why this isn't a one-off: the project's own contribution norm guarantees `main:VERSION` lags `main` by design, so any release cadence slower than the merge cadence reopens the window. `c7e182b` is the trigger here, not the cause.
## Impact
The 31 unreachable commits include security fixes:
- `f55e270` / `8b53302` — `basic-ftp` overridden to 5.3.1, clearing CVE-2026-39983 + 3 sibling HIGH advisories (plus a lockfile tripwire)
- `279fda4` — `browse`: case-insensitive exfiltration-domain blocklist matching (a mixed-case host currently bypasses the blocklist)
- `ea648b7` — `/careful`: fail closed on compound recursive deletes
- `a2a447a` — consistent credential-action classification
A user who runs `/gstack-upgrade`, is told they're current, and reasonably concludes they have the blocklist fix does not have it. The failure is silent and reads as success — the same shape as #1974, but from version-gate granularity rather than a crash.
To actually get these I had to bypass the tool entirely (`git stash && git reset --hard origin/main && ./setup`). Note the version number does not change afterward, so the install is now materially different from a fresh `1.60.1.0` while reporting the same string — two installs claiming one version.
## Suggested fixes
Pick one clock and use it for both the check and the upgrade:
**Option A — compare commits (matches what the upgrade actually does).** For git installs this is nearly free: the script already resolves `_REMOTE_SHA` via `git ls-remote ... refs/heads/main` at line 197. Compare it to local `git rev-parse HEAD` and report `UPGRADE_AVAILABLE` on mismatch. The VERSION comparison can stay as the display string. Vendored installs would need a recorded source SHA (e.g. written to `.gstack-source-sha` at setup).
**Option B — make releases the real unit of distribution.** Have `/gstack-upgrade` check out the latest release tag rather than `origin/main`. Then `main:VERSION` becomes an honest answer, at the cost of shipping fixes only at release boundaries.
Either resolves the mismatch. A cheap interim mitigation, independent of the above: have `/ship` (or CI) fail when `main` is more than N commits past its last VERSION bump, so this window is visible to maintainers rather than to users who find out by reading `git log`.
The semver-order guard at line 225-229 is also worth a look under Option A — it suppresses `UPGRADE_AVAILABLE` whenever `REMOTE` doesn't sort strictly higher than `LOCAL`, so a legitimate VERSION *revert* on `main` is indistinguishable from the stale-CDN regression it's meant to absorb.
## Environment
- gstack `1.60.1.0`, global git install (`~/.claude/skills/gstack`)
- macOS 15.5 (Darwin 25.5.0), Claude Code
Contributor guide
Research direction
Start with bin/gstack-update-check, especially the SHA fetch around line 183, remote resolution at line 197, version gate at line 220, and semver guard at lines 225-229; then trace the /gstack-upgrade path. Reproduce the release-commit versus origin/main case and confirm the chosen approach makes the check and upgrade report the same state, including security-fix commits.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git
- Domain
- cli, devops, release
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100