Skip to content

Repeated checks for update when the command fails #12599

@jsoref

Description

@jsoref

CheckForUpdate behavior appears suboptimal as cli appears to do a ping to github on each run on macOS unless GH_NO_UPDATE_NOTIFIER is set because check for updates is a go routine that doesn't finish before the main task completes

$ for a in $(seq 2); do; echo "run $a:"; zGH_NO_UPDATE_NOTIFIER=1 GH_NO_EXTENSION_UPDATE_NOTIFIER=1 GH_HOST=localhost GH_DEBUG=api gh api /installation/repositories; echo; echo; done
run 1:
* Request at 2026-02-02 07:47:24.061823 -0500 EST m=+0.048067542
* Request to https://api.github.com/repos/cli/cli/releases/latest
> GET /repos/cli/cli/releases/latest HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Type: application/json; charset=utf-8
> Time-Zone: America/Toronto
> User-Agent: GitHub CLI 2.86.0

* Request at 2026-02-02 07:47:24.06955 -0500 EST m=+0.055794042
* Request to https://localhost/api/v3/installation/repositories
> GET /api/v3/installation/repositories HTTP/1.1
> Host: localhost
> Accept: */*
> Content-Type: application/json; charset=utf-8
> Time-Zone: America/Toronto
> User-Agent: GitHub CLI 2.86.0

* dial tcp 127.0.0.1:443: connect: connection refused
* Request took 2.102209ms
Get "https://localhost/api/v3/installation/repositories": dial tcp 127.0.0.1:443: connect: connection refused


run 2:
* Request at 2026-02-02 07:47:24.164912 -0500 EST m=+0.080609501
* Request to https://api.github.com/repos/cli/cli/releases/latest
> GET /repos/cli/cli/releases/latest HTTP/1.1
> Host: api.github.com
> Accept: application/vnd.github.merge-info-preview+json, application/vnd.github.nebula-preview
> Authorization: token ████████████████████
> Content-Type: application/json; charset=utf-8
> Time-Zone: America/Toronto
> User-Agent: GitHub CLI 2.86.0

* Request at 2026-02-02 07:47:24.229458 -0500 EST m=+0.145155209
* Request to https://localhost/api/v3/installation/repositories
> GET /api/v3/installation/repositories HTTP/1.1
> Host: localhost
> Accept: */*
> Content-Type: application/json; charset=utf-8
> Time-Zone: America/Toronto
> User-Agent: GitHub CLI 2.86.0

* dial tcp 127.0.0.1:443: connect: connection refused
* Request took 2.027834ms
Get "https://localhost/api/v3/installation/repositories": dial tcp 127.0.0.1:443: connect: connection refused

Compare:

for a in $(seq 2); do; echo "run $a:"; GH_NO_UPDATE_NOTIFIER=1 GH_NO_EXTENSION_UPDATE_NOTIFIER=1 GH_HOST=localhost GH_DEBUG=api gh api /installation/repositories; echo; echo; done
run 1:
* Request at 2026-02-02 07:48:00.697519 -0500 EST m=+0.067201085
* Request to https://localhost/api/v3/installation/repositories
> GET /api/v3/installation/repositories HTTP/1.1
> Host: localhost
> Accept: */*
> Content-Type: application/json; charset=utf-8
> Time-Zone: America/Toronto
> User-Agent: GitHub CLI 2.86.0

* dial tcp 127.0.0.1:443: connect: connection refused
* Request took 1.969375ms
Get "https://localhost/api/v3/installation/repositories": dial tcp 127.0.0.1:443: connect: connection refused


run 2:
* Request at 2026-02-02 07:48:00.816502 -0500 EST m=+0.103877293
* Request to https://localhost/api/v3/installation/repositories
> GET /api/v3/installation/repositories HTTP/1.1
> Host: localhost
> Accept: */*
> Content-Type: application/json; charset=utf-8
> Time-Zone: America/Toronto
> User-Agent: GitHub CLI 2.86.0

* dial tcp 127.0.0.1:443: connect: connection refused
* Request took 1.702458ms
Get "https://localhost/api/v3/installation/repositories": dial tcp 127.0.0.1:443: connect: connection refused

%[1]sGH_NO_UPDATE_NOTIFIER%[1]s: set to any value to disable GitHub CLI update notifications.
When any command is executed, gh checks for new versions once every 24 hours.
If a newer version was found, an upgrade notice is displayed on standard error.

If it's supposedly checking once every 24 hours, then why is it pinging each time it's run unless it's told not to? These two runs are seconds apart.

rel, err := checkForUpdate(updateCtx, cmdFactory, buildVersion)

I can see how to use GH_NO_UPDATE_NOTIFIER

func ShouldCheckForUpdate() bool {
if os.Getenv("GH_NO_UPDATE_NOTIFIER") != "" {
return false

to suppress:

cli/internal/ghcmd/cmd.go

Lines 216 to 225 in 150834b

func checkForUpdate(ctx context.Context, f *cmdutil.Factory, currentVersion string) (*update.ReleaseInfo, error) {
if updaterEnabled == "" || !update.ShouldCheckForUpdate() {
return nil, nil
}
httpClient, err := f.HttpClient()
if err != nil {
return nil, err
}
stateFilePath := filepath.Join(config.StateDir(), "state.yml")
return update.CheckForUpdate(ctx, httpClient, stateFilePath, updaterEnabled, currentVersion)

But, I can't find any evidence that this works:

func CheckForUpdate(ctx context.Context, client *http.Client, stateFilePath, repo, currentVersion string) (*ReleaseInfo, error) {
stateEntry, _ := getStateEntry(stateFilePath)
if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 {
return nil, nil
}

I can't trace this code...
https://github.com/search?q=repo%3Acli/cli%20StateDir&type=code

Imagines a StateDir but there's apparently no public implementation??

no sign of `state` in strace
$ docker run -it ubuntu
root@ae5a1f4233d4:/# apt update; apt install -y strace gh
...
root@ae5a1f4233d4:/# strace -ff /usr/bin/gh api /installation/repositories 2>&1 |grep -E -v 'futex|poll|connect|sock|peer|mprotect|sigaltstack|rt_|sleep|mmap.NULL,|mmap.0x|dbus-|prlimit|random|munmap|sched_|fcntl|/etc/ld.so|/sys/kernel|tgkill|SIGURG|mmap resumed|rseq|gettid|getpid|brk|madvise|ioctl'
execve("/usr/bin/gh", ["/usr/bin/gh", "api", "/installation/repositories"], 0xffffeb181998 /* 10 vars */) = 0
fstat(3, {st_mode=S_IFREG|0644, st_size=5199, ...}) = 0
close(3)                                = 0
openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0\360\206\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1722920, ...}) = 0
close(3)                                = 0
set_tid_address(0xffffb7e47fd0)         = 402
set_robust_list(0xffffb7e47fe0, 24)     = 0
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(3, "00400000-02dd4000 r-xp 00000000 "..., 1024) = 1024
read(3, "00 00:00 0 \nffffb7e49000-ffffb7e"..., 1024) = 497
close(3)                                = 0
read(3, "2097152\n", 20)                = 8
close(3)                                = 0
clone3({flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, child_tid=0xffff711cf250, parent_tid=0xffff711cf250, exit_signal=0, stack=0xffff709c0000, stack_size=0x80ea40, tls=0xffff711cf8c0}, 88) = -1 ENOSYS (Function not implemented)
clone(child_stack=0xffff711cea40, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTIDstrace: Process 403 attached
, parent_tid=[403], tls=0xffff711cf8c0, child_tidptr=0xffff711cf250) = 403
[pid   403] set_robust_list(0xffff711cf260, 24) = 0
[pid   402] clone(child_stack=0xffff6bffea40, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID <unfinished ...>
 <unfinished ...>
[pid   402] <... clone resumed>, parent_tid=[404], tls=0xffff6bfff8c0, child_tidptr=0xffff6bfff250) = 404
[pid   404] set_robust_list(0xffff6bfff260, 24 <unfinished ...>
[pid   404] <... set_robust_list resumed>) = 0
[pid   402] clone(child_stack=0xffff709bea40, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID <unfinished ...>
strace: Process 405 attached
[pid   402] <... clone resumed>, parent_tid=[405], tls=0xffff709bf8c0, child_tidptr=0xffff709bf250) = 405
[pid   405] set_robust_list(0xffff709bf260, 24 <unfinished ...>
[pid   405] <... set_robust_list resumed>) = 0
[pid   402] clone(child_stack=0xffff6b7eea40, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID <unfinished ...>
strace: Process 406 attached
[pid   406] set_robust_list(0xffff6b7ef260, 24) = 0
[pid   402] <... clone resumed>, parent_tid=[406], tls=0xffff6b7ef8c0, child_tidptr=0xffff6b7ef250) = 406
[pid   402] readlinkat(AT_FDCWD, "/proc/self/exe",  <unfinished ...>
[pid   402] <... readlinkat resumed>"/usr/bin/gh", 128) = 11
[pid   402] openat(AT_FDCWD, "/usr/bin/gh", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid   402] <... openat resumed>)       = 3
[pid   402] pipe2([5, 6], O_NONBLOCK|O_CLOEXEC) = 0
[pid   402] fstat(3, {st_mode=S_IFREG|0755, st_size=44919048, ...}) = 0
[pid   402] pread64(3,  <unfinished ...>
[pid   402] <... pread64 resumed>"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\2\0\267\0\1\0\0\0\300\26@\0\0\0\0\0"..., 64, 0) = 64
[pid   402] pread64(3,  <unfinished ...>
[pid   402] <... pread64 resumed>"\3A\371\202Z\0\260B\200>\221\243\2\200\322(\361\352\227\341\3~\262\1\4\0\371\1\10\0\371\373"..., 64, 5614881) = 64
[pid   402] pread64(3,  <unfinished ...>
[pid   402] <... pread64 resumed>"\1\371\340)\0\220\0\0 \221\371o\325\227\3413@\371\1\4\0\371;\5\1\220a\3I\271\201\0"..., 64, 11229762) = 64
[pid   402] pread64(3, "\1\0\0\0\0\340\26(\1\0\0\0\0\230\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\10\0\0"..., 64, 16844643) = 64
[pid   402] pread64(3,  <unfinished ...>
[pid   402] <... pread64 resumed>"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 64, 22459524) = 64
[pid   402] pread64(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 64, 28074405) = 64
[pid   402] pread64(3,  <unfinished ...>
[pid   402] <... pread64 resumed>"\4\0\0\0\0\0\0\0\0\0\240\4\0\0\0\0\0\0\0\0\0\240\244\4\0\0\0\0\0\0\0\0"..., 64, 33689286) = 64
[pid   402] pread64(3, "\4\20\2\3\1\2\2\2\0\0\6\2\10\2J\3\4\0\2\7\340\4J\337\4\1\340\4I\337\4\t"..., 64, 39304167) = 64
[pid   402] close(3)                    = 0
[pid   402] clone(child_stack=0xffff6afdea40, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID <unfinished ...>
strace: Process 407 attached
[pid   407] set_robust_list(0xffff6afdf260, 24) = 0
[pid   402] <... clone resumed>, parent_tid=[407], tls=0xffff6afdf8c0, child_tidptr=0xffff6afdf250) = 407
[pid   402] openat(AT_FDCWD, "/root/.config/gh/config.yml", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid   402] <... openat resumed>)       = -1 ENOENT (No such file or directory)
[pid   402] openat(AT_FDCWD, "/root/.config/gh/hosts.yml", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid   402] <... openat resumed>)       = -1 ENOENT (No such file or directory)
[pid   402] readlinkat(AT_FDCWD, "/proc/self/exe",  <unfinished ...>
[pid   402] <... readlinkat resumed>"/usr/bin/gh", 128) = 11
[pid   402] newfstatat(AT_FDCWD, "/usr/local/sbin/gh", 0x4000142038, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
[pid   402] newfstatat(AT_FDCWD, "/usr/local/bin/gh", 0x4000142338, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
[pid   402] newfstatat(AT_FDCWD, "/usr/sbin/gh",  <unfinished ...>
[pid   402] <... newfstatat resumed>0x40001423f8, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
[pid   402] newfstatat(AT_FDCWD, "/usr/bin/gh", {st_mode=S_IFREG|0755, st_size=44919048, ...}, AT_SYMLINK_NOFOLLOW) = 0
[pid   402] newfstatat(AT_FDCWD, "/etc/localtime", 0x4000142578, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
[pid   402] openat(AT_FDCWD, "/root/.local/share/gh/extensions", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid   402] <... openat resumed>)       = -1 ENOENT (No such file or directory)
[pid   402] newfstatat(AT_FDCWD, "/etc/localtime", 0x40000a27b8, AT_SYMLINK_NOFOLLOW) = -1 ENOENT (No such file or directory)
[pid   402] openat(AT_FDCWD, "/dev/tty", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid   402] <... openat resumed>)       = 3
[pid   402] close(3)                    = 0
[pid   402] getuid( <unfinished ...>
[pid   402] <... getuid resumed>)       = 0
[pid   402] close(3)                    = 0
[pid   402] close(3)                    = 0
[pid   402] newfstatat(AT_FDCWD, "/etc/nsswitch.conf", {st_mode=S_IFREG|0644, st_size=494, ...}, 0) = 0
[pid   402] newfstatat(AT_FDCWD, "/", {st_mode=S_IFDIR|0755, st_size=4096, ...}, 0) = 0
[pid   402] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
[pid   402] fstat(3, {st_mode=S_IFREG|0644, st_size=494, ...}) = 0
[pid   402] read(3, "# /etc/nsswitch.conf\n#\n# Example"..., 4096) = 494
[pid   402] read(3, "", 4096)           = 0
[pid   402] fstat(3, {st_mode=S_IFREG|0644, st_size=494, ...}) = 0
[pid   402] close(3 <unfinished ...>
[pid   402] <... close resumed>)        = 0
[pid   402] openat(AT_FDCWD, "/etc/passwd", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid   402] <... openat resumed>)       = 3
[pid   402] fstat(3, {st_mode=S_IFREG|0644, st_size=888, ...}) = 0
[pid   402] lseek(3, 0, SEEK_SET)       = 0
[pid   402] read(3, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 888
[pid   402] close(3)                    = 0
[pid   402] newfstatat(AT_FDCWD, "/run/user/0/bus", 0x40000a20f8, 0) = -1 ENOENT (No such file or directory)
[pid   402] openat(AT_FDCWD, "/etc/localtime", O_RDONLY <unfinished ...>
[pid   402] <... openat resumed>)       = -1 ENOENT (No such file or directory)
[pid   402] write(2, "* Request at 2026-02-02 13:02:53"..., 68* Request at 2026-02-02 13:02:53.799175345 +0000 UTC m=+0.074146417
) = 68
[pid   402] write(2, "* Request to https://localhost/a"..., 64* Request to https://localhost/api/v3/installation/repositories
) = 64
[pid   402] write(2, "> GET /api/v3/installation/repos"..., 49> GET /api/v3/installation/repositories HTTP/1.1
) = 49
[pid   402] write(2, "> Host: localhost\n", 18> Host: localhost
) = 18
[pid   402] write(2, "> Accept: */*\n", 14> Accept: */*
) = 14
[pid   402] write(2, "> Content-Type: application/json"..., 48> Content-Type: application/json; charset=utf-8
) = 48
[pid   402] write(2, "> User-Agent: GitHub CLI 2.45.0\n", 32> User-Agent: GitHub CLI 2.45.0
) = 32
[pid   402] write(2, "\n", 1
)           = 1
[pid   402] openat(AT_FDCWD, "/etc/resolv.conf", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid   402] <... openat resumed>)       = 3
[pid   402] fstat(3, {st_mode=S_IFREG|0644, st_size=233, ...}) = 0
[pid   402] read(3, "# Generated by Docker Engine.\n# "..., 65536) = 233
[pid   402] read(3, "", 65303)          = 0
[pid   402] read(3, "", 65536)          = 0
[pid   402] close(3)                    = 0
[pid   402] openat(AT_FDCWD, "/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
[pid   402] fstat(3, {st_mode=S_IFREG|0644, st_size=494, ...}) = 0
[pid   402] read(3, "# /etc/nsswitch.conf\n#\n# Example"..., 65536) = 494
[pid   402] read(3, "", 65042)          = 0
[pid   402] read(3, "", 65536)          = 0
[pid   402] close(3)                    = 0
[pid   402] newfstatat(AT_FDCWD, "/etc/hosts", {st_mode=S_IFREG|0644, st_size=172, ...}, 0) = 0
[pid   402] openat(AT_FDCWD, "/etc/hosts", O_RDONLY|O_CLOEXEC) = 3
[pid   402] read(3, "127.0.0.1\tlocalhost\n::1\tlocalhos"..., 65536) = 172
[pid   402] read(3, "", 65364)          = 0
[pid   402] read(3, "", 65536)          = 0
[pid   402] close(3)                    = 0
[pid   402] close(3)                    = 0
[pid   402] close(3)                    = 0
[pid   402] write(6, "\0", 1 <unfinished ...>
[pid   402] <... write resumed>)        = 1
[pid   407] read(5, "\0", 16)           = 1
[pid   407] close(3)                    = 0
[pid   407] write(6, "\0", 1 <unfinished ...>
[pid   407] <... write resumed>)        = 1
[pid   402] read(5,  <unfinished ...>
[pid   402] <... read resumed>"\0", 16) = 1
[pid   405] close(3)                    = 0
 <unfinished ...>
[pid   405] <... write resumed>)        = 50
[pid   405] write(2, "* Request took 9.522459ms\n", 26* Request took 9.522459ms
) = 26
) = 106
[pid   405] exit_group(1 <unfinished ...>
[pid   405] <... exit_group resumed>)   = ?
[pid   405] +++ exited with 1 +++
[pid   404] +++ exited with 1 +++
[pid   403] +++ exited with 1 +++
[pid   407] +++ exited with 1 +++
[pid   406] +++ exited with 1 +++
+++ exited with 1 +++
root@ae5a1f4233d4:/#

Note that I can't reproduce the update check behavior (see lack of requests in the ubuntu container). But on macOS it's consistent in performing an extra get for no apparent reason.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp3Affects a small number of users or is largely cosmetic

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions