Documentation
¶
Overview ¶
Package selfupdate replaces the running jaira binary with a published release.
core/release answers "what version am I, and what changed since an older one" — a question the binary can answer entirely from what is embedded in itself. This package answers a different question: "what version is published right now, and how do I become it" — which means touching the network and the filesystem's executable bit. Keeping the two apart keeps the blast radius of a bug in the self-replace path contained to one small package that release.go never has to import.
Index ¶
- Constants
- func AssetName(version, goos, goarch string) string
- func Disabled() bool
- func Overridden() []string
- func OverrideOf(name string) string
- func Path() string
- func PollCache() (latest string, known bool)
- func Replace(target string, bin []byte) error
- func SpawnRefresh() error
- func Stale(c Check, ok bool) bool
- func Sweep(target string) int
- func Write(c Check) error
- type Check
- type Client
- type Kind
- type Release
Constants ¶
const MaxAge = time.Hour
MaxAge is how long a cached Check is trusted before it counts as stale. An hour, so a release reaches a session that is already running within the hour it is known, not the next day (T8A8KM; it was a day before). A refresh is one detached HTTPS request, at most one an hour per machine.
Variables ¶
This section is empty.
Functions ¶
func AssetName ¶
AssetName returns the archive name goreleaser publishes for a version and platform. This must stay in step with .goreleaser.yaml's default archive name_template — that file names the thing this function must keep predicting.
func Disabled ¶
func Disabled() bool
Disabled reports whether the background release check has been switched off.
func Overridden ¶
func Overridden() []string
Overridden reports which of the two host overrides are in effect, so a command can name them before it replaces a binary with what they served.
func OverrideOf ¶
OverrideOf reads a remote-host override from the environment, refusing any scheme that is not https or a loopback http address.
These overrides move the trust root, not just the address: checksums.txt is fetched from the same base as the archive it certifies, so pointing them somewhere else means the verification compares an archive against that same host's own digest. The end of that path is Replace() writing over the running binary.
They exist because the test suite serves a whole release from an httptest.Server rather than requiring network access, which is why plain http on a loopback address is still allowed. Everything else must be https: a downgrade to cleartext would leave the one remaining protection — TLS to a host somebody chose deliberately — with nothing behind it.
Overriding is also reported by the commands that act on it, so a value set in a shell profile or a CI job cannot redirect an upgrade silently.
func Path ¶
func Path() string
Path is where the cache file lives: JAIRA_HOME if set, else ~/.jaira, never inside a repository. Which release is published is a fact about the machine, not about a working tree — putting it per-tree would make someone with ten clones run ten checks an hour for the same answer.
func PollCache ¶
PollCache returns the last known "latest published release" answer, refreshing it in the background first if it is stale.
This is the one non-network entry point a consumer should read through instead of composing Read/Stale/Write/SpawnRefresh itself — its consumers are the TUI's version indicator and 'jaira next' / 'resume' / 'claim' / 'move'. It never blocks on the network: a stale cache gets touched and handed to a detached child (see SpawnRefresh's own doc comment for why), and this function returns immediately with whatever was already known, which may be nothing at all.
Disabled() short-circuits before touching anything, so an opted-out machine gets neither a spawned refresh nor a reported answer.
known is false exactly when nothing has actually been learned yet — the check is disabled, or the cache is absent/unparseable and no prior write ever recorded a real Latest. A caller must not render "up to date" on a false known: that would assert a fact nobody has checked.
func Replace ¶
Replace atomically installs bin at target.
A running process on unix keeps its inode open even after the file at its path is replaced, so renaming a new binary over a running one is safe: the process currently executing target keeps running from the old inode until it exits, and the next invocation of the path picks up the new file. There is nothing else to do here.
func SpawnRefresh ¶
func SpawnRefresh() error
SpawnRefresh starts a detached child that refreshes the cache and does not wait for it.
The child gets JAIRA_NO_UPDATE_CHECK=1 as a recursion guard: it must never itself decide the cache is stale and spawn a grandchild. That happens to be the same switch a user flips to turn the whole thing off, so there is one mechanism here, not two.
There is no Wait() — that is the whole point. The parent is about to exit; the child is reparented to init and reaped there. There is deliberately no attempt to setsid: that needs per-OS SysProcAttr and buys only immunity from a Ctrl-C sent to the process group, and losing a best-effort release check to a Ctrl-C costs nothing — the cache stays stale and the next command retries.
Stdio goes to os.DevNull rather than being inherited, because a child writing to the parent's terminal after the parent has already exited is the exact failure this whole design exists to avoid.
func Stale ¶
Stale reports whether c should be refreshed: an absent or unparseable cache (ok == false) always is, and so is one whose CheckedAt is more than MaxAge old.
Types ¶
type Check ¶
Check is what gets cached: the answer to "what is the latest published release", and when that answer was last obtained.
func Read ¶
Read returns the cached Check, or (Check{}, false) on any error. A missing, truncated or garbage cache is indistinguishable from "never checked" for every purpose this serves, so every error is swallowed here the same way release.Stamped swallows its own — a best-effort informational cache must never be able to fail a command.
type Client ¶
Client talks to the release host.
func New ¶
func New() *Client
New returns a Client with an explicit timeout. The zero-value http.Client has no timeout at all, so a hung TLS handshake would hang the command forever — and this command sometimes runs detached (see core/selfupdate's cache.go), where nobody would ever see it stuck.
func (*Client) At ¶
At resolves an explicit tag or version to a Release without calling the API at all. It normalizes the tag (adding a leading "v" if the caller gave a bare version), then confirms the release exists by fetching checksums.txt for it — mapping a 404 to a "no such release" error.
This skips releases/tags/<tag> deliberately: that would be a second GitHub payload shape to model and to keep working, when the artifact this actually needs already proves the release exists.
func (*Client) Binary ¶
Binary downloads, verifies and extracts the jaira executable for rel on the given platform. Nothing is written to disk anywhere in this function: a verification failure must not be able to leave a partial file that some other code path could pick up. The order below — checksums first, then the archive, then the hash comparison, then extraction — is the security property itself: no byte of the archive is ever interpreted before it is known to be the byte-for-byte published one.
func (*Client) DownloadURL ¶
DownloadURL returns the address of a named asset in a tagged release.
type Kind ¶
type Kind string
Kind classifies how the running binary got onto this machine, which decides whether 'jaira self upgrade' is allowed to touch it.
const ( // SelfManaged means nothing else claims to own this file; upgrading it // in place is safe. SelfManaged Kind = "self-managed" // Managed means a package manager (Homebrew) owns this file. Managed Kind = "homebrew" // GoInstall means 'go install' put this file where it is. GoInstall Kind = "go-install" // Unwritable means neither of the above applies, but the directory // cannot be written to anyway. Unwritable Kind = "unwritable" )
func Detect ¶
Detect classifies target and, for anything other than SelfManaged, returns the instruction to print instead of upgrading.
This exists because replacing a file a package manager owns leaves that manager's database lying about what is installed — its next operation (a `brew upgrade`, a `go install` of something else) will silently step on or contradict what this command just did. Refusing is the correct behavior here, not a limitation: `uv self update` draws exactly this same line for the same reason.
The checks run in this order — Homebrew, then go-install, then writability — because ownership is a stronger fact than writability. A Homebrew prefix is usually writable by its owning user, so checking writability first would misclassify a Homebrew-owned file as merely self-managed-and-writable.
type Release ¶
Release identifies one published release.
Tag carries the leading "v" — GitHub tags do, and DownloadURL's path needs it. Version does not — release.Current and the archive name never have one. Getting the two backwards is the obvious bug here, which is exactly why they are two named fields instead of one string reused two ways.