Skip to content

install.sh: install the deploy supervisor on checkout installs (timer off) - #1335

Merged
dp-web4 merged 3 commits into
mainfrom
tristan/install-deploy-supervisor
Oct 19, 2026
Merged

dp-web4 merged 3 commits into
mainfrom
tristan/install-deploy-supervisor

Conversation

@lewisfab

Copy link
Copy Markdown
Collaborator

Why

The operator on tl-dell asked: "on a BRAND NEW install, will that code have to be run or will it just work from the getgo?" It would not just work. The deploy supervisor that the dashboard's update button starts (hestia-deploy.service) is a separate once-per-box install from deploy/from-main/README.md, and no installer runs it. So every box set up with deploy/fleet/install.sh gets an update button that can only fail with Unit hestia-deploy.service not found (#791: Nomad 2026-09-01, tl-dell 2026-10-16).

Companion to #1334, which adds the dashboard's auto-update toggle and "check for updates" button and hides both where no supervisor is installed. This PR is what makes them present on a fresh box. The two PRs are independent and can merge in either order.

What

New step_deploy_supervisor in deploy/fleet/install.sh, run after the workspace marker:

  • Checkout install (a resolved HESTIA_WORKSPACE) with git and cargo, on Linux:
    • clones hestia and web4 into $HESTIA_HOME/deploy over https, since the units run without an ssh agent;
    • installs ~/.local/bin/hestia-deploy and both units, with HESTIA_WORKSPACE rendered into the service;
    • reads the unit back, then runs daemon-reload.
  • The timer stays OFF by default. Nothing builds or restarts until the operator clicks update or turns auto-update on. HESTIA_AUTO_UPDATE=1 arms it at install time. A re-run never disarms a timer the operator turned on.
  • Skips, and says why:
    • Release (curl | sh) installs: they track releases, not main, and are updated by re-running the installer. Converting them to main-tracking is a choice this step doesn't make for them.
    • Missing git or cargo.
    • macOS: deriving the launchd agent's binary path isn't automated yet.
    • HESTIA_SKIP_DEPLOY is set.
  • deploy/from-main/README.md: a note at the top of the Linux install section. The manual lines stay as the reference and as the opt-in route for release installs.

Questions for review

  1. Release installs. Should a release box get a "check for updates" that re-runs the installer for the latest release, instead of being skipped? That would be a different supervisor, so I've left it out of this PR.
  2. Default off. It matches the operator's choice here: hestia moves daily, so scheduled deploys of unreviewed main are opt-in. Say if the fleet default should differ.

Tested

  • New deploy/fleet/deploy_supervisor_step_test.py (picked up by tools/ci_discovery.py bare): 17 checks. It runs the step itself in a temporary HOME, with stub git, cargo, systemctl and uname, against the real shipped hestia-deploy.{sh,service,timer}. It covers:
    • the files installed, with the workspace rendered;
    • nothing asking systemd to enable the timer by default;
    • opt-in enabling it, and an armed timer left armed;
    • each skip arm, with its message.
  • Existing: install_version_test.py, tools/no_path_literals_test.py, tools/public_boundary.py, tools/ci_test_coverage_test.py, tools/shebang_exec_bit_test.py, and the deploy/from-main/*_test.py files all pass.
  • Not tested: a full install.sh run on a real fresh box. On this box it would replace the main-built daemon with a release binary.

Review gate

surface: deploy/fleet/install.sh step_deploy_supervisor   act: install a supervisor that can deploy hestia main to this box
S: med/reversible [construct: timer off by default; units removable; nothing builds until an operator act]
R: n/a (operator-run installer, same trust as the rest of install.sh)   W: n/a (no daemon surface; the deploy act itself stays behind operator_gate / the operator's own timer choice)
O: pass [construct: prerequisites (checkout, git, cargo, Linux) checked before the first write]   A: n/a (installer output; the deploy acts it enables are witnessed by the daemon)
V: present [construct: hestia-deploy.sh preflight_gate + rollback, unchanged]
verdict: PASS

🤖 Generated with Claude Code

… off

Until now the deploy supervisor (deploy/from-main) was a separate once-per-box
install from a README that no installer ran. Every box set up with
deploy/fleet/install.sh therefore had a dashboard update button that could only
fail with "Unit hestia-deploy.service not found" (#791: Nomad 2026-09-01,
tl-dell 2026-10-16). The operator on tl-dell asked whether a brand-new install
would "just work from the getgo"; it would not.

New step_deploy_supervisor, run after the workspace marker:
- checkout install (HESTIA_WORKSPACE resolved) with git + cargo on Linux:
  clones hestia + web4 into $HESTIA_HOME/deploy over https (the units run with
  no ssh agent), installs ~/.local/bin/hestia-deploy and both units with
  HESTIA_WORKSPACE rendered, reads the unit back, daemon-reload.
- the TIMER STAYS OFF by default. HESTIA_AUTO_UPDATE=1 arms it. A re-run never
  disarms a timer the operator turned on.
- skips, saying why: release (curl | sh) installs track releases, not main;
  missing git/cargo; macOS (launchd binary path derivation is not automated
  yet); HESTIA_SKIP_DEPLOY.

deploy/fleet/deploy_supervisor_step_test.py runs the step itself in a temp HOME
with stub git/cargo/systemctl/uname against the REAL shipped unit files: 17
checks, including "nothing asked systemd to enable the timer" by default.

surface: deploy/fleet/install.sh step_deploy_supervisor   act: install a supervisor that can deploy hestia main to this box
S: med/reversible [construct: timer off by default; units removable; nothing builds until an operator act]
R: n/a (operator-run installer, same trust as the rest of install.sh)   W: n/a (no daemon surface; the deploy act itself stays behind operator_gate / the operator's own timer choice)
O: pass [construct: prerequisites (checkout, git, cargo, Linux) checked before the first write]   A: n/a (installer output; the deploy acts it enables are witnessed by the daemon)
V: present [construct: hestia-deploy.sh preflight_gate + rollback, unchanged]
verdict: PASS

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

dp-web4 commented Oct 18, 2026

Copy link
Copy Markdown
Owner

NOVA review follow-up (2026-10-17): HOLD: installer systemd Environment= path rendering mishandles spaces/special chars and custom HESTIA_HOME may not propagate into installed unit. Verify parsed unit with systemd-analyze and add non-default-home/path tests.

…lt HESTIA_HOME

Nova's HOLD on #1335 (2026-10-17):

- The workspace went into the unit unquoted, through sed. systemd splits an unquoted
  Environment= value at whitespace, treats % as a specifier, and backslash and quote as
  syntax; sed reads | & \ in the path as its own. Measured on the old code with the new test:
  "My Projects" reached systemd as ".../My", "pipe|amp&x" broke sed, and in the space cases
  the step still printed "the dashboard update button works". Now `systemd_env_line` writes
  Environment="VAR=value" with \ " escaped and % doubled, awk substitutes it from the
  environment, the read-back is grep -F, and systemd-analyze verify (where present) must
  not complain about the environment.
- A custom HESTIA_HOME was not followed: the step cloned into $HESTIA_HOME/deploy, but the
  shipped units run with HESTIA_HOME=%h/.hestia and hestia-deploy defaults its clones to
  ~/.hestia/deploy, as does the daemon unit this script installs. Rather than install a
  supervisor that clones into one home and deploys into another, the step now skips and
  names the manual route.

deploy_supervisor_step_test.py: a model of systemd's Environment= parser checks six awkward
workspace names round-trip with no stray variable, plus the HESTIA_HOME skip and a direct
systemd-analyze verify of the rendered unit. 8 of the new checks fail on the previous commit.

-- cbp-claude

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
@dp-web4

dp-web4 commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Addressed Nova's HOLD in 5e5c552 (pushed onto this branch, no force):

  • Path rendering:
    • The new systemd_env_line writes Environment="HESTIA_WORKSPACE=…", escaping \ and " and doubling %.
    • awk substitutes it from the environment, so sed metacharacters in the path are harmless.
    • The read-back uses grep -F.
    • Where systemd-analyze is installed, it has to parse the unit without an environment warning.
    • The test models systemd's parser and checks that six awkward workspace names round-trip with no stray variable. It also runs systemd-analyze verify directly. On the previous commit, "My Projects" reached systemd as …/My, and the step still printed "the dashboard update button works".
  • Custom HESTIA_HOME: the shipped deploy unit, hestia-deploy's clone default, and the daemon unit this installer writes are all bound to ~/.hestia. The step now skips a non-default HESTIA_HOME and names the manual route, rather than cloning into one home and deploying into another. Making the units follow HESTIA_HOME would be a larger change across all three.
  • Not changed, but the same class: install.sh's daemon unit heredoc renders Environment=HESTIA_WORKSPACE=${HESTIA_WORKSPACE} unquoted (main, line ~241). systemd_env_line could fix it in a follow-up.

— cbp-claude

5e5c552's HESTIA_HOME guard spelled the default home out twice more, and the
no-path-literals ratchet (CI, plugin tests) failed on it: shell-home-hestia 5 -> 6,
tilde-hestia 0 -> 1. The default is now named once, as HESTIA_HOME_DEFAULT, on the line
that already resolved it. The guard and its message use the variable. The pin moves by one
reclassification (that line's shell-home-default becomes the existing shell-home-hestia
count): 10 -> 9.

-- cbp-claude

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014LYVQN8U1xqy6U6pbyUDvH
@dp-web4
dp-web4 merged commit 691b4d3 into main Oct 19, 2026
10 checks passed
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.

2 participants