diff --git a/.releaserc.cjs b/.releaserc.cjs index eff9aeab..ff0273aa 100644 --- a/.releaserc.cjs +++ b/.releaserc.cjs @@ -70,6 +70,8 @@ const devConfig = { '@semantic-release/github', { prerelease: true, + // Disable automatic success comment - custom step in dev-release.yml handles this + successComment: false, }, ], [ diff --git a/src/utils/update-checker.ts b/src/utils/update-checker.ts index c04dc585..d1f78934 100644 --- a/src/utils/update-checker.ts +++ b/src/utils/update-checker.ts @@ -17,6 +17,7 @@ const REQUEST_TIMEOUT = 5000; // 5 seconds interface UpdateCache { last_check: number; latest_version: string | null; + dev_version: string | null; // Version for @dev tag dismissed_version: string | null; } @@ -193,13 +194,18 @@ function fetchVersionFromNpmTag(tag: 'latest' | 'dev'): Promise { export function readCache(): UpdateCache { try { if (!fs.existsSync(UPDATE_CHECK_FILE)) { - return { last_check: 0, latest_version: null, dismissed_version: null }; + return { last_check: 0, latest_version: null, dev_version: null, dismissed_version: null }; } const data = fs.readFileSync(UPDATE_CHECK_FILE, 'utf8'); - return JSON.parse(data) as UpdateCache; + const parsed = JSON.parse(data) as UpdateCache; + // Ensure dev_version exists (backward compatibility) + if (parsed.dev_version === undefined) { + parsed.dev_version = null; + } + return parsed; } catch { - return { last_check: 0, latest_version: null, dismissed_version: null }; + return { last_check: 0, latest_version: null, dev_version: null, dismissed_version: null }; } } @@ -234,18 +240,31 @@ export async function checkForUpdates( const cache = readCache(); const now = Date.now(); - // Check if we should check for updates - if (!force && now - cache.last_check < CHECK_INTERVAL) { - // Use cached result if available - if ( - cache.latest_version && - compareVersionsWithPrerelease(cache.latest_version, currentVersion) > 0 - ) { + // Get cached version for the target tag + const cachedVersion = targetTag === 'dev' ? cache.dev_version : cache.latest_version; + + // Check if we should check for updates (only use cache for same-tag checks) + if (!force && now - cache.last_check < CHECK_INTERVAL && cachedVersion) { + // Use cached result if available for this tag + if (compareVersionsWithPrerelease(cachedVersion, currentVersion) > 0) { // Don't show if user dismissed this version - if (cache.dismissed_version === cache.latest_version) { + if (cache.dismissed_version === cachedVersion) { return { status: 'no_update', reason: 'dismissed' }; } - return { status: 'update_available', latest: cache.latest_version, current: currentVersion }; + return { status: 'update_available', latest: cachedVersion, current: currentVersion }; + } + // Check for channel switch: if current is on different channel than target + // Only trigger if target version matches the target channel type + const currentIsDevChannel = currentVersion.includes('-dev.'); + const targetIsDevChannel = targetTag === 'dev'; + const cachedIsDevVersion = cachedVersion.includes('-dev.'); + if ( + currentIsDevChannel !== targetIsDevChannel && + cachedIsDevVersion === targetIsDevChannel && + cachedVersion !== currentVersion + ) { + // Channel switch - treat as update available + return { status: 'update_available', latest: cachedVersion, current: currentVersion }; } return { status: 'no_update', reason: 'cached' }; } @@ -271,10 +290,14 @@ export async function checkForUpdates( if (!latestVersion) fetchError = 'github_api_error'; } - // Update cache + // Update cache with tag-specific version cache.last_check = now; if (latestVersion) { - cache.latest_version = latestVersion; + if (targetTag === 'dev') { + cache.dev_version = latestVersion; + } else { + cache.latest_version = latestVersion; + } } writeCache(cache); @@ -288,12 +311,36 @@ export async function checkForUpdates( } // Check if update available - if (latestVersion && compareVersionsWithPrerelease(latestVersion, currentVersion) > 0) { - // Don't show if user dismissed this version - if (cache.dismissed_version === latestVersion) { - return { status: 'no_update', reason: 'dismissed' }; + if (latestVersion) { + const versionComparison = compareVersionsWithPrerelease(latestVersion, currentVersion); + + // Update available if newer version + if (versionComparison > 0) { + // Don't show if user dismissed this version + if (cache.dismissed_version === latestVersion) { + return { status: 'no_update', reason: 'dismissed' }; + } + return { status: 'update_available', latest: latestVersion, current: currentVersion }; + } + + // Check for channel switch (stable <-> dev) + // Only trigger if: switching channels AND target version is on the target channel + const currentIsDevChannel = currentVersion.includes('-dev.'); + const targetIsDevChannel = targetTag === 'dev'; + const latestIsDevVersion = latestVersion.includes('-dev.'); + + // Channel switch is valid only when: + // 1. User is switching channels (current != target) + // 2. Target version matches the target channel type + // 3. Version is different from current + if ( + currentIsDevChannel !== targetIsDevChannel && + latestIsDevVersion === targetIsDevChannel && + latestVersion !== currentVersion + ) { + // Channel switch - treat as update even if "downgrade" + return { status: 'update_available', latest: latestVersion, current: currentVersion }; } - return { status: 'update_available', latest: latestVersion, current: currentVersion }; } return { status: 'no_update', reason: 'latest' }; diff --git a/tests/unit/utils/update-checker-beta-channel.test.js b/tests/unit/utils/update-checker-beta-channel.test.js index f25520a9..4cca9820 100644 --- a/tests/unit/utils/update-checker-beta-channel.test.js +++ b/tests/unit/utils/update-checker-beta-channel.test.js @@ -365,15 +365,17 @@ describe('Beta Channel Implementation (Phase 3)', function () { const cachePath = path.join(os.homedir(), '.ccs', 'cache', 'update-check.json'); const cacheData = JSON.parse(mockFileSystem[cachePath]); - assert.strictEqual(cacheData.latest_version, '5.5.0'); + // Dev versions are now stored in dev_version field (not latest_version) + assert.strictEqual(cacheData.dev_version, '5.5.0'); assert(cacheData.last_check > 0, 'should update timestamp'); }); it('should use cached result when within interval', async function () { - // Set up cache with recent check + // Set up cache with recent check - use dev_version for dev channel const cacheData = { last_check: Date.now() - 1000, // 1 second ago - latest_version: '5.5.0', + latest_version: null, + dev_version: '5.5.0', dismissed_version: null }; mockFileSystem[path.join(os.homedir(), '.ccs', 'cache', 'update-check.json')] = JSON.stringify(cacheData); @@ -475,10 +477,11 @@ describe('Beta Channel Implementation (Phase 3)', function () { }); it('should handle dismissed versions correctly', async function () { - // Set up cache with dismissed version + // Set up cache with dismissed version - use dev_version for dev channel const cacheData = { last_check: Date.now() - 1000, - latest_version: '5.5.0', + latest_version: null, + dev_version: '5.5.0', dismissed_version: '5.5.0' }; mockFileSystem[path.join(os.homedir(), '.ccs', 'cache', 'update-check.json')] = JSON.stringify(cacheData);