-
-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add update available notification #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+455
−3
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
5b613d0
feat: add update available notification
BYK 40a51ed
test: improve coverage for version-check module
BYK cb02ad0
refactor: address PR review feedback for version check
BYK eaceaed
fix: handle version check errors gracefully and remove unused code
BYK File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| /** | ||
| * Version check state persistence. | ||
| * | ||
| * Stores the last time we checked for updates and the latest known version | ||
| * in the metadata table for the "new version available" notification. | ||
| */ | ||
|
|
||
| import { getDatabase } from "./index.js"; | ||
| import { runUpsert } from "./utils.js"; | ||
|
|
||
| const KEY_LAST_CHECKED = "version_check.last_checked"; | ||
| const KEY_LATEST_VERSION = "version_check.latest_version"; | ||
|
|
||
| export type VersionCheckInfo = { | ||
| /** Unix timestamp (ms) of last check, or null if never checked */ | ||
| lastChecked: number | null; | ||
| /** Latest version string from GitHub, or null if never fetched */ | ||
| latestVersion: string | null; | ||
| }; | ||
|
|
||
| /** | ||
| * Get the stored version check state. | ||
| */ | ||
| export function getVersionCheckInfo(): VersionCheckInfo { | ||
| const db = getDatabase(); | ||
|
|
||
| const lastCheckedRow = db | ||
| .query("SELECT value FROM metadata WHERE key = ?") | ||
| .get(KEY_LAST_CHECKED) as { value: string } | undefined; | ||
|
|
||
| const latestVersionRow = db | ||
| .query("SELECT value FROM metadata WHERE key = ?") | ||
| .get(KEY_LATEST_VERSION) as { value: string } | undefined; | ||
|
|
||
| return { | ||
| lastChecked: lastCheckedRow ? Number(lastCheckedRow.value) : null, | ||
| latestVersion: latestVersionRow?.value ?? null, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Store the version check result. | ||
| * Updates both the last checked timestamp and the latest known version. | ||
| */ | ||
| export function setVersionCheckInfo(latestVersion: string): void { | ||
| const db = getDatabase(); | ||
| const now = Date.now(); | ||
|
|
||
| runUpsert(db, "metadata", { key: KEY_LAST_CHECKED, value: String(now) }, [ | ||
| "key", | ||
| ]); | ||
| runUpsert(db, "metadata", { key: KEY_LATEST_VERSION, value: latestVersion }, [ | ||
| "key", | ||
| ]); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| /** | ||
| * Background version check for "new version available" notifications. | ||
| * | ||
| * Checks GitHub releases for new versions without blocking CLI execution. | ||
| * Results are cached in the database and shown on subsequent runs. | ||
| */ | ||
|
|
||
| // biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import | ||
| import * as Sentry from "@sentry/bun"; | ||
| import { CLI_VERSION } from "./constants.js"; | ||
| import { | ||
| getVersionCheckInfo, | ||
| setVersionCheckInfo, | ||
| } from "./db/version-check.js"; | ||
| import { cyan, muted } from "./formatters/colors.js"; | ||
| import { fetchLatestFromGitHub } from "./upgrade.js"; | ||
|
|
||
| /** Target check interval: ~24 hours */ | ||
| const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; | ||
|
|
||
| /** Jitter factor for probabilistic checking (±20%) */ | ||
| const JITTER_FACTOR = 0.2; | ||
|
|
||
| /** Commands/flags that should not show update notifications */ | ||
| const SUPPRESSED_ARGS = new Set(["upgrade", "--version", "-V", "--json"]); | ||
|
|
||
| /** AbortController for pending version check fetch */ | ||
| let pendingAbortController: AbortController | null = null; | ||
|
|
||
| /** | ||
| * Determine if we should check for updates based on time since last check. | ||
| * Uses probabilistic approach: probability increases as we approach/pass the interval. | ||
| */ | ||
| function shouldCheckForUpdate(): boolean { | ||
| const { lastChecked } = getVersionCheckInfo(); | ||
|
|
||
| if (lastChecked === null) { | ||
| return true; | ||
| } | ||
|
|
||
| const elapsed = Date.now() - lastChecked; | ||
|
|
||
| // Add jitter to the interval (±20%) | ||
| const jitter = (Math.random() - 0.5) * 2 * JITTER_FACTOR; | ||
| const effectiveInterval = CHECK_INTERVAL_MS * (1 + jitter); | ||
|
|
||
| // Probability ramps up as we approach/exceed the interval | ||
| // At 0% of interval: ~0% chance | ||
| // At 100% of interval: ~63% chance (1 - 1/e) | ||
| // At 200% of interval: ~86% chance | ||
| const probability = 1 - Math.exp(-elapsed / effectiveInterval); | ||
|
|
||
| return Math.random() < probability; | ||
| } | ||
|
|
||
| /** | ||
| * Check if update notifications should be suppressed for these args. | ||
| */ | ||
| export function shouldSuppressNotification(args: string[]): boolean { | ||
| return args.some((arg) => SUPPRESSED_ARGS.has(arg)); | ||
| } | ||
|
|
||
| /** | ||
| * Abort any pending version check to allow process exit. | ||
| * Call this when main CLI work is complete. | ||
| */ | ||
| export function abortPendingVersionCheck(): void { | ||
| pendingAbortController?.abort(); | ||
| pendingAbortController = null; | ||
| } | ||
|
|
||
| /** | ||
| * Start a background check for new versions. | ||
| * Does not block - fires a fetch and lets it complete in the background. | ||
| * Reports errors to Sentry in a detached span for visibility. | ||
| * Never throws - errors are caught and reported to Sentry. | ||
| */ | ||
| function checkForUpdateInBackgroundImpl(): void { | ||
| try { | ||
| if (!shouldCheckForUpdate()) { | ||
| return; | ||
| } | ||
| } catch (error) { | ||
| // DB access failed - report to Sentry but don't crash CLI | ||
| Sentry.captureException(error); | ||
| return; | ||
| } | ||
|
|
||
| pendingAbortController = new AbortController(); | ||
| const { signal } = pendingAbortController; | ||
|
|
||
| Sentry.startSpanManual( | ||
| { | ||
| name: "version-check", | ||
| op: "version.check", | ||
| forceTransaction: true, | ||
| }, | ||
| async (span) => { | ||
| try { | ||
| const latestVersion = await fetchLatestFromGitHub(signal); | ||
| setVersionCheckInfo(latestVersion); | ||
| span.setStatus({ code: 1 }); // OK | ||
| } catch (error) { | ||
| // Don't report abort errors - they're expected when process exits | ||
| if (error instanceof Error && error.name !== "AbortError") { | ||
| Sentry.captureException(error); | ||
| } | ||
| span.setStatus({ code: 2 }); // Error | ||
| } finally { | ||
| pendingAbortController = null; | ||
| span.end(); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Get the update notification message if a new version is available. | ||
| * Returns null if up-to-date, no cached version info, or on error. | ||
| * Never throws - errors are caught and reported to Sentry. | ||
| */ | ||
| function getUpdateNotificationImpl(): string | null { | ||
| try { | ||
| const { latestVersion } = getVersionCheckInfo(); | ||
|
|
||
| if (!latestVersion) { | ||
| return null; | ||
| } | ||
|
|
||
| // Use Bun's native semver comparison (polyfilled for Node.js) | ||
| // order() returns 1 if first arg is greater than second | ||
| if (Bun.semver.order(latestVersion, CLI_VERSION) !== 1) { | ||
| return null; | ||
| } | ||
|
|
||
| return `\n${muted("Update available:")} ${cyan(CLI_VERSION)} → ${cyan(latestVersion)} Run ${cyan('"sentry upgrade"')} to update.\n`; | ||
| } catch (error) { | ||
| // DB access failed - report to Sentry but don't crash CLI | ||
| Sentry.captureException(error); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| // No-op implementations for when update check is disabled | ||
| function noop(): void { | ||
| // Intentionally empty - used when update check is disabled | ||
| } | ||
|
|
||
| function noopNull(): null { | ||
| return null; | ||
| } | ||
|
|
||
| // Export either real implementations or no-ops based on environment variable | ||
| const isDisabled = process.env.SENTRY_CLI_NO_UPDATE_CHECK === "1"; | ||
|
|
||
| export const maybeCheckForUpdateInBackground = isDisabled | ||
| ? noop | ||
| : checkForUpdateInBackgroundImpl; | ||
|
|
||
| export const getUpdateNotification = isDisabled | ||
| ? noopNull | ||
| : getUpdateNotificationImpl; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.