-
Notifications
You must be signed in to change notification settings - Fork 80
feat: Add version check #310
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
base: main
Are you sure you want to change the base?
Conversation
|
Also, we can add a check to skip this for dev build I guess cause dev build has dev version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds automatic version checking functionality to notify users when a new version of sesh is available. The implementation fetches the latest release information from GitHub's API and compares it with the current version on every CLI invocation.
Key Changes:
- Added
getLatestVersion()to fetch the latest release tag from GitHub API - Added
isUpgradeAvailable()to compare semantic versions - Integrated version check into
NewRootCommand()initialization to display update notifications
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func isUpgradeAvailable(current, latest string) bool { | ||
| c := strings.TrimPrefix(current, "v") | ||
| l := strings.TrimPrefix(latest, "v") | ||
| cParts := strings.Split(c, ".") | ||
| lParts := strings.Split(l, ".") | ||
| for i := 0; i < len(cParts) && i < len(lParts); i++ { | ||
| cNum, _ := strconv.Atoi(cParts[i]) | ||
| lNum, _ := strconv.Atoi(lParts[i]) | ||
| if lNum > cNum { | ||
| return true | ||
| } else if lNum < cNum { | ||
| return false | ||
| } | ||
| } | ||
| return len(lParts) > len(cParts) | ||
| } |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version check functions (getLatestVersion and isUpgradeAvailable) lack test coverage. Given that the codebase has comprehensive test coverage for other components (e.g., lister, namer, tmux packages all have corresponding test files), these new functions should also be tested.
Consider adding a test file seshcli/root_command_test.go with tests for:
isUpgradeAvailablewith various version formats (e.g., "v1.2.3" vs "v1.2.4", "v2.0.0" vs "v1.9.9", equal versions, etc.)- Edge cases like empty strings, malformed versions, versions with different lengths
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why I'm recommending we move this to it's own package :)
| func getLatestVersion() (string, error) { | ||
| resp, err := http.Get("https://api.github.com/repos/joshmedeski/sesh/releases/latest") | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| defer resp.Body.Close() | ||
| var release struct { | ||
| TagName string `json:"tag_name"` | ||
| } | ||
| if err := stdjson.NewDecoder(resp.Body).Decode(&release); err != nil { | ||
| return "", err | ||
| } |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub API has rate limits (60 requests/hour for unauthenticated requests). When users hit this limit, they'll see no update notification and won't know why the check failed. Additionally, if many users run sesh frequently, they could exhaust their rate limit quickly.
Consider:
- Adding a User-Agent header to identify the client:
req.Header.Set("User-Agent", "sesh/"+version) - Logging rate limit errors to help users understand why updates aren't being checked
- Implementing caching to reduce API calls
| func getLatestVersion() (string, error) { | |
| resp, err := http.Get("https://api.github.com/repos/joshmedeski/sesh/releases/latest") | |
| if err != nil { | |
| return "", err | |
| } | |
| defer resp.Body.Close() | |
| var release struct { | |
| TagName string `json:"tag_name"` | |
| } | |
| if err := stdjson.NewDecoder(resp.Body).Decode(&release); err != nil { | |
| return "", err | |
| } | |
| // cache for latest version | |
| var latestVersionCache struct { | |
| version string | |
| timestamp int64 | |
| } | |
| func getLatestVersion(version string) (string, error) { | |
| // cache for 1 hour | |
| now := runtimewrap.Now().Unix() | |
| if latestVersionCache.version != "" && now-latestVersionCache.timestamp < 3600 { | |
| return latestVersionCache.version, nil | |
| } | |
| req, err := http.NewRequest("GET", "https://api.github.com/repos/joshmedeski/sesh/releases/latest", nil) | |
| if err != nil { | |
| return "", err | |
| } | |
| req.Header.Set("User-Agent", "sesh/"+version) | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| return "", err | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode == http.StatusForbidden { | |
| slog.Error("GitHub API rate limit exceeded. Update checks are temporarily disabled.") | |
| return "", errors.New("GitHub API rate limit exceeded") | |
| } | |
| if resp.StatusCode != http.StatusOK { | |
| return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) | |
| } | |
| var release struct { | |
| TagName string `json:"tag_name"` | |
| } | |
| if err := stdjson.NewDecoder(resp.Body).Decode(&release); err != nil { | |
| return "", err | |
| } | |
| latestVersionCache.version = release.TagName | |
| latestVersionCache.timestamp = now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this further supports my suggestion to limit how often we check for this.
| func getLatestVersion() (string, error) { | ||
| resp, err := http.Get("https://api.github.com/repos/joshmedeski/sesh/releases/latest") | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| defer resp.Body.Close() | ||
| var release struct { | ||
| TagName string `json:"tag_name"` | ||
| } | ||
| if err := stdjson.NewDecoder(resp.Body).Decode(&release); err != nil { | ||
| return "", err | ||
| } | ||
| return release.TagName, nil | ||
| } |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HTTP request to GitHub API lacks a timeout, which could cause the CLI to hang indefinitely if the network is slow or GitHub is unresponsive. This blocks the application startup for all users on every invocation.
Consider using an HTTP client with a reasonable timeout (e.g., 2-3 seconds):
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get("https://api.github.com/repos/joshmedeski/sesh/releases/latest")There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I highly recommend this, since sesh is always meant to be fast.
I think it's less likely users will go out of their way to explicitly check for updates (which is why you built this feature), but I'm thinking 1 second would be the longest we'd want to wait.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
| defer resp.Body.Close() | ||
| var release struct { | ||
| TagName string `json:"tag_name"` | ||
| } | ||
| if err := stdjson.NewDecoder(resp.Body).Decode(&release); err != nil { | ||
| return "", err | ||
| } | ||
| return release.TagName, nil |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The HTTP response status code is not checked before attempting to decode the body. A non-200 response (e.g., 404, 403 from rate limiting, or 500) will attempt to decode an error response as a release, leading to silent failures or incorrect behavior.
Add status code validation:
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably something simple like "Unable to determine if upgrade is available"
| cNum, _ := strconv.Atoi(cParts[i]) | ||
| lNum, _ := strconv.Atoi(lParts[i]) |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Version comparison silently ignores errors from strconv.Atoi, which means non-numeric version components (e.g., "2.0.0-beta", "1.2.3-rc1") will be treated as 0. This can lead to incorrect version comparison results.
Consider handling the error or using a proper semver library like github.com/Masterminds/semver/v3:
cNum, err := strconv.Atoi(cParts[i])
if err != nil {
return false // or handle pre-release versions properly
}
lNum, err := strconv.Atoi(lParts[i])
if err != nil {
return false
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't done pre-release versions before, but if I choose to start doing that I'll make sure to update this area of the code.
| latest, err := getLatestVersion() | ||
| if err == nil && isUpgradeAvailable(version, latest) { | ||
| fmt.Printf("A new version of sesh is available: %s (current: %s)\n", latest, version) | ||
| } |
Copilot
AI
Dec 8, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The version check is performed synchronously during CLI initialization, blocking the startup of every command for every user. This adds network latency (potentially 100ms-2000ms+) to all CLI invocations, even when users just want to run quick commands.
Consider one of these approaches:
- Run the version check in a goroutine to avoid blocking startup
- Cache the result with a TTL (e.g., check once per day)
- Only check on specific commands or with a
--check-updatesflag - Move this to a background service or scheduled task
Example with goroutine (non-blocking):
go func() {
latest, err := getLatestVersion()
if err == nil && isUpgradeAvailable(version, latest) {
fmt.Fprintf(os.Stderr, "A new version of sesh is available: %s (current: %s)\n", latest, version)
}
}()There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some interesting ideas here, let me know what you think
joshmedeski
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this is a great idea! I've got some feedback on code organization and functionality.
I think the main one for me is to not have this run every time any sesh command is run. Perhaps there is a more controlled way to present this to the user.
| ) | ||
|
|
||
| func getLatestVersion() (string, error) { | ||
| resp, err := http.Get("https://api.github.com/repos/joshmedeski/sesh/releases/latest") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use graphql or a query param to only return the tag_name instead of the entire payload?
| func getLatestVersion() (string, error) { | ||
| resp, err := http.Get("https://api.github.com/repos/joshmedeski/sesh/releases/latest") | ||
| if err != nil { | ||
| return "", err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming if there's no internet it just silently fails?
| "github.com/joshmedeski/sesh/v2/zoxide" | ||
| ) | ||
|
|
||
| func getLatestVersion() (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't want this feature to live in the root_command. Perhaps a "updater" package? Then we can add unit tests to ensure the isUpgradeAvailable has unit tests and mock the http response when testing other functionality
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will try to get this wrapped up in the coming weekend
|
|
||
| slog.Debug("seshcli/root_command.go: NewRootCommand", "version", version, "config", config) | ||
|
|
||
| latest, err := getLatestVersion() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is uncommon to see sesh's output as many commands switch the user to a different session.
What would be a good way to present this to the user? I'm thinking when a user calls sesh without an argument and/or calls the --version flag.
And I agree, during development we'll need a way to skip this all-together.
|
I came up with some more ideas for improving how upgrade notifications are handled, I'll work on these once we get this PR merged. |
Co-authored-by: Josh Medeski <[email protected]>
ref #199