From c8bc1928f28618136153ff85738a10433551513c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 12 Aug 2026 18:35:34 +0200 Subject: [PATCH] Get the GitHub token from gh instead of resolving it in-process go-gh reads gh's config file once per process and answers from that snapshot for the rest of the process's life. gh rewrites the file whenever the active account changes, and stores the active account's token either in it or in the system keyring, depending on the account. A lazygit that has been running for a while therefore consults a snapshot that no longer describes reality: it either keeps using a token for an account that is no longer active, or, when the snapshot was taken while a keyring-backed account was active, finds no token at all and silently stops showing pull requests until it is restarted. Asking gh resolves the token afresh on every refresh, from whichever of the environment, the keyring or the config file currently holds it. go-gh's lookup stays behind as a fallback for setups without the gh binary, where it still picks up GH_TOKEN and friends. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/commands/git_commands/github.go | 48 +++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/pkg/commands/git_commands/github.go b/pkg/commands/git_commands/github.go index 3a61469233f..2b0568685be 100644 --- a/pkg/commands/git_commands/github.go +++ b/pkg/commands/git_commands/github.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "net/http" + "os" + "os/exec" "regexp" "strings" "time" @@ -160,9 +162,51 @@ func fetchPullRequestsQuery(branches []string, owner string, repo string) (strin return queryString, variables } +// GetAuthToken returns the token to authenticate against the given host with, +// or an empty string if there is none. +// +// The token has to come from gh itself rather than from an in-process lookup +// with go-gh: that reads gh's config file once per process and answers from +// that snapshot ever after, whereas gh rewrites the file whenever the active +// account changes, and keeps the active account's token either there or in the +// system keyring. Under a long-running lazygit the snapshot therefore drifts +// out of date, leaving us with a token for an account that is no longer active, +// or with no token at all. func (self *GitHubCommands) GetAuthToken(host string) string { - token, _ := auth.TokenForHost(host) - return token + ghExe := ghExecutable() + if ghExe == "" { + // Without gh installed, the environment variables and config file that + // gh would have consulted are still worth a look. + token, _ := auth.TokenFromEnvOrConfig(host) + return token + } + + cmdArgs := []string{ghExe, "auth", "token", "--hostname", host} + output, _, err := self.cmd.New(cmdArgs).DontLog().RunWithOutputs() + if err != nil { + // Not being logged in to this host is a normal state rather than + // something to report; the runner logs gh's stderr for the rest. + return "" + } + + return strings.TrimSpace(output) +} + +// ghExecutable returns the path of the gh binary, or an empty string if it +// isn't installed. +func ghExecutable() string { + if ghExe := os.Getenv("GH_PATH"); ghExe != "" { + return ghExe + } + + // A gh found in the current directory rather than on PATH comes back as + // exec.ErrDot, which we treat as not having found one at all. + ghExe, err := exec.LookPath("gh") + if err != nil { + return "" + } + + return ghExe } // FetchRecentPRs fetches recent pull requests using GraphQL. serviceInfo