Skip to content

feat: say when a newer release is out - #96

Merged
groupsky merged 1 commit into
mainfrom
feat/newer-release
Oct 17, 2026
Merged

groupsky merged 1 commit into
mainfrom
feat/newer-release

Conversation

@groupsky

Copy link
Copy Markdown
Contributor

#88 (nodkey update, in #95) and #89 (the version header, in #93) are merged. This change builds on both.

Nothing told you that a newer release is out. Now nodkey version says it on a terminal, and the broker sends one silent Telegram notice.

What changed

  • internal/release (new package): the lookup of the newest release.
    • GET https://api.github.com/repos/code6x/nodkey/releases/latest, no login, with Accept: application/vnd.github+json, X-GitHub-Api-Version: 2022-11-28 and User-Agent: nodkey/<version>.
    • Its HTTP client is like the Telegram one: Proxy: http.ProxyFromEnvironment, TLS 1.2 or newer with the system roots, HTTP/1.1 only, no redirects (a redirect is an error with its status), 10 s for each step of a connection, 30 s for the whole lookup, no connection kept open.
    • The answer is at most 1 MiB. It reads only tag_name, draft and prerelease, with the exact key names (Go's decoder would also take TAG_NAME for a struct field). A draft, a prerelease, a missing key, or a tag that is not v + a release version is an error. A tag longer than 21 bytes (v999999.999999.999999) is refused before it is parsed: a tag of 1 MiB of dots cost about 17 MiB in version.ParseRelease, and now costs about 2 MiB. 403 and 429 give ErrRateLimit. Any other status but 200 is an error with the status only.
    • Every error has a fixed text. No text, header or link of the answer is ever shown or logged. The release page link is made from the version.
    • The base URL and the HTTP client are unexported fields, changed only by tests.
  • The broker's check (release.Check, started by nodkey broker): it waits 10 minutes after the start (release.FirstWait), then asks, and then every 24 hours; after a failed lookup or a failed notice, after 1 hour. So a broker that systemd starts again and again, and that ends within 10 minutes, never asks GitHub and sends no notice in each start. A newer release gets one line in the journal and one silent notice through telegram.Approver.Inform, which it shares with the notices of feat: warn when the client and the broker run different versions #89 (3 in any 60 s). A notice counts as sent only after Inform returns no error. A release that is not newer than the broker, or than a release that already got its notice, gets none. A build from the source, and [updates] check = false, never ask.
    • Start order: after the approver runs and the alerts are sent, before the server reads connections (new step 10 of DESIGN.md section 15). Stop order: the check stops first, before the polling (new step 1). Its wait, a lookup or a notice in progress is cancelled, also the dial of a lookup: Go's HTTP client goes on with a dial after its request ended, so Latest calls CloseIdleConnections when it returns, which ends such a dial. No request and no notice goes out after the step.
    • The notice text: ℹ️ nodkey: A newer release is out: nodkey 0.3.0. The broker runs nodkey 0.2.0. To update the broker, run as root on the broker host: nodkey update. Then restart it: systemctl restart nodkey. The restart ends all grants. Update the agent machines too: sudo nodkey update. Release notes: https://github.com/code6x/nodkey/releases/tag/v0.3.0
  • nodkey version [--offline]: stdout is the same nodkey <version> line as before. Then, only when stdout is a terminal (ioctl TCGETS, like isatty; only stdout decides), the version is a release and --offline is not given, it asks GitHub for at most 10 seconds and prints one line on stderr: a newer release (version, sudo nodkey update, release notes link), this is the newest release (also when GitHub's newest is older), or cannot check for a newer release: <fixed reason>. Exit code 0 also on errors. So $(nodkey version) in the installer of feat: install script and nodkey update #88 never calls GitHub and never gets another line.
  • Config: [updates] with check, true by default (also without the table). Strict as every table: unknown keys, wrong types, dotted keys and inline tables are errors.
  • Docs: DESIGN.md (section 4 table; section 7: the notice, its text and its limit; section 14: [updates]; section 15: the usage, nodkey version, the lookup, the new start step 10 and stop step 1, and the step numbers that moved; section 17: the tests), THREAT-MODEL.md (a row for GitHub's API in section 3, a note in T9, and a new T34: what GitHub learns; that a false answer, from GitHub, a proxy that inspects TLS or any CA the host trusts, can only give a wrong notice or hide one, because nodkey update checks the attestation and the version; that the notices cannot fill the chat; and two worse cases: an agent machine behind the same public address can use up GitHub's 60 calls an hour, so a security release gets no notice, and a release built from a merged pull request (T20) passes the attestation check, while the notice now asks every owner to install it within a day), docs/setup.md (section 2, the config key with a note on outbound HTTPS to api.github.com and HTTPS_PROXY, the proxy notes of section 8, and the notice in section 13), docs/releasing.md (step 3 of "Verify a download": a newer nodkey asks GitHub from the machine where you check it, and prints one more stderr line; ./nodkey version --offline skips that, and 0.1.x does not know --offline), the comment about the proxy in the example unit, and AGENTS.md (the new package).

Tests

No test reaches the real network:

  • The lookup tests use an httptest TLS server, with the real client (only the base URL and the CA pool differ): newer, same and older tags; bad tags (no v, leading zeros, junk, empty, a release candidate, too long, and more); a tag of 1 MiB of dots is refused with at most 6 MiB of allocation (without the length check: 18 MiB, and the test fails); a lookup that ends while the proxy holds the CONNECT ends its dial too (without the fix, the dial goes on, and the test fails after 10 s); draft, prerelease, missing or null keys; keys in another case; 403, 429, 500 and other statuses; exactly 1 MiB (read) and 1 MiB + 1 byte (error, also in chunks); five kinds of redirect (not followed: one request only); a server that hangs, and one that stops in the body; a cancelled context; an untrusted certificate; a TLS 1.1 server; no server; the headers of the request. A child process checks that the client takes HTTPS_PROXY from the environment: it may dial only the proxy, and the proxy sees CONNECT example.com:443 but not the path.
  • The broker's check runs with a fake lookup, a fake notice and a fake clock: the first wait is 10 minutes with no lookup before it, and a stop during it ends the check with no lookup, no notice and no log line; one notice per newer release, none for the same, an older, or the broker's own release, a second notice for a second newer release, 1 hour after each kind of failed lookup and after a failed or limited notice, 24 hours after success, the log lines, the deadline of each try, and that it stops during a lookup or a notice and sends nothing after.
  • nodkey version runs with a fake terminal and a fake lookup: each stderr line, stdout unchanged, exit 0 on errors; no lookup when stdout is not a terminal, with --offline, or for a source build; wrong usage gives 64 and no lookup. isTerminal is false for a pipe, a file, /dev/null, a closed file and a buffer, and true for /dev/ptmx.
  • nodkey broker in a child process: the fake proxy now also answers for api.github.com with a fake GitHub, with the same test CA. Without a seam, a release broker waits: it makes no call to GitHub before its stop, and the stop ends the wait first and is clean. With the seam fast-release-check (first wait 0), a release broker sends exactly one notice with the exact text, silent and without link preview, makes exactly one call to GitHub with the right headers, starts the check after the approver runs, stops it first, and stops cleanly. With check = false, and in a build from the source, there is no call to GitHub. TestBrokerChild now also refuses to start when calls to api.github.com would not go to the fake proxy.
  • The end-to-end test builds dev, so it never asks. It now also checks that the broker logs that the check is off. Its old check that the fake proxy saw only CONNECT api.telegram.org:443 stays.
  • Config: default true without the table and without the key, check = false, and errors for an unknown key, a key in upper case, a wrong type, a dotted key, an inline table, a subtable and [[updates]].

Results, on 2026-10-17, after the fixes of the security review:

command result
CI=true go test -race ./... (Go 1.27.1) pass
CI=true go test -race ./... (Go 1.26.0) pass
go vet ./... and go vet -tags e2e ./test/e2e/... pass
gofmt -l . no files
golangci-lint run --build-tags e2e (v2.14.0) 0 issues
CI=true go test -race -tags e2e ./test/e2e/... pass, all cases ran
go test -race -count=20 -cpu 1,2,4 ./internal/release/ pass
the same for the new tests of internal/config and cmd/nodkey (-run 'TestLoadBrokerUpdates|TestLoadBrokerErrors|TestLoadBrokerValid', and -run 'TestVersion|TestIsTerminal|TestBrokerRelease|TestBrokerVersionNotice|TestBrokerStartsAndStops|TestRun$') pass

By hand, with a release build on a pseudo-terminal (script) and HTTPS_PROXY set to a closed port on the loopback address, so nothing left the machine: the terminal gets nodkey 0.2.0 and nodkey: cannot check for a newer release: cannot reach GitHub: cannot connect to the proxy, exit 0. $(nodkey version), --offline, a dev build and >/dev/null print only the version line and make no call. gh api repos/code6x/nodkey/releases/latest shows that GitHub's real answer has tag_name v0.1.0, draft false and prerelease false, which the lookup accepts.

Left out, and open points

  • The rebase onto main (with feat: install script and nodkey update #88 and feat: warn when the client and the broker run different versions #89) had small text conflicts in the usage text of cmd/nodkey/main.go and in the command table and usage block of DESIGN.md. Both sides are kept. Tests pass again after it on Go 1.27 and 1.26.
  • The texts that say sudo nodkey update are part of install: low points from the review of #88 (sudo hints, TMPDIR, CA, tests) #94.
  • The security review of .claude/agents/security-reviewer.md ran on this change. It found nothing high or medium. Its low findings and notes are fixed here: the first wait (L1), the cases in T34 (L2), "only stdout decides" (N3), and the length cap of the tag (N4). The review of the fixes found a dial that went on after the stop; that is fixed too, with a test.
  • The first check comes 10 minutes after the start, not "at the start" as the issue says: a restart loop then sends no notice (security review, L1). A broker that crashes after 10 minutes or more again and again sends at most one notice in each start, about 6 an hour.
  • No change to nodkey update (feat: install script and nodkey update #88): it does its own lookup in install.sh.

Closes #90

@groupsky
groupsky merged commit 1ceee4d into main Oct 17, 2026
7 checks passed
@groupsky
groupsky deleted the feat/newer-release branch October 17, 2026 06:11
@groupsky groupsky mentioned this pull request Oct 17, 2026
4 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: say when a newer release is out

1 participant