0031: The CLI checks for a newer release hourly, anonymously and in the background, and says so on stderr¶
Context and Problem Statement¶
People should learn that a newer Heinzelmännchen exists without watching the repository. ADR 0027
forbids update checks with identifying data. The first version of this decision asked only on demand
(hz system release) and showed a hint from that cache in hz --version and hz system info; in practice nobody ran
hz system release, so nobody saw a hint. The maintainer asked for the notice to show always, but never in JSON,
cached for an hour, with a short timeout, and without retrying on every command when offline. How does
Heinzelmännchen find out about a newer release, and who tells the user?
Decision Drivers¶
- ADR 0027: nothing identifying leaves the machine.
- No command gets noticeably slower, online or offline; an offline machine doesn't ask on every command.
- Machine output (JSON, MCP, REST) stays stable and free of messages meant for people (ADR 0003); pipes stay clean.
- Read-only operations are read-only in every head (ADR 0019).
Considered Options¶
- The CLI checks at most hourly in the background with a short timeout, caches successes and failures, and prints a notice on stderr after human output (chosen)
- Ask only on demand (
hz system release) and show a hint from that cache: the first version of this decision; the cache stays empty unless someone asks, so the hint almost never shows. - Check synchronously on every command: every command waits for the network, and offline for the timeout.
hz system update --check:updatechanges the installation, so MCP marks it destructive and REST makes it a POST; a flag cannot make one call read-only.- Send the user's GitHub token for a higher rate limit: a token identifies the user to GitHub, which ADR 0027
rules out for a check nobody asked for; 60 unauthenticated requests an hour are plenty for one check an hour. (The
user-initiated
hz system changelog --newermay send one; see below.)
Decision Outcome¶
Chosen option: the CLI checks at most hourly in the background with a short timeout, caches successes and failures, and prints a notice on stderr after human output, because people see the notice without asking, while no command waits and nothing about the user is sent.
- What is sent: one GET to the latest-release endpoint of the package source the installation came from, today the
GitHub releases API of the repository in the uv tool receipt (the default repository otherwise), with only a
heinzel/<version>User-Agent: no token, no identifier. The check asks firstgithub.com/<repository>/releases/latestwith a HEAD that is never followed: GitHub answers it with a redirect to the latest release's tag page, which spends none of the REST API's 60 unauthenticated requests an hour per address (a shared network exhausts them, and the check then never learns of a newer release). Only if that answer is unusable does it ask the releases API; both carry nothing but the User-Agent. The system module provides it to the kernel as aReleaseSource; heads never import GitHub code. - When: the
hzentry point starts the check on a daemon thread when the release cache (ReleaseCachein the kernel, in the cache directory) was last asked an hour ago or longer. The cache is stamped before asking, so a check that fails, times out or is cut short when the command ends also waits an hour; a failure is recorded with its reason (hz system doctorshows it). The timeout is 2 seconds per network operation (urllib's timeout applies to connecting and to each read, not in total), and after its own work a command waits at most 2 seconds in total for a check still running, then exits and drops it. JSON invocations and the servers start the check too; it writes only the cache. - Who tells: the notice "hz X.Y.Z is available (installed A.B.C): hz system update", with "What's new: hz system
changelog --newer" on a second line, comes from the cache only. The CLI prints it on stderr after the text output
of every command, after
hz --version, and in the footer ofhzandhz --help(the start screen), only when the setting is on, the stream is a terminal and no CI variable is set. Commands that say it themselves print no notice:hz system release,hz system update,hz system doctor(an informationalreleasecheck, which also says why the last check failed) andhz system changelog(a header, "You're on X; Y is available: hz system update. What's new: hz system changelog --newer", above the installed version's notes; the changelog itself asks nothing, and its JSON stays the installed changelog). Never in JSON, MCP or REST output; there the version is data:latest_releaseinhz system infoand the doctor'sreleasecheck. hz system releasealways asks, because someone asked, and caches the answer or the failure like the check.- What a newer release brings:
hz system changelog --newershows the release notes of every release newer than the installed version, newest first (--json: a list of{version, date, notes, url}), so people see what they get before they update; the notice, the changelog header,hz system releaseandhz system updatepoint to it. It uses the network only when asked: GETs to the releases API of the same repository throughheinzel.modules.system._github, theheinzel/<version>User-Agent, answers of at most 1 MB, at most 4 pages of 30 releases, 5 seconds per network operation. Because someone asked, it sends their GitHub token if one is set (the setting[system.changelog] github_token,HZ_SYSTEM_CHANGELOG__GITHUB_TOKEN,GH_TOKENorGITHUB_TOKEN), ashz system plugin searchdoes, for GitHub's higher rate limit; only to api.github.com (_githubrefuses any other URL and any redirect off api.github.com, so the token never reaches another host or plain http). Without a token the requests are unauthenticated and it works the same, at 60 requests an hour. Drafts and pre-releases are skipped. Release notes are third-party text: the module removes control characters and invisible format characters (bidirectional controls, zero-width and tag characters; the zero-width joiner of emoji stays), so every head, MCP and REST included, gets them clean; a release'surlis kept only if it is a plain release page of that repository; each release's notes are cut at 20,000 characters; and in the terminal links show their target. Offline or rate-limited, it fails with one error naming the reason and the releases page, exit code 1; rate-limited without a token, the error says thatGH_TOKENorGITHUB_TOKENraises the limit. Without--newerthe changelog and its JSON are unchanged. - Opt-out: the setting
update_check([hz] update_checkin the user, site orHZ_CONFIGfile,HZ_UPDATE_CHECK; a project'shz.tomlcannot set it, so a repository can't switch it on or off for the people who clone it), default on;falseturns both the check and the notice off. Any CI variable (CI,GITHUB_ACTIONS,GITLAB_CI, ...) turns both off too. hz system releasejoins thehz systemcommands of ADR 0018. A daemon job (ADR 0020) may refresh the same cache later, under the same setting.
This is consistent with ADR 0027: the request carries nothing about the user, and the setting switches it off.
Consequences¶
- Good: people learn about a release within an hour of using
hz, without asking and without waiting. - Good: offline or rate-limited costs one failed request an hour, never a slower command.
- Good: the kernel only caches a version and knows a
ReleaseSourceby type; GitHub stays in the system module (heinzel.modules.system._github). - Bad: GitHub sees one anonymous request an hour from each machine that uses
hz(its IP address), which the setting turns off. - Bad: a command that ends while the check is still running drops it; the next check is an hour later. The cache is
replaced atomically, so a dropped check can at worst leave a small
.release.json.<pid>file behind. - Bad: installs from other package sources (PyPI) still ask GitHub until the check learns their APIs.
Confirmation¶
tests/unit/kernel/release_test.py (freshness, stamping, failures), tests/unit/heads/cli_update_test.py (one check
an hour, negative cache, the budget with a slow source, CI and the setting), tests/integration/update_notice_test.py
(the notice on stderr only, never with --json, --format json or output_format = "json", the start screen,
--version, doctor, the changelog header, and the entry point checking once an hour with a fake source),
tests/unit/modules/changelog_test.py (--newer: newest first, drafts and pre-releases skipped, bounded pages,
cleaned and cut notes, no token unless one is set, the token from GH_TOKEN, GITHUB_TOKEN or the setting, never
following a redirect off api.github.com, the rate-limit hint, offline), tests/integration/update_notice_test.py::test_changelog_newer_* (text,
JSON and offline through the CLI with a fake GitHub),
tests/unit/kernel/installation_test.py::test_the_hz_probe_never_checks_for_updates,
tests/unit/modules/system_service_test.py::test_release_never_sends_a_token; the import-linter contract "Heads only
know what the kernel discovered" keeps GitHub out of the heads. The security-reviewer checks changes to the request
against this ADR.
More Information¶
- Answers the update-notifier question left open in ADR 0027.
- The check is automatic, cached for an hour (failures included), with a
short timeout, shown always but never in JSON, and switched off by the
update_checksetting and in CI. The first version asked only on demand. - Users should see what they get before installing a new release,
so
hz system changelog --newershows the notes of every newer release, and the notice andhz system updatepoint to it. hz system changelog --newersends the user's GitHub token if one is set, to api.github.com only, because the shared runners of CI exhausted the unauthenticated rate limit and the README test of--newerfailed; CI passes its read-onlyGITHUB_TOKEN. The hourly check andhz system releasestill never send a token.hz system plugin searchis a different, user-initiated search and may send the user's configured GitHub token.