Skip to content

Add alert text for users to update gh#99

Merged
probablycorey merged 28 commits intomasterfrom
upgrade-gh-reminder
Dec 4, 2019
Merged

Add alert text for users to update gh#99
probablycorey merged 28 commits intomasterfrom
upgrade-gh-reminder

Conversation

@probablycorey
Copy link
Copy Markdown
Contributor

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

probablycorey and others added 6 commits November 20, 2019 11:07
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>
@probablycorey probablycorey requested a review from a team November 21, 2019 00:11
@probablycorey probablycorey self-assigned this Nov 21, 2019
mislav
mislav previously requested changes Nov 21, 2019
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start 🎉

I feel like two additional mechanisms would be necessary for the overall feature:

  1. CheckForUpdate being 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)
  2. 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.

"os"

"github.com/github/gh-cli/utils"
"github.com/mattn/go-isatty"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ¯_(ツ)_/¯

}
}

func getLastestVersion() (string, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "lastest" a typo or a cute misspelling :3


if updateAvailable {
handleUpdate <- func() {
fmt.Printf(utils.Cyan(`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

handleUpdate <- func() {
fmt.Printf(utils.Cyan(`
A new version of gh is available! %s → %s
Changelog: https://github.com/%s/releases/tag/%[2]s
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get the release URL from the release API record (since we've just queried it) instead of manually constructing it?

@@ -0,0 +1,59 @@
package command
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 👍🏼


func getLastestVersion() (string, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", nwo)
resp, err := http.Get(url)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started down this road, but quickly exited out for three main reasons:

  1. This calls an api endpoint on a public repo so we can call it with a plain GET call
  2. 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
  3. 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.

handleUpdate <- nil
}

updateAvailable := latestVersion != Version
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to point out: love the async implementation of this! Great job 👏

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ¯_(ツ)_/¯

@probablycorey probablycorey changed the title Upgrade gh reminder Update gh reminder Nov 21, 2019
@probablycorey probablycorey changed the title Update gh reminder Add alter text for users to update gh Nov 21, 2019
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely want people to in general use brew upgrade gh (no github/gh/ prefix necessary) instead of reinstall

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@probablycorey
Copy link
Copy Markdown
Contributor Author

Thanks for the review @mislav. I left some comments and made some additional changes. I'm holding off on this request 👇

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).

... 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.

@probablycorey probablycorey changed the title Add alter text for users to update gh Add alert text for users to update gh Nov 22, 2019
Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the updates!


func CheckForUpdate(handleUpdate chan func()) {
// Only check for updates in production
if os.Getenv("APP_ENV") != "production" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's worth updating utils and ui packages since they're basically dead code; we should clean them up instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we are still using the color code in util to color some text.

Copy link
Copy Markdown
Contributor Author

@probablycorey probablycorey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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())
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ¯_(ツ)_/¯

@nerdneha nerdneha added the alpha label Dec 3, 2019
main.go Outdated
}

func updateInBackground(updateMessageChan chan *string) {
if os.Getenv("APP_ENV") != "production" {
Copy link
Copy Markdown
Contributor

@mislav mislav Dec 3, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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=yes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GAH! I forget I'm living in a compiled app world now! Thanks

@probablycorey probablycorey requested a review from mislav December 3, 2019 21:24
- 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.
@mislav
Copy link
Copy Markdown
Contributor

mislav commented Dec 4, 2019

Pushed some updates: many fixes, more tests, plus some tweaks. See commit messages for more info.

To test the update notifier, build gh like so:

rm -f bin/gh; GH_VERSION=v0.2.3 LDFLAGS='-X main.updaterEnabled=github/homebrew-gh' make

Screen Shot 2019-12-04 at 3 16 51 PM

Copy link
Copy Markdown
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@probablycorey probablycorey merged commit 1289f35 into master Dec 4, 2019
@probablycorey probablycorey deleted the upgrade-gh-reminder branch December 4, 2019 18:12
@tierninho
Copy link
Copy Markdown
Contributor

Should it be "A new version of CLI is..." and not "gh"?

@mislav
Copy link
Copy Markdown
Contributor

mislav commented Dec 4, 2019

@tierninho Good point! We could go either way. Since the user is invoking the command as gh, I find it acceptable that we say "a new version of gh is available" since that references the name of the binary, and the binary is what was updated. But in future we can opt to call it "GitHub CLI" instead so we're consistent across the board.

@tierninho
Copy link
Copy Markdown
Contributor

Another thought is telling the user what the link is:

A new release of gh is available: 0.3.1 → v0.3.4
https://github.com/github/homebrew-gh/releases/tag/v0.3.4

At first glance it looks like that is where you should go to update. Perhaps this?:

A new release of gh is available: 0.3.1 → v0.3.4
Release notes: https://github.com/github/homebrew-gh/releases/tag/v0.3.4User 
Use `brew upgrade gh --fetch-HEAD` to upgrade now

@probablycorey
Copy link
Copy Markdown
Contributor Author

The problem with adding the brew upgrade... line is that it won't work for linux and windows users. But I think giving the users some action to start the upgrade is needed. We start by detecting the OS and given specific instructions for the OSes we can.

@mislav
Copy link
Copy Markdown
Contributor

mislav commented Dec 6, 2019

@tierninho: @probablycorey initially authored both "Release notes" and brew upgrade instructions but:

  1. I have removed “Release notes” because that link at the moment doesn't contain any release notes. @probablycorey did set up copying of release notes from the gh-cli repo to homebrew-gh repo, but I had disabled that because cross-references to specific commits/issues were copied, and they don't work when transplanted into another repository.

I'm open to bringing this back if we implement scrubbing of issue&commit cross-references in the release copying process.

  1. I have removed brew upgrade instructions because the same upgrade notice will show for people on Windows and Linux and macOS even if CLI wasn't installed by Homebrew, and if we display the brew upgrade instructions in that case, we're giving a user a command that will fail for them if they run it.

I'm open to providing the brew upgrade instructions if we first verify that the person had used Homebrew to install this instance of gh in the first place.

mislav pushed a commit that referenced this pull request Sep 28, 2021
preparatory cleanups to ssh tunnel/port forwarding code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants