新しいgithub CLI toolのghコマンドが更新チェックをいつ行っているのか気になったので調べた

github.com

最近githubhubの代わりにghという新しいgithub用のCLIツールを出していました。cli/cliという位置にあるので組織名とリポジトリ名がすごい。特等席。

例えばmacでは以下の様な形でインストールすると、このghコマンドが使える様になる。

$ brew install github/gh/gh

# upgrade
# brew update && brew upgrade gh

現在のcwdが所属していそうなgithub repositoryに対してその場でPRの作成ができたり、ステータスが見れたりとけっこう便利。

まぁそれは置いておいて、このghコマンドを使っていたときに、どうも自動で更新チェックをやってくれるようだった。これをどのタイミングでやっているのかなーと気になったのでソースコードを覗いて見たらなるほど~と思うことがあったのでメモをしておく。

ghコマンドの更新チェックのメッセージ

ghコマンドにアップデートがあったときにはどのようなメッセージが表示されるかというと以下の様な表示がされる。

$ gh pr status
...
A new release of gh is available: 0.5.4 → v0.5.7
https://github.com/cli/cli/releases/tag/v0.5.7

まぁそういう感じで最新のバージョンを使うことを促される。 この更新チェックを行っている処理があるはず。

更新チェックを行っているコード

そんなわけでmain.goを覗いてみるとほとんどこのファイルそのものが答え。

なるほど、立ち上がると同時に徐にgoroutineを動かしていた。なるほど。これは賢い。

こういう感じのコードになっている(コメントは勝手に追加したもの)。

func main() {
    currentVersion := command.Version

// 更新確認のためのgoroutineを起動
    updateMessageChan := make(chan *update.ReleaseInfo)
    go func() {
        rel, _ := checkForUpdate(currentVersion)
        updateMessageChan <- rel
    }()

// 通常の処理
    hasDebug := os.Getenv("DEBUG") != ""
    if cmd, err := command.RootCmd.ExecuteC(); err != nil {
        printError(os.Stderr, err, cmd, hasDebug)
        os.Exit(1)
    }

// 起動したgoroutineを待つ
    newRelease := <-updateMessageChan
    if newRelease != nil {

// 更新があったときのメッセージ
        msg := fmt.Sprintf("%s %s → %s\n%s",
            ansi.Color("A new release of gh is available:", "yellow"),
            ansi.Color(currentVersion, "cyan"),
            ansi.Color(newRelease.Version, "cyan"),
            ansi.Color(newRelease.URL, "yellow"))

        stderr := utils.NewColorable(os.Stderr)
        fmt.Fprintf(stderr, "\n\n%s\n\n", msg)
    }
}

なるほど。コマンドを立ち上げたタイミングで安直にgoroutineを立ち上げてしまえば良い。便利。

あとはふつうに通常の処理を書いてしまって、終了したタイミングでgoroutineを待てば良い。ふつう実行自体が終わったタイミングでは終了しているだろうし、githubに対するコマンドなどというのはネットワークがつながっていなければ何もできないのだから、常にrequestしてしまっても良い。

これは何か他にCLIのツールを作るときには参考になるかもしれないなーと思ったりした。

ちなみに更新チェックの処理自体の実装は

ちなみにcheckforUpdate()自体は以下の様な実装になっていて、pipeでつなげていたりしたときには省略されそうな感じ。

func shouldCheckForUpdate() bool {
    return updaterEnabled != "" && utils.IsTerminal(os.Stderr)
}

func checkForUpdate(currentVersion string) (*update.ReleaseInfo, error) {
    if !shouldCheckForUpdate() {
        return nil, nil
    }

    client, err := command.BasicClient()
    if err != nil {
        return nil, err
    }

    repo := updaterEnabled
    stateFilePath := path.Join(context.ConfigDir(), "state.yml")
    return update.CheckForUpdate(client, stateFilePath, repo, currentVersion)
}

処理自体も特に複雑なことをしているわけではなくgithubのreleases apiを呼んで良い感じにやっていっているだけの模様。

$ http -b https://api.github.com/repos/cli/cli/releases
[
    {
        "assets": [
            {
                "browser_download_url": "https://github.com/cli/cli/releases/download/v0.5.7/gh_0.5.7_checksums.txt",
                "content_type": "text/plain; charset=utf-8",
                "created_at": "2020-02-20T22:23:34Z",
                "download_count": 16,
                "id": 18185843,
                "label": "",
                "name": "gh_0.5.7_checksums.txt",
                "node_id": "MDEyOlJlbGVhc2VBc3NldDE4MTg1ODQz",
                "size": 1100,
                "state": "uploaded",
                "updated_at": "2020-02-20T22:23:35Z",
                "uploader": {
                    "avatar_url": "https://avatars2.githubusercontent.com/u/887?v=4",
                    "events_url": "https://api.github.com/users/mislav/events{/privacy}",
                    "followers_url": "https://api.github.com/users/mislav/followers",
                    "following_url": "https://api.github.com/users/mislav/following{/other_user}",
                    "gists_url": "https://api.github.com/users/mislav/gists{/gist_id}",
                    "gravatar_id": "",
                    "html_url": "https://github.com/mislav",
                    "id": 887,
                    "login": "mislav",
                    "node_id": "MDQ6VXNlcjg4Nw==",
                    "organizations_url": "https://api.github.com/users/mislav/orgs",
                    "received_events_url": "https://api.github.com/users/mislav/received_events",
                    "repos_url": "https://api.github.com/users/mislav/repos",
                    "site_admin": true,
                    "starred_url": "https://api.github.com/users/mislav/starred{/owner}{/repo}",
                    "subscriptions_url": "https://api.github.com/users/mislav/subscriptions",
                    "type": "User",
                    "url": "https://api.github.com/users/mislav"
                },
                "url": "https://api.github.com/repos/cli/cli/releases/assets/18185843"
            },
...
]