fix(hooks): use semver comparison for update check instead of inequality (#1617)

* fix(hooks): use semver comparison for update check instead of inequality

gsd-check-update.js used `installed !== latest` to determine if an
update is available. This incorrectly flags an update when the installed
version is NEWER than npm (e.g., installing from git ahead of a release).

Replace with proper semver comparison: update_available is true only
when the npm version is strictly newer than the installed version.

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

* fix(hooks): use semver comparison for update check instead of inequality

gsd-check-update.js used `installed !== latest` to determine if an
update is available. This incorrectly flags an update when the installed
version is NEWER than npm (e.g., installing from git ahead of a release).

Fix:
- Move isNewer() inside the spawned child process (was in parent scope,
  causing ReferenceError in production)
- Strip pre-release suffixes before Number() to avoid NaN
- Apply same semver comparison to stale hooks check (line 95)

update_available is now true only when npm version is strictly newer.

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

* test: add semver comparison tests for gsd-check-update isNewer()

12 test cases covering: major/minor/patch comparison, equal versions,
installed-ahead-of-npm scenario, pre-release suffix stripping,
null/empty handling, two-segment versions.

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

* docs: explain why isNewer is duplicated in test file

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:
Maxim Brashenko
2026-04-04 16:10:21 +05:00
committed by GitHub
parent 5011ff1562
commit bd6a13186b
2 changed files with 95 additions and 2 deletions

View File

@@ -50,6 +50,18 @@ const child = spawn(process.execPath, ['-e', `
const path = require('path');
const { execSync } = require('child_process');
// Compare semver: true if a > b (a is strictly newer than b)
// Strips pre-release suffixes (e.g. '3-beta.1' → '3') to avoid NaN from Number()
function isNewer(a, b) {
const pa = (a || '').split('.').map(s => Number(s.replace(/-.*/, '')) || 0);
const pb = (b || '').split('.').map(s => Number(s.replace(/-.*/, '')) || 0);
for (let i = 0; i < 3; i++) {
if (pa[i] > pb[i]) return true;
if (pa[i] < pb[i]) return false;
}
return false;
}
const cacheFile = ${JSON.stringify(cacheFile)};
const projectVersionFile = ${JSON.stringify(projectVersionFile)};
const globalVersionFile = ${JSON.stringify(globalVersionFile)};
@@ -81,7 +93,7 @@ const child = spawn(process.execPath, ['-e', `
const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
if (versionMatch) {
const hookVersion = versionMatch[1].trim();
if (hookVersion !== installed && !hookVersion.includes('{{')) {
if (isNewer(installed, hookVersion) && !hookVersion.includes('{{')) {
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
}
} else {
@@ -100,7 +112,7 @@ const child = spawn(process.execPath, ['-e', `
} catch (e) {}
const result = {
update_available: latest && installed !== latest,
update_available: latest && isNewer(latest, installed),
installed,
latest: latest || 'unknown',
checked: Math.floor(Date.now() / 1000),