From c86faa7968688993406fd4c6cd9550d58306408f Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Mon, 16 Mar 2026 14:14:15 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20update=20check=20cache=20=E2=80=94=2060m?= =?UTF-8?q?in=20UP=5FTO=5FDATE=20TTL=20+=20--force=20flag=20(v0.4.4)=20(#1?= =?UTF-8?q?10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: split update check cache TTL + add --force flag UP_TO_DATE cache now expires after 60 min (was 720 min / 12 hours). UPGRADE_AVAILABLE keeps 720 min TTL to keep nagging. --force flag deletes cache before checking, used by /gstack-upgrade standalone invocation to always get a fresh result from GitHub. Co-Authored-By: Claude Opus 4.6 (1M context) * feat: /gstack-upgrade standalone uses --force for fresh check Co-Authored-By: Claude Opus 4.6 (1M context) * chore: bump version and changelog (v0.4.4) Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 11 +++++ VERSION | 2 +- bin/gstack-update-check | 27 +++++++----- browse/test/gstack-update-check.test.ts | 58 +++++++++++++++++++++++-- gstack-upgrade/SKILL.md | 11 ++++- gstack-upgrade/SKILL.md.tmpl | 11 ++++- 6 files changed, 104 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c3fccebec77179c7441d80635e643d8418395cd..d61ba4f21202d5a7ce022f0544969f2f1f50478c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 0.4.4 — 2026-03-16 + +- **New releases detected in under an hour, not half a day.** The update check cache was set to 12 hours, which meant you could be stuck on an old version all day while new releases dropped. Now "you're up to date" expires after 60 minutes, so you'll see upgrades within the hour. "Upgrade available" still nags for 12 hours (that's the point). +- **`/gstack-upgrade` always checks for real.** Running `/gstack-upgrade` directly now bypasses the cache and does a fresh check against GitHub. No more "you're already on the latest" when you're not. + +### For contributors + +- Split `last-update-check` cache TTL: 60 min for `UP_TO_DATE`, 720 min for `UPGRADE_AVAILABLE`. +- Added `--force` flag to `bin/gstack-update-check` (deletes cache file before checking). +- 3 new tests: `--force` busts UP_TO_DATE cache, `--force` busts UPGRADE_AVAILABLE cache, 60-min TTL boundary test with `utimesSync`. + ## 0.4.3 — 2026-03-16 - **New `/document-release` skill.** Run it after `/ship` but before merging — it reads every doc file in your project, cross-references the diff, and updates README, ARCHITECTURE, CONTRIBUTING, CHANGELOG, and TODOS to match what you actually shipped. Risky changes get surfaced as questions; everything else is automatic. diff --git a/VERSION b/VERSION index 17b2ccd9bf9050efdf57d7800677e87919b9b5b9..6f2743d65dc06508954334e88edb660bf8efea20 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.3 +0.4.4 diff --git a/bin/gstack-update-check b/bin/gstack-update-check index 7c5e5ca035d1a49a9e38d8e4adc3e41adb54e2da..d44c7e0f27fea3556d987fae17603a4f282f62e2 100755 --- a/bin/gstack-update-check +++ b/bin/gstack-update-check @@ -20,6 +20,11 @@ SNOOZE_FILE="$STATE_DIR/update-snoozed" VERSION_FILE="$GSTACK_DIR/VERSION" REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}" +# ─── Force flag (busts cache for standalone /gstack-upgrade) ── +if [ "${1:-}" = "--force" ]; then + rm -f "$CACHE_FILE" +fi + # ─── Step 0: Check if updates are disabled ──────────────────── _UC=$("$GSTACK_DIR/bin/gstack-config" get update_check 2>/dev/null || true) if [ "$_UC" = "false" ]; then @@ -97,24 +102,27 @@ if [ -f "$MARKER_FILE" ]; then exit 0 fi -# ─── Step 3: Check cache freshness (12h = 720 min) ────────── +# ─── Step 3: Check cache freshness ────────────────────────── +# UP_TO_DATE: 60 min TTL (detect new releases quickly) +# UPGRADE_AVAILABLE: 720 min TTL (keep nagging) if [ -f "$CACHE_FILE" ]; then - # Cache is fresh if modified within 720 minutes - STALE=$(find "$CACHE_FILE" -mmin +720 2>/dev/null || true) - if [ -z "$STALE" ]; then - # Cache is fresh — read it - CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)" + CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)" + case "$CACHED" in + UP_TO_DATE*) CACHE_TTL=60 ;; + UPGRADE_AVAILABLE*) CACHE_TTL=720 ;; + *) CACHE_TTL=0 ;; # corrupt → force re-fetch + esac + + STALE=$(find "$CACHE_FILE" -mmin +$CACHE_TTL 2>/dev/null || true) + if [ -z "$STALE" ] && [ "$CACHE_TTL" -gt 0 ]; then case "$CACHED" in UP_TO_DATE*) - # Verify local version still matches cached version CACHED_VER="$(echo "$CACHED" | awk '{print $2}')" if [ "$CACHED_VER" = "$LOCAL" ]; then exit 0 fi - # Local version changed — fall through to re-check ;; UPGRADE_AVAILABLE*) - # Verify local version still matches cached old version CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')" if [ "$CACHED_OLD" = "$LOCAL" ]; then CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')" @@ -124,7 +132,6 @@ if [ -f "$CACHE_FILE" ]; then echo "$CACHED" exit 0 fi - # Local version changed (manual upgrade?) — fall through to re-check ;; esac fi diff --git a/browse/test/gstack-update-check.test.ts b/browse/test/gstack-update-check.test.ts index 2ec70e2dc232f905e2a1f40947da1fc0aed1d9fa..66239931ebc370294a59df5c1735b88068bc1e2a 100644 --- a/browse/test/gstack-update-check.test.ts +++ b/browse/test/gstack-update-check.test.ts @@ -7,7 +7,7 @@ */ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync } from 'fs'; +import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync, utimesSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; @@ -16,8 +16,8 @@ const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-update-check'); let gstackDir: string; let stateDir: string; -function run(extraEnv: Record = {}) { - const result = Bun.spawnSync(['bash', SCRIPT], { +function run(extraEnv: Record = {}, args: string[] = []) { + const result = Bun.spawnSync(['bash', SCRIPT, ...args], { env: { ...process.env, GSTACK_DIR: gstackDir, @@ -412,4 +412,56 @@ describe('gstack-update-check', () => { expect(exitCode).toBe(0); expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); }); + + // ─── --force flag tests ────────────────────────────────────── + + test('--force busts fresh UP_TO_DATE cache', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); + writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3'); + + // Without --force: cache hit, silent + const cached = run(); + expect(cached.stdout).toBe(''); + + // With --force: cache busted, re-fetches, finds upgrade + const forced = run({}, ['--force']); + expect(forced.exitCode).toBe(0); + expect(forced.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + }); + + test('--force busts fresh UPGRADE_AVAILABLE cache', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n'); + writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0'); + + // Without --force: cache hit, outputs stale upgrade + const cached = run(); + expect(cached.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + + // With --force: cache busted, re-fetches, now up to date + const forced = run({}, ['--force']); + expect(forced.exitCode).toBe(0); + expect(forced.stdout).toBe(''); + const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8'); + expect(cache).toContain('UP_TO_DATE'); + }); + + // ─── Split TTL tests ───────────────────────────────────────── + + test('UP_TO_DATE cache expires after 60 min (not 720)', () => { + writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n'); + writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n'); + writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3'); + + // Set cache mtime to 90 minutes ago (past 60-min TTL) + const ninetyMinAgo = new Date(Date.now() - 90 * 60 * 1000); + const cachePath = join(stateDir, 'last-update-check'); + utimesSync(cachePath, ninetyMinAgo, ninetyMinAgo); + + // Cache should be stale at 60-min TTL, re-fetches and finds upgrade + const { exitCode, stdout } = run(); + expect(exitCode).toBe(0); + expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0'); + }); }); diff --git a/gstack-upgrade/SKILL.md b/gstack-upgrade/SKILL.md index 42f13f69af879c4364ba215cb1aeab07dc686a5e..9d63565128fc2baeae54f9ebed28644ca40edf7b 100644 --- a/gstack-upgrade/SKILL.md +++ b/gstack-upgrade/SKILL.md @@ -189,4 +189,13 @@ After showing What's New, continue with whatever skill the user originally invok ## Standalone usage -When invoked directly as `/gstack-upgrade` (not from a preamble), follow Steps 2-6 above. If already on the latest version, tell the user: "You're already on the latest version (v{version})." +When invoked directly as `/gstack-upgrade` (not from a preamble): + +1. Force a fresh update check (bypass cache): +```bash +~/.claude/skills/gstack/bin/gstack-update-check --force +``` +Use the output to determine if an upgrade is available. + +2. If `UPGRADE_AVAILABLE `: follow Steps 2-6 above. +3. If no output (up to date): tell the user "You're already on the latest version (v{version})." diff --git a/gstack-upgrade/SKILL.md.tmpl b/gstack-upgrade/SKILL.md.tmpl index a199db6c3967017f16fb532560d52eb6118e5c23..a441b8d664f5b9c29862f1d1b9d2d6689c659135 100644 --- a/gstack-upgrade/SKILL.md.tmpl +++ b/gstack-upgrade/SKILL.md.tmpl @@ -187,4 +187,13 @@ After showing What's New, continue with whatever skill the user originally invok ## Standalone usage -When invoked directly as `/gstack-upgrade` (not from a preamble), follow Steps 2-6 above. If already on the latest version, tell the user: "You're already on the latest version (v{version})." +When invoked directly as `/gstack-upgrade` (not from a preamble): + +1. Force a fresh update check (bypass cache): +```bash +~/.claude/skills/gstack/bin/gstack-update-check --force +``` +Use the output to determine if an upgrade is available. + +2. If `UPGRADE_AVAILABLE `: follow Steps 2-6 above. +3. If no output (up to date): tell the user "You're already on the latest version (v{version})."