-
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?
Changes from all commits
ae60ef9
854e088
63265da
cc905db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,13 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package seshcli | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stdjson "encoding/json" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "log/slog" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -31,6 +35,38 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/joshmedeski/sesh/v2/zoxide" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func getLatestVersion() (string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp, err := http.Get("https://api.github.com/repos/joshmedeski/sesh/releases/latest") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm assuming if there's no internet it just silently fails? |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer resp.Body.Close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var release struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TagName string `json:"tag_name"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := stdjson.NewDecoder(resp.Body).Decode(&release); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return "", err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+49
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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.
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"
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
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.
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.
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
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
isUpgradeAvailablehas unit tests and mock the http response when testing other functionalityThere 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