generated from eblume/project-template
Compare commits
1 commit
c321d72e7d
...
443763489b
| Author | SHA1 | Date | |
|---|---|---|---|
| 443763489b |
11 changed files with 57 additions and 345 deletions
|
|
@ -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
|
||||
|
|
|
|||
56
docs/how-to/self-update.md
Normal file
56
docs/how-to/self-update.md
Normal file
|
|
@ -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 <public-https-url>
|
||||
--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
|
||||
|
|
@ -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]].
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
---
|
||||
title: hephd self-update
|
||||
modified: 2026-06-04
|
||||
tags:
|
||||
- how-to
|
||||
requires:
|
||||
- self-restart-after-update
|
||||
- verify-hub-dropout-resilience
|
||||
---
|
||||
|
||||
# hephd self-update
|
||||
|
||||
**Implemented.** 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. Enable on the managed service with `heph daemon start --self-update`
|
||||
(see [[run-the-daemon]]).
|
||||
|
||||
> **One remaining live check (owner):** the install mechanism is verified
|
||||
> end-to-end (anonymous public HTTPS `cargo install`), and the detection/apply
|
||||
> logic is unit-tested, but a real older→newer upgrade can only be observed when
|
||||
> the next release lands. Enable `--self-update` and confirm the upgrade then.
|
||||
|
||||
## 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.
|
||||
|
|
@ -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]].
|
||||
|
|
@ -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]].
|
||||
|
|
@ -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]].
|
||||
|
|
@ -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]].
|
||||
|
|
@ -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]].
|
||||
|
|
@ -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]].
|
||||
|
|
@ -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]].
|
||||
Loading…
Add table
Add a link
Reference in a new issue