fix: update check cache — 60min UP_TO_DATE TTL + --force flag (v0.4.4) (#110)

* 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) <noreply@anthropic.com>

* feat: /gstack-upgrade standalone uses --force for fresh check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.4.4)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-03-16 14:14:15 -05:00
committed by GitHub
parent a68244ab57
commit c86faa7968
6 changed files with 104 additions and 16 deletions
+11
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
0.4.3
0.4.4
+17 -10
View File
@@ -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
+55 -3
View File
@@ -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<string, string> = {}) {
const result = Bun.spawnSync(['bash', SCRIPT], {
function run(extraEnv: Record<string, string> = {}, 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');
});
});
+10 -1
View File
@@ -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 <old> <new>`: follow Steps 2-6 above.
3. If no output (up to date): tell the user "You're already on the latest version (v{version})."
+10 -1
View File
@@ -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 <old> <new>`: follow Steps 2-6 above.
3. If no output (up to date): tell the user "You're already on the latest version (v{version})."