From 443763489b18d43f313cffab36249880eaf4f27e Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 4 Jun 2026 14:48:44 -0700 Subject: [PATCH] =?UTF-8?q?C2(hephd-self-update):=20finalize=20=E2=80=94?= =?UTF-8?q?=20single=20self-update=20how-to=20+=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse the eight Mikado scaffolding cards (+ goal card) into one user-facing how-to, docs/how-to/self-update.md: what self-update is and how to enable it. The per-card breakdown was build-time scaffolding, not documentation. Keeps the changelog fragment; updates the how-to index. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/changelog.d/hephd-self-update.feature.md | 1 + docs/how-to/how-to.md | 15 +---- docs/how-to/self-update.md | 56 +++++++++++++++++++ .../self-update/cargo-install-from-tag.md | 37 ------------ docs/how-to/self-update/hephd-self-update.md | 51 ----------------- .../self-update/release-poll-version-check.md | 31 ---------- .../self-update/self-restart-after-update.md | 33 ----------- .../self-update/self-update-opt-in-flag.md | 31 ---------- .../self-update/self-update-poll-loop.md | 33 ----------- .../self-update/service-env-forge-access.md | 41 -------------- .../service-respawn-on-clean-exit.md | 34 ----------- .../verify-hub-dropout-resilience.md | 36 ------------ 12 files changed, 58 insertions(+), 341 deletions(-) create mode 100644 docs/changelog.d/hephd-self-update.feature.md create mode 100644 docs/how-to/self-update.md delete mode 100644 docs/how-to/self-update/cargo-install-from-tag.md delete mode 100644 docs/how-to/self-update/hephd-self-update.md delete mode 100644 docs/how-to/self-update/release-poll-version-check.md delete mode 100644 docs/how-to/self-update/self-restart-after-update.md delete mode 100644 docs/how-to/self-update/self-update-opt-in-flag.md delete mode 100644 docs/how-to/self-update/self-update-poll-loop.md delete mode 100644 docs/how-to/self-update/service-env-forge-access.md delete mode 100644 docs/how-to/self-update/service-respawn-on-clean-exit.md delete mode 100644 docs/how-to/self-update/verify-hub-dropout-resilience.md diff --git a/docs/changelog.d/hephd-self-update.feature.md b/docs/changelog.d/hephd-self-update.feature.md new file mode 100644 index 0000000..90cd33d --- /dev/null +++ b/docs/changelog.d/hephd-self-update.feature.md @@ -0,0 +1 @@ +Opt-in (default off) **hephd self-update**: `hephd --self-update` polls the forge for a newer release on an interval and, when one appears, rebuilds via `cargo install` from the release tag (anonymous HTTPS clone of the public repo — no credentials) and restarts onto the new binary. Enable it on the managed service with `heph daemon start --self-update` (which also bakes a cargo-capable `PATH` into the launchd/systemd unit and switches systemd to `Restart=always` so a clean self-exit respawns). The install mechanism is verified end-to-end; a live cross-version upgrade is confirmed on the first release after this lands. Also hardens hub resilience: the daemon's HTTP client now has a 30s timeout so a black-hole hub can't stall the sync/self-update loop. diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index b8ee8f5..eb0a6c8 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -20,17 +20,4 @@ Task-oriented guides for common operations. - [[run-the-daemon]] — Run `hephd` as an OS service with `heph daemon start/stop/restart/status` - [[set-up-sync-hub]] — Stand up the canonical hub (indri) and connect an existing device as an offline-capable spoke - [[import-todoist]] — Seed a heph store from your Todoist projects + tasks (`mise run import-todoist`) - -## Active Mikado chains - -C2 chain: **hephd self-update** (opt-in daemon auto-update). See [[agent-change-process]] for the method. - -- [[hephd-self-update]] — goal: opt-in, default-off mode where `hephd` polls for new releases and auto-updates itself -- [[self-update-opt-in-flag]] — the `--self-update` opt-in flag (default off) -- [[release-poll-version-check]] — poll the forge releases API and semver-compare against the running version -- [[self-update-poll-loop]] — background task wiring the flag to the version check (notify-only core) -- [[service-env-forge-access]] — give the daemon's service environment cargo + forge SSH access (the cargo/forge blocker) -- [[cargo-install-from-tag]] — rebuild + install the new binaries via `cargo install` from the release tag -- [[service-respawn-on-clean-exit]] — make the service manager respawn hephd after a clean exit (systemd `Restart=always`) -- [[self-restart-after-update]] — exit cleanly after a successful install so the new binary takes over -- [[verify-hub-dropout-resilience]] — lock in "the hub can vanish at any moment" as the base case +- [[self-update]] — Opt-in `hephd` self-update: poll the forge for new releases and auto-update diff --git a/docs/how-to/self-update.md b/docs/how-to/self-update.md new file mode 100644 index 0000000..d4dda1f --- /dev/null +++ b/docs/how-to/self-update.md @@ -0,0 +1,56 @@ +--- +title: hephd self-update +modified: 2026-06-04 +tags: + - how-to +--- + +# hephd self-update + +`hephd` can keep itself current: it polls the forge for a newer release and, when +one appears, rebuilds and restarts onto it — unattended. It is **opt-in and off +by default**. + +## Enable it + +On the managed service: + +```bash +heph daemon start --self-update +``` + +That generates a launchd/systemd service that runs `hephd --self-update` and +gives it a `PATH` that can find `cargo`. `heph daemon restart` preserves the +setting (pass `--self-update` again to turn it on later). To run the daemon +directly instead: + +```bash +hephd --self-update # default: poll every 6h +hephd --self-update --self-update-interval-secs 3600 +``` + +## How it works + +1. Each interval, `hephd` GETs the forge's `releases/latest` and compares the tag + against its own version (the one `heph --version` reports). +2. On a newer release it runs `cargo install --locked --git + --tag vX.Y.Z` for `heph`/`hephd`/`heph-tui`/`heph-quickadd`. hephaestus is a + public repo, so this is an anonymous clone — **no credentials**. +3. On a successful install it exits cleanly; the service manager (launchd + `KeepAlive` / systemd `Restart=always`) brings the new binary up. + +A failed poll or build is logged and the daemon keeps running on its current +version — self-update never takes the daemon down. + +## Requirements & notes + +- The **Rust toolchain** (`cargo`) must be installed for the service user; the + update builds from source. +- Off by default — nothing happens unless `--self-update` is passed. +- The first real cross-version upgrade is observable on the first release cut + after enabling it. + +## Related + +- [[run-the-daemon]] — running `hephd` as an OS service +- [[install-heph]] — installing the binaries diff --git a/docs/how-to/self-update/cargo-install-from-tag.md b/docs/how-to/self-update/cargo-install-from-tag.md deleted file mode 100644 index a099802..0000000 --- a/docs/how-to/self-update/cargo-install-from-tag.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: Cargo install from tag -modified: 2026-06-04 -tags: - - how-to -requires: - - self-update-poll-loop - - service-env-forge-access ---- - -# Cargo install from tag - -The apply step: when the poll loop detects a newer release, rebuild + install -the new binaries from the release tag. - -## Deliverables - -- From the detected tag `vX.Y.Z`, run (via `tokio::task::spawn_blocking`, since - it's a long blocking child process): - ``` - cargo install --locked \ - --git ssh://forgejo@forge.ops.eblu.me:2222/eblume/hephaestus.git \ - --tag vX.Y.Z heph hephd heph-tui heph-quickadd - ``` - This is the exact command the install how-to and the manual redeploy use; it - swaps `~/.cargo/bin/*` in place. -- Capture stdout/stderr and exit status; log success/failure. A failed build - must **not** restart the daemon — only a successful install proceeds to - [[self-restart-after-update]]. -- Guard against re-running while an install is in flight (the long compile spans - multiple poll ticks): a simple "update in progress" flag. - -## Done when - -On a real newer tag, the daemon completes the install and the new binary is on -disk at `~/.cargo/bin`. Requires [[self-update-poll-loop]] and -[[service-env-forge-access]]. Part of [[hephd-self-update]]. diff --git a/docs/how-to/self-update/hephd-self-update.md b/docs/how-to/self-update/hephd-self-update.md deleted file mode 100644 index e9b679a..0000000 --- a/docs/how-to/self-update/hephd-self-update.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: hephd self-update -modified: 2026-06-04 -tags: - - how-to -status: active -branch: mikado/hephd-self-update -requires: - - self-restart-after-update - - verify-hub-dropout-resilience ---- - -# hephd self-update - -**Goal (desired end state).** An opt-in, **default-off** mode where `hephd` -periodically polls the forge for a newer release and, when one exists, -rebuilds via `cargo install` from the release tag and restarts itself onto the -new binary — unattended. - -## End state - -- A new daemon flag (`--self-update`, default off) plus a poll interval. When - off, behaviour is unchanged. See [[self-update-opt-in-flag]]. -- A background task (modelled on the existing spoke sync loop, - `crates/hephd/src/server.rs` `spawn_sync_loop`) that on each tick fetches the - latest release and compares it to `heph_core::VERSION`. See - [[self-update-poll-loop]] and [[release-poll-version-check]]. -- On a newer release: run `cargo install --locked --git ssh://… --tag vX.Y.Z` - for all workspace binaries ([[cargo-install-from-tag]]), then exit cleanly so - the OS service manager respawns the new binary - ([[self-restart-after-update]], [[service-respawn-on-clean-exit]]). -- Running `cargo install` from inside the service requires the daemon's - environment to have cargo + forge SSH access — the known blocker tracked in - [[service-env-forge-access]]. - -## Design decisions (owner) - -- **Default off**, opt-in only. Never self-update silently by default. -- Delivery is **`cargo install` from the tag** for now (prebuilt release - binaries are a possible future, pending a cargo/forge canonical-host fix). -- **Hub can disappear at any moment** — that resilience is the *base case*, not - a special guard. The sync loop already tolerates an unreachable hub; we lock - that in rather than add update-specific guards. See - [[verify-hub-dropout-resilience]]. - -## Scope notes - -Captured from task `01KTA2NSNRYT902HC3VRW00S1J` in the `Hephaestus` project. -Possible later refinements (own cards if pursued): checksum/signature -verification of the built binary, prebuilt release-binary delivery, and a -notify-only sub-mode. diff --git a/docs/how-to/self-update/release-poll-version-check.md b/docs/how-to/self-update/release-poll-version-check.md deleted file mode 100644 index 8ecfca2..0000000 --- a/docs/how-to/self-update/release-poll-version-check.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Release poll + version check -modified: 2026-06-04 -tags: - - how-to -requires: [] ---- - -# Release poll + version check - -The piece that answers "is a newer release available?" — independent of any -daemon wiring, so it can be unit-tested in isolation. - -## Deliverables - -- Fetch the latest release from the forge: - `GET https://forge.ops.eblu.me/api/v1/repos/eblume/hephaestus/releases/latest`, - read `tag_name` (e.g. `v1.0.4`). hephd already depends on `ureq` and - `reqwest` (`crates/hephd/Cargo.toml`) — reuse one (the poll loop is async, so - `reqwest` fits; `ureq` would need `spawn_blocking`). -- Parse the running version: `heph_core::VERSION` is `"1.0.3 (sha)"` — take the - `X.Y.Z` head. Add `semver = "1"` to `crates/hephd/Cargo.toml` (already in the - lockfile transitively) and compare `tag_name` (strip leading `v`) against it. -- A pure `is_newer(current, tag) -> bool` helper with tests covering equal / - older / newer / malformed tags. - -## Done when - -Given a fixed current version and a sample releases-API JSON body, the helper -correctly reports whether an update exists. No daemon loop yet — that's -[[self-update-poll-loop]]. Part of [[hephd-self-update]]. diff --git a/docs/how-to/self-update/self-restart-after-update.md b/docs/how-to/self-update/self-restart-after-update.md deleted file mode 100644 index 1f95128..0000000 --- a/docs/how-to/self-update/self-restart-after-update.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Self-restart after update -modified: 2026-06-04 -tags: - - how-to -requires: - - cargo-install-from-tag - - service-respawn-on-clean-exit ---- - -# Self-restart after update - -The last step: once the new binary is installed, get the running daemon to hand -off to it. - -## Deliverables - -- After a successful [[cargo-install-from-tag]], have hephd exit cleanly - (`std::process::exit(0)`) so the service manager respawns the new binary. - hephd has no graceful-shutdown path today (`serve` is an infinite accept - loop) — a clean process exit is acceptable; in-flight RPC connections simply - drop and clients reconnect (the plugin already reconnects-once). -- Relies on [[service-respawn-on-clean-exit]] so the exit is actually followed - by a respawn on both platforms. -- Log a clear "restarting into vX.Y.Z" line before exit. Optionally re-check - that the on-disk version actually changed before restarting, to avoid a - restart loop if the install was a no-op. - -## Done when - -End-to-end: an enabled daemon on an older version detects a newer release, -installs it, restarts, and comes back reporting the new `version` RPC value. -This closes the apply path of [[hephd-self-update]]. diff --git a/docs/how-to/self-update/self-update-opt-in-flag.md b/docs/how-to/self-update/self-update-opt-in-flag.md deleted file mode 100644 index ecf5625..0000000 --- a/docs/how-to/self-update/self-update-opt-in-flag.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Self-update opt-in flag -modified: 2026-06-04 -tags: - - how-to -requires: [] ---- - -# Self-update opt-in flag - -The opt-in surface. hephd config today is pure clap flags (no config file) in -`crates/hephd/src/main.rs`. - -## Deliverables - -- Add `--self-update` (bool, **default false**) and an interval override (e.g. - `--self-update-interval-secs`, with a sane default like 6h). Document them in - the flag help. -- Thread them into the daemon the same way `--hub-url` / spoke auth are - (`Daemon::new(...).with_hub(...)` → add `.with_self_update(cfg)`). -- When the flag is absent, the daemon behaves exactly as today (the loop in - [[self-update-poll-loop]] is simply not spawned). -- Later, bake the flag into the generated service definition (launchd/systemd) - so an enabled daemon keeps self-updating across restarts — coordinate with - [[service-respawn-on-clean-exit]] (same templates in `crates/heph/src/service.rs`). - -## Done when - -`hephd --self-update` starts the daemon with the mode enabled (verifiable via a -startup log line); omitting it leaves current behaviour untouched. Part of -[[hephd-self-update]]. diff --git a/docs/how-to/self-update/self-update-poll-loop.md b/docs/how-to/self-update/self-update-poll-loop.md deleted file mode 100644 index da3fd06..0000000 --- a/docs/how-to/self-update/self-update-poll-loop.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Self-update poll loop -modified: 2026-06-04 -tags: - - how-to -requires: - - release-poll-version-check - - self-update-opt-in-flag ---- - -# Self-update poll loop - -The background task that ties the flag to the version check. This card alone -yields a working **notify-only** daemon ("update available: vX.Y.Z" in the -log) — the apply path layers on after. - -## Deliverables - -- Spawn a `tokio` task modelled on `spawn_sync_loop` - (`crates/hephd/src/server.rs`): `tokio::time::interval` ticking at the - configured cadence, guarded so it's a no-op unless `--self-update` is set. -- Each tick: run the [[release-poll-version-check]]. On "newer available", log - it (and, once the apply path exists, hand off to [[cargo-install-from-tag]]). -- Errors (forge unreachable, bad JSON) are logged and the loop continues — - same resilience pattern the sync loop uses. A flaky forge must never crash or - block the daemon. - -## Done when - -With `--self-update` on and a stubbed/real "newer" release, the daemon logs an -update-available line once per detection; with the flag off, no task runs. -Requires [[release-poll-version-check]] and [[self-update-opt-in-flag]]. Part of -[[hephd-self-update]]. diff --git a/docs/how-to/self-update/service-env-forge-access.md b/docs/how-to/self-update/service-env-forge-access.md deleted file mode 100644 index 7a31238..0000000 --- a/docs/how-to/self-update/service-env-forge-access.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Service env forge access -modified: 2026-06-04 -tags: - - how-to -requires: [] ---- - -# Service env forge access - -The runtime-environment prerequisite for the apply path: a `hephd` started by -launchd/systemd runs with a minimal environment, so it must be able to find -cargo and fetch the repo when it runs `cargo install`. - -## Resolved (and how the original premise was wrong) - -This card was first written assuming self-update needed **forge SSH -credentials** for a headless service — because the install how-to uses -`ssh://forgejo@forge.ops.eblu.me:2222/…`. That premise was wrong: - -- **hephaestus is a public repo**, and `cargo install --git` is a plain - anonymous git clone — *not* the Forgejo cargo **registry** (the registry is - access-restricted and is the thing that required `forge.ops.eblu.me`; it is - unrelated to git clone). So **no credentials, no SSH, no deploy key**. -- Verified end-to-end: `cargo install --git https://forge.eblu.me/eblume/hephaestus.git --tag v1.0.3 hephd` - builds a working binary anonymously. Self-update uses that canonical public - HTTPS URL (`INSTALL_GIT_URL`), and the release poll uses the same host. - -So the only real requirement was the **environment**, handled in -`crates/heph/src/service.rs`: `heph daemon start --self-update` generates a -launchd/systemd service that passes `--self-update` and bakes a `PATH` -(including `~/.cargo/bin`) + `HOME` so the minimal service env can find cargo -and the toolchain. `restart` preserves the setting. Default services are -unchanged. - -## Remaining (owner) - -The Rust toolchain must be installed for the service user (cargo builds from -source), and a real on-device run — enable `--self-update`, then confirm a -live upgrade when the next release lands — is the final end-to-end check. See -[[hephd-self-update]]. diff --git a/docs/how-to/self-update/service-respawn-on-clean-exit.md b/docs/how-to/self-update/service-respawn-on-clean-exit.md deleted file mode 100644 index 88db290..0000000 --- a/docs/how-to/self-update/service-respawn-on-clean-exit.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Service respawn on clean exit -modified: 2026-06-04 -tags: - - how-to -requires: [] ---- - -# Service respawn on clean exit - -For "self-restart" to mean "exit and let the manager bring up the new binary", -both service managers must respawn hephd after a **clean** (exit code 0) -shutdown. Templates live in `crates/heph/src/service.rs`. - -## Current state (from research) - -- **launchd (macOS):** plist has `KeepAlive = true` → already respawns on clean - exit. No change needed. -- **systemd (Linux):** unit is `Restart=on-failure` → a clean exit (code 0) - does **not** respawn. Self-restart would silently stop the daemon. - -## Deliverables - -- Change the systemd unit template to `Restart=always` (with a small - `RestartSec`) so a deliberate clean exit is respawned. -- Note in install/upgrade docs that **already-installed services must be - reinstalled** (`heph daemon` re-generates the unit) to pick up the new - policy; otherwise self-restart won't work on existing Linux installs. - -## Done when - -On both platforms, a hephd that calls `exit(0)` is brought back up by the -service manager. Pairs with [[self-restart-after-update]]. Part of -[[hephd-self-update]]. diff --git a/docs/how-to/self-update/verify-hub-dropout-resilience.md b/docs/how-to/self-update/verify-hub-dropout-resilience.md deleted file mode 100644 index 9f01c46..0000000 --- a/docs/how-to/self-update/verify-hub-dropout-resilience.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Verify hub-dropout resilience -modified: 2026-06-04 -tags: - - how-to -requires: [] ---- - -# Verify hub-dropout resilience - -Owner requirement: "the hub can go poof at any moment" must be the **base -case**, not a guard bolted on for self-update. A self-updating hub will restart -under its spokes, so spokes must already shrug off an unreachable hub. - -## Current state (from research) - -Already largely true: `sync_once` (`crates/hephd/src/sync.rs`) propagates -errors, and the background loop (`spawn_sync_loop`, `crates/hephd/src/server.rs`) -catches them — `tracing::warn!("background sync failed: {e}")` — and continues. -The local SQLite store stays writable, so the spoke works offline and -reconciles on the next successful tick. No panic, no block. - -## Deliverables - -- Lock the guarantee in with an explicit test: a spoke whose hub is unreachable - for one or more sync cycles keeps serving local RPCs and accepting writes, - then reconciles when the hub returns. -- If any path is found that *doesn't* degrade gracefully (a blocking call, an - unwrapped error, a restart that loses unsynced ops), fix it here — that is the - whole point of this card. - -## Done when - -A test demonstrates spoke survival across hub downtime, documenting the -base-case guarantee that makes a self-updating hub safe. Part of -[[hephd-self-update]].