module
Build::UpdateCheck
Overview
Once-per-day GitHub release check. Fully silent on any failure. Records every attempt (success or failure) so a bad network cannot cause every invocation to re-fetch. Disable with BUILD_NO_UPDATE_CHECK=1.
State is stored as an extended attribute on the user's ~/.netrc:
user.io.build.update-check = "
This keeps the state cost-free (no extra dotfile, no separate lockfile) and — critically — never touches the netrc's contents, so nothing we do here can corrupt the user's auth tokens. Individual setxattr/getxattr calls are atomic in the kernel, so no locking is needed either.
Fallback strategy: if we can't persist the state (Windows, or a filesystem like FAT/exFAT that returns ENOTSUP from setxattr), the check is skipped entirely rather than run without back-off. Without a working persistent cache we would otherwise hit GitHub on every invocation, which is exactly the "hammering when offline" behavior we're trying to avoid. Users on those platforms simply don't get update notices — that's the correct trade.
Startup latency is bounded by TOTAL_BUDGET. HTTP::Client's connect and
read timeouts don't cover the blocking getaddrinfo DNS lookup, so a
captive portal or dead resolver could otherwise hang startup for the OS
DNS timeout (many seconds). We work around this by running the fetch in
a fiber and racing it against select's timeout, and by pre-recording
a "none" result before the fetch so back-off is guaranteed even if
the process is killed mid-hang.
Defined in:
update_check.crConstant Summary
-
CACHE_TTL =
24.hours -
DISABLE_VAR =
"BUILD_NO_UPDATE_CHECK" -
GITHUB_REPO =
"buildio/cli" -
IO_TIMEOUT =
500.milliseconds -
Backup timeouts on the TCP connect and each socket read once DNS is done.
-
NO_VERSION =
"none" -
RELEASE_URL =
"https://api.github.com/repos/#{GITHUB_REPO}/releases/latest" -
SKIP_ARGV =
{"_complete", "--version", "-V", "--help", "-h"} -
Argv tokens that indicate a fast/non-interactive path where the check would add user-visible latency for no benefit (shell completion) or where the user is asking a specific quick question (--version / --help).
-
TOTAL_BUDGET =
700.milliseconds -
Wall-clock cap on how long we'll block the CLI waiting for the fetch. If exceeded the fetch fiber keeps running in the background — it may still populate the cache before the process exits, which surfaces the notice on the next invocation.
-
XATTR_NAME =
"user.io.build.update-check" -
user.* namespace is required by Linux for non-root xattr writes and is accepted by macOS without special handling.
Class Method Summary
- .check!(current : String, io : IO = STDERR) : Nil
- .disabled? : Bool
-
.fast_path? : Bool
Skip on shell completion and on --version / --help, which are the quick-answer paths where an extra HTTP round-trip is most annoying.
- .fetch : String | Nil
-
.interactive?(io : IO) : Bool
An interactive TTY is a prerequisite: notices to a redirected STDERR would be invisible, and non-TTY contexts (CI, subshells running completion) shouldn't pay the network latency.
- .netrc_path : String
- .newer?(latest : String, current : String) : Bool
- .read_entry : NamedTuple(checked_at: Time, latest_version: String | Nil) | Nil
-
.read_xattr(path : String) : String | Nil
Read the xattr as a UTF-8 string, or nil if absent / unsupported.
-
.record(latest : String | Nil) : Bool
Persists a new attempt as an xattr on the netrc file.
-
.refresh : String | Nil
Refresh the cached "latest" version with a strict wall-clock budget.
-
.write_xattr(path : String, value : String) : Bool
Returns true iff the syscall succeeded.
Class Method Detail
Skip on shell completion and on --version / --help, which are the quick-answer paths where an extra HTTP round-trip is most annoying.
An interactive TTY is a prerequisite: notices to a redirected STDERR would be invisible, and non-TTY contexts (CI, subshells running completion) shouldn't pay the network latency.
Read the xattr as a UTF-8 string, or nil if absent / unsupported.
Persists a new attempt as an xattr on the netrc file. Nil is stored as "none" to distinguish "we tried and failed" from "we never tried". Returns true iff the xattr was written; callers use false to mean "we have no working back-off cache — skip the network entirely".
Refresh the cached "latest" version with a strict wall-clock budget. Pre-records "none" so a hang or DNS block that outlives startup still counts toward back-off. The fetch fiber records its own result so that if it eventually beats the process exit (but not our budget), the next invocation still gets the notice.
If the pre-record fails (xattrs unsupported), skip the network fetch entirely: without a working back-off cache we'd hit GitHub on every invocation, which is worse than not checking at all.
Returns true iff the syscall succeeded. False signals to callers that this platform/filesystem can't persist state, so the check should be skipped rather than repeated without back-off.