Add alert text for users to update gh#99
Conversation
Co-Authored-By: Josh Abernathy <joshaber@gmail.com>
Co-Authored-By: Josh Abernathy <joshaber@gmail.com>
Co-Authored-By: Josh Abernathy <joshaber@gmail.com>
Co-Authored-By: Josh Abernathy <joshaber@gmail.com>
Co-Authored-By: Josh Abernathy <joshaber@gmail.com>
Co-Authored-By: Josh Abernathy <joshaber@gmail.com>
mislav
left a comment
There was a problem hiding this comment.
This is a great start 🎉
I feel like two additional mechanisms would be necessary for the overall feature:
CheckForUpdatebeing disabled by default (when e.g. cloning this repo and building from source) and explicitly enabled via build flag for releases (e.g. via goreleaser)- there should be an env flag to disable automatic upgrade lookup, in case users find upgrade notices annoying (even if they are spread around e.g. only once a week).
This extra functionality can by all means be addressed in followup PRs, but I think that this PR is not safe to merge as-is to master as the upgrade lookup is currently enabled by default.
command/checkForUpdate.go
Outdated
| "os" | ||
|
|
||
| "github.com/github/gh-cli/utils" | ||
| "github.com/mattn/go-isatty" |
There was a problem hiding this comment.
We now use mixed libraries for checking IsTerminal: golang.org/x/crypto/ssh/terminal and github.com/mattn/go-isatty. If I'm not mistaken, the latter is written in pure Go and works without CGO, but until CGO becomes a problem for us, maybe we can consider consistently using the former since it's more "official"?
There was a problem hiding this comment.
I don't have a strong opinion on this, so I'll change the few places we use go-isatty to use the terminal lib instead.
There was a problem hiding this comment.
I got rid of our dependency for go-isatty but it turns out that github.com/AlecAivazis/survey creates an indirect dependency. So we still have it in our go.mod file ¯_(ツ)_/¯
command/checkForUpdate.go
Outdated
| } | ||
| } | ||
|
|
||
| func getLastestVersion() (string, error) { |
There was a problem hiding this comment.
Is "lastest" a typo or a cute misspelling :3
command/checkForUpdate.go
Outdated
|
|
||
| if updateAvailable { | ||
| handleUpdate <- func() { | ||
| fmt.Printf(utils.Cyan(` |
There was a problem hiding this comment.
This will print to stdout, which might mess up someone script if they're piping e.g. gh pr list to somewhere else other than to a terminal. Can we ensure that this goes to stderr, or—even better—that the caller to CheckForUpdate() can decide which output stream the message goes to?
There was a problem hiding this comment.
I assumed that that the check on line 17 terminal.IsTerminal would handle this case. Wouldn't CheckForUpdate do an early return if gh was piped somewhere?
command/checkForUpdate.go
Outdated
| handleUpdate <- func() { | ||
| fmt.Printf(utils.Cyan(` | ||
| A new version of gh is available! %s → %s | ||
| Changelog: https://github.com/%s/releases/tag/%[2]s |
There was a problem hiding this comment.
Can we get the release URL from the release API record (since we've just queried it) instead of manually constructing it?
command/checkForUpdate.go
Outdated
| @@ -0,0 +1,59 @@ | |||
| package command | |||
There was a problem hiding this comment.
It surprised me that this functionality is in the command package, since it doesn't seem to be a command at all. If we follow the philosophy that seems popular in the Go community that “oversplitting” is generally better than large packages, can we consider putting the updater functionality into its own package?
There was a problem hiding this comment.
I'll move it into it's own package. At one point I was relying on private methods from command and that's why it was moved in there, but it's all self-contained now 👍🏼
command/checkForUpdate.go
Outdated
|
|
||
| func getLastestVersion() (string, error) { | ||
| url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", nwo) | ||
| resp, err := http.Get(url) |
There was a problem hiding this comment.
Should we consider using our own api package to handle some of the HTTP stuff for us? We could add functionalty to make REST calls in addition to existing GraphQL. We can also use that for forking, which seems at the time only available via REST.
Additionally, I think we should try to use the user's API token (if available) and set the appropriate User-Agent string. This would ensure that these lookups are subject to a more permissive rate limit and that they are appropriately tracked on our end as CLI activity.
There was a problem hiding this comment.
I started down this road, but quickly exited out for three main reasons:
- This calls an api endpoint on a public repo so we can call it with a plain GET call
- Making a generic REST endpoint based on this one call seemed like overkill because it wouldn't take advantage of much of the api package
- Because we only plan on running this once a day I wasn't concerned about rate limiting.
Once we have another REST endpoint I think it makes sense to move to the api package and makes something more generic, but for now I felt like it was too much code before we need it.
command/checkForUpdate.go
Outdated
| handleUpdate <- nil | ||
| } | ||
|
|
||
| updateAvailable := latestVersion != Version |
There was a problem hiding this comment.
I wonder if the version comparison should be more sophisticated, i.e. implement true semver comparison. If the current gh version was a prerelease, e.g. v1.0-beta1, and the latest stable version was v0.9, this will result in the message:
A new version of gh is available! v1.0-beta1 → v0.9
There was a problem hiding this comment.
This was an effort to minimize the complexity of the code. For now I thought the simple approach is better because we have a very simple release pattern (we just increment the release number). I may be naive in thinking this pattern will last us for awhile, so if you think we should get this in before it becomes a problem I can change my mind about it.
main.go
Outdated
| os.Exit(1) | ||
| } | ||
|
|
||
| (<-printUpdateMessage)() |
There was a problem hiding this comment.
Forgot to point out: love the async implementation of this! Great job 👏
There was a problem hiding this comment.
I played around with a few variations of this. It still feels weird to me, and I still have worries about it accidentally forgetting to send a message and hanging the main thread. But ¯_(ツ)_/¯
command/checkForUpdate.go
Outdated
| fmt.Printf(utils.Cyan(` | ||
| A new version of gh is available! %s → %s | ||
| Changelog: https://github.com/%s/releases/tag/%[2]s | ||
| Run 'brew upgrade github/gh/gh' to update!`)+"\n\n", Version, latestVersion, nwo) |
There was a problem hiding this comment.
We definitely want people to in general use brew upgrade gh (no github/gh/ prefix necessary) instead of reinstall
|
Thanks for the review @mislav. I left some comments and made some additional changes. I'm holding off on this request 👇
... it might make more sense to add that to the config file or other file that we use to store state. I'm not sure if we want to merge this without the state file that makes sure it doesn't alert people after EVERY command. So I'm going to work on the state file next in a follow up PR now. |
mislav
left a comment
There was a problem hiding this comment.
Thank you for the updates!
update/checkForUpdate.go
Outdated
|
|
||
| func CheckForUpdate(handleUpdate chan func()) { | ||
| // Only check for updates in production | ||
| if os.Getenv("APP_ENV") != "production" { |
There was a problem hiding this comment.
Nit: perhaps configuration should be passed down by the caller, not decided from within the update package. A good rule of thumb to be asking ourselves when extracting code into packages is: would this package be theoretically reusable in another application?
There was a problem hiding this comment.
This is a great idea and I like the reasoning behind it.
utils/color.go
Outdated
| return func(arg string) string { | ||
| output := arg | ||
| if isatty.IsTerminal(os.Stdout.Fd()) { | ||
| if terminal.IsTerminal(int(os.Stdout.Fd())) { |
There was a problem hiding this comment.
I don't think it's worth updating utils and ui packages since they're basically dead code; we should clean them up instead.
There was a problem hiding this comment.
It looks like we are still using the color code in util to color some text.
probablycorey
left a comment
There was a problem hiding this comment.
Ok, I think this is ready to go now. I made some changes that I documented below
| } | ||
|
|
||
| // REST performs a REST request and parses the response. | ||
| func (c Client) REST(method string, p string, body io.Reader, data interface{}) error { |
There was a problem hiding this comment.
I ended up writing a REST method for our client struct. Because it adds the token and user-agent headers like @mislav recommended. But mostly because it made it easier to test because of the RoundTrip stuff.
| func BasicClient() (*api.Client, error) { | ||
| return apiClientForContext(initContext()) | ||
| } | ||
|
|
There was a problem hiding this comment.
Because we are using the client from the update package I needed to make a public way to create the client with a context.
main.go
Outdated
|
|
||
| func main() { | ||
| updateMessageChan := make(chan *string) | ||
| go updateInBackground(updateMessageChan) |
There was a problem hiding this comment.
I pulled the goroutine/channel stuff out of the update package and put it in main, this made things a littler easier to deal with I think.
update/update.go
Outdated
| func UpdateMessage(client *api.Client) *string { | ||
| latestRelease, err := getLatestRelease(client) | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "%+v", err) |
There was a problem hiding this comment.
I wasn't sure what to do with this error, it felt like a bad idea to let it die silently. But it also felt pretty nasty to just dump it to stderr ¯_(ツ)_/¯
main.go
Outdated
| } | ||
|
|
||
| func updateInBackground(updateMessageChan chan *string) { | ||
| if os.Getenv("APP_ENV") != "production" { |
There was a problem hiding this comment.
This reads the variable from the user's environment at runtime, which most likely will never be set (or, possibly, might be set for a purpose different from GitHub CLI).
The release action sets the APP_ENV variable during build, but that affects nothing, since APP_ENV is checked at runtime, not at build time.
I would suggest that instead of using an environment variable, we check for a package variable that is by default disabled:
package main
var updaterEnabled = ""
func main() {
if updaterEnabled == "yes" {
// kick off updater logic
}
}And then enable that using build flags:
ldflags:
- -X github.com/github/gh-cli.updaterEnabled=yesThere was a problem hiding this comment.
GAH! I forget I'm living in a compiled app world now! Thanks
- Only report an update available if the version number of the release is greater than the current version - Removes `command` dependency from `update` package; instead, pass current version as an argument - Remove `brew upgrade` instructions since we can't be certain that gh was installed via Homebrew in the first place. - Does not check for updates unless stderr is a tty - Preserve stderr color output even if stdout is not a tty - Fixes stderr color output on Windows
To test the update notifier:
rm -f bin/gh; GH_VERSION=v0.2.3 LDFLAGS='-X main.updaterEnabled=github/homebrew-gh' make
- Individual components are now colored - We don't say "Release notes" anymore since the URL doesn't contain any release notes yet
- Check for updates even if `~/.config/gh` does not exist. In this case, the API call is unauthenticated. - Avoid having the update notifier ever triggering the OAuth flow.
|
Should it be "A new version of CLI is..." and not "gh"? |
|
@tierninho Good point! We could go either way. Since the user is invoking the command as |
|
Another thought is telling the user what the link is: At first glance it looks like that is where you should go to update. Perhaps this?: |
|
The problem with adding the |
|
@tierninho: @probablycorey initially authored both "Release notes" and
I'm open to bringing this back if we implement scrubbing of issue&commit cross-references in the release copying process.
I'm open to providing the |
preparatory cleanups to ssh tunnel/port forwarding code

We want to let users know there is an update, but we don't want to be annoying about it. I've broken this problem into two stages, this PR solves the first state. Why two stages? Because this ended up being a more complicated that I expected, mostly because it needs to store state and a CLI needs to use a persistent file to store state between calls.
Eventually it should work like I described it in this issue #85 (comment). For now this PR will check for an update every time and remind you to update. The next step is to only remind and check for updates every X hours.
Part 1 of #85