mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-08 23:36:06 +02:00
fix: gstack-update-check resolves remote VERSION via SHA-pinned URL
Replace branch-raw fetch with git ls-remote + SHA-pinned raw URL. Add semver-order guard via sort -V so REMOTE < LOCAL stays silent instead of emitting a backwards UPGRADE_AVAILABLE line. Fence git ls-remote with GIT_TERMINAL_PROMPT=0 + 5s low-speed timeout. Honor explicit GSTACK_REMOTE_URL overrides for test fixtures and private mirrors. 3 new tests cover stale-CDN regression, multi-segment 1.9 vs 1.10 both directions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0c88517a0f
commit
610c7cf778
+41
-4
@@ -8,7 +8,8 @@
|
|||||||
#
|
#
|
||||||
# Env overrides (for testing):
|
# Env overrides (for testing):
|
||||||
# GSTACK_DIR — override auto-detected gstack root
|
# GSTACK_DIR — override auto-detected gstack root
|
||||||
# GSTACK_REMOTE_URL — override remote VERSION URL
|
# GSTACK_REMOTE_URL — override remote VERSION URL (branch-pinned fallback)
|
||||||
|
# GSTACK_REMOTE_REPO — override remote git URL for ls-remote SHA resolution
|
||||||
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
|||||||
SNOOZE_FILE="$STATE_DIR/update-snoozed"
|
SNOOZE_FILE="$STATE_DIR/update-snoozed"
|
||||||
VERSION_FILE="$GSTACK_DIR/VERSION"
|
VERSION_FILE="$GSTACK_DIR/VERSION"
|
||||||
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
||||||
|
REMOTE_REPO="${GSTACK_REMOTE_REPO:-https://github.com/garrytan/gstack.git}"
|
||||||
|
|
||||||
# ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ──
|
# ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ──
|
||||||
if [ "${1:-}" = "--force" ]; then
|
if [ "${1:-}" = "--force" ]; then
|
||||||
@@ -178,9 +180,34 @@ if [ -n "$_SUPA_URL" ] && [ -n "$_SUPA_KEY" ] && [ "${_TEL_TIER:-off}" != "off"
|
|||||||
>/dev/null 2>&1 &
|
>/dev/null 2>&1 &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# GitHub raw fetch (primary, always reliable)
|
# Resolve VERSION via a SHA-pinned raw URL. GitHub's branch-raw CDN
|
||||||
|
# (raw.githubusercontent.com/<owner>/<repo>/<branch>/...) can serve stale
|
||||||
|
# content for several minutes after a push, which previously caused
|
||||||
|
# /gstack-upgrade to silently report "up to date" right after a release
|
||||||
|
# landed. git ls-remote always returns the live HEAD; SHA-pinned raw URLs
|
||||||
|
# are immediately consistent.
|
||||||
|
#
|
||||||
|
# An explicit GSTACK_REMOTE_URL override (tests, mirrors) skips this path
|
||||||
|
# so the override is honored verbatim.
|
||||||
REMOTE=""
|
REMOTE=""
|
||||||
REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
|
if [ -z "${GSTACK_REMOTE_URL:-}" ]; then
|
||||||
|
# Disable credential prompts and apply a 5-second low-speed timeout so a
|
||||||
|
# flaky network or captive portal can't hang every skill preamble.
|
||||||
|
_LSR_LINE="$(GIT_TERMINAL_PROMPT=0 GIT_HTTP_LOW_SPEED_LIMIT=1000 GIT_HTTP_LOW_SPEED_TIME=5 \
|
||||||
|
git ls-remote "$REMOTE_REPO" refs/heads/main 2>/dev/null || true)"
|
||||||
|
_REMOTE_SHA="$(echo "$_LSR_LINE" | awk '{print $1}')"
|
||||||
|
if echo "$_REMOTE_SHA" | grep -qE '^[0-9a-f]{40}$'; then
|
||||||
|
_SHA_URL="https://raw.githubusercontent.com/garrytan/gstack/${_REMOTE_SHA}/VERSION"
|
||||||
|
REMOTE="$(curl -sf --max-time 5 "$_SHA_URL" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: branch-pinned URL when ls-remote is unavailable (no git, no
|
||||||
|
# network, mirror without refs/heads/main) or when GSTACK_REMOTE_URL was
|
||||||
|
# explicitly overridden.
|
||||||
|
if [ -z "$REMOTE" ]; then
|
||||||
|
REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
|
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
|
||||||
|
|
||||||
# Validate: must look like a version number (reject HTML error pages)
|
# Validate: must look like a version number (reject HTML error pages)
|
||||||
@@ -195,7 +222,17 @@ if [ "$LOCAL" = "$REMOTE" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Versions differ — upgrade available
|
# Semver-order guard: only flag an upgrade when REMOTE sorts higher than
|
||||||
|
# LOCAL. Protects against transient stale-CDN regressions (REMOTE < LOCAL)
|
||||||
|
# and dev installs running ahead of main, both of which would otherwise
|
||||||
|
# emit a backwards UPGRADE_AVAILABLE line.
|
||||||
|
_HIGHER="$(printf '%s\n%s\n' "$LOCAL" "$REMOTE" | sort -V | tail -1)"
|
||||||
|
if [ "$_HIGHER" != "$REMOTE" ]; then
|
||||||
|
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# REMOTE is strictly newer — upgrade available
|
||||||
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
||||||
if check_snooze "$REMOTE"; then
|
if check_snooze "$REMOTE"; then
|
||||||
exit 0 # snoozed — stay quiet
|
exit 0 # snoozed — stay quiet
|
||||||
|
|||||||
@@ -496,6 +496,40 @@ describe('gstack-update-check', () => {
|
|||||||
|
|
||||||
// ─── Split TTL tests ─────────────────────────────────────────
|
// ─── Split TTL tests ─────────────────────────────────────────
|
||||||
|
|
||||||
|
// ─── Semver-order guard ─────────────────────────────────────
|
||||||
|
// When the upstream raw CDN serves a stale (older) VERSION right after a
|
||||||
|
// release, the script previously emitted a backwards UPGRADE_AVAILABLE
|
||||||
|
// line. The guard treats REMOTE < LOCAL as up-to-date.
|
||||||
|
|
||||||
|
test('remote older than local (stale CDN) → silent, cache UP_TO_DATE', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '1.34.0.0\n');
|
||||||
|
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.33.2.0\n');
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||||
|
expect(cache).toContain('UP_TO_DATE 1.34.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multi-segment sort: 1.9.0.0 < 1.10.0.0', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '1.9.0.0\n');
|
||||||
|
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.10.0.0\n');
|
||||||
|
|
||||||
|
const { stdout } = run();
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 1.9.0.0 1.10.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multi-segment reverse sort: 1.10.0.0 > 1.9.0.0 → no rewind', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '1.10.0.0\n');
|
||||||
|
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '1.9.0.0\n');
|
||||||
|
|
||||||
|
const { stdout } = run();
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||||
|
expect(cache).toContain('UP_TO_DATE 1.10.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
test('UP_TO_DATE cache expires after 60 min (not 720)', () => {
|
test('UP_TO_DATE cache expires after 60 min (not 720)', () => {
|
||||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
|
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
|
||||||
|
|||||||
Reference in New Issue
Block a user