Uh oh!
There was an error while loading. Please reload this page.
feat(install): unattended installation mode - #408
Conversation
install.sh could not complete without a terminal. Two things blocked it: `exec < /dev/tty` ran whenever stdin was not a tty, which fails with ENXIO under cloud-init and ends the script; and past the dependency phase the installer always ran the interactive `nerve init` wizard, with NERVE_YES=1 auto-confirming it rather than skipping it. Adds --non-interactive / NERVE_NON_INTERACTIVE=1, which implies NERVE_YES, never prompts, and runs `nerve init --non-interactive`. The daemon is left stopped for a service manager to own unless NERVE_START=1. The tty reclaim is now probed in a subshell and skipped when it cannot succeed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| PREFERRED_PYTHON_MINOR=13 | ||
| # Vite 7 (see web/package.json) requires Node 20.19+ or 22.12+. | ||
| MIN_NODE_VERSION="20.19.0" | ||
| NON_INTERACTIVE="${NERVE_NON_INTERACTIVE:-0}" |
There was a problem hiding this comment.
Could we make dependency elevation honor unattended mode too? The official ubuntu:24.04 image runs as root and has apt-get, but has neither sudo nor git, so the current HAS_SUDO=0 guard aborts even though it can install packages directly. In a container with sudo added, sudo apt-get -y also allowed tzdata to emit its Geographic area: debconf prompt because DEBIAN_FRONTEND was stripped; -y only answers apt's own confirmation.
One way to centralize both concerns:
run_as_root() {
if [ "$(id -u)"-eq 0 ];then"$@"elif! command_exists sudo;then
error "Root privileges are required, but sudo is not available."return 1
elif [ "$NON_INTERACTIVE"="1" ];then# Fail instead of waiting for a sudo password in automation.
sudo -n "$@"else
sudo "$@"fi
}
run_debian_command() {
if [ "$NON_INTERACTIVE"="1" ];then# Set this after the sudo boundary so sudo cannot strip it.
run_as_root env DEBIAN_FRONTEND=noninteractive "$@"else
run_as_root "$@"fi
}The apt paths can then use:
run_debian_command apt-get update -qq
run_debian_command apt-get install -y -qq git
curl -fsSL https://deb.nodesource.com/setup_lts.x \
| run_debian_command bash -
run_debian_command apt-get install -y -qq nodejsThe Python apt path should use the same wrapper, while dnf/pacman/zypper can use run_as_root. This keeps interactive sudo behavior unchanged, lets root images install directly, and guarantees unattended sudo never prompts.
There was a problem hiding this comment.
Done
run_as_root runs directly when id -u is 0, errors when there is no sudo and no root, and uses sudo -n under --non-interactive so a missing password rule fails immediately rather than waiting. run_debian_command wraps it with DEBIAN_FRONTEND=noninteractive past the sudo boundary.
Every apt path now goes through run_debian_command.
| step "Running Nerve setup (non-interactive)" | ||
| cd "$INSTALL_DIR" || exit 1 | ||
| # Fail loudly: a half-configured unattended install is worse than none. | ||
| "$nerve_bin" -c "$INSTALL_DIR" init --non-interactive |
There was a problem hiding this comment.
Could we resolve the config directory once, for example CONFIG_DIR="${NERVE_CONFIG_DIR:-$INSTALL_DIR}", and use it consistently? This explicit -c "$INSTALL_DIR" ignores NERVE_CONFIG_DIR during init, but offer_start later calls nerve start without -c, where the same environment variable wins and may select a different config. The upgrade check also only looks at $INSTALL_DIR/config.local.yaml.
I do not think the config directory needs to be mandatory—the install directory is a safe default—but upgrade detection, init, start, and the summary should all use the same resolved value.
There was a problem hiding this comment.
Fixed. CONFIG_DIR="${NERVE_CONFIG_DIR:-$INSTALL_DIR}" is resolved once next to INSTALL_DIR.
…g dir Review feedback on two gaps. Dependency installation assumed a non-root user with sudo. A root container has apt-get but neither sudo nor git, and the old guard aborted anyway. Adds run_as_root, which runs directly as root, uses sudo -n under --non-interactive so a missing password rule fails immediately, and run_debian_command, which sets DEBIAN_FRONTEND=noninteractive past the sudo boundary so debconf cannot prompt behind apt's -y. The config directory was resolved inconsistently: init was pinned to INSTALL_DIR while start resolved NERVE_CONFIG_DIR itself, so the two could act on different configurations. Resolves it once as CONFIG_DIR and uses it for upgrade detection, init, start and the summary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
alex-clickhouse
commented
Aug 25, 2026
LGTM, thank you for the PR! |
Uh oh!
There was an error while loading. Please reload this page.
Problem
install.shcannot complete without a terminal, so it can't be used from cloud-init, Ansible, a Dockerfile or an image build. Two separate things block it.1. The tty reclaim is fatal without a controlling terminal.
/dev/ttyexists on any normal system, but a process started by a daemon has no controlling terminal, so the open fails with ENXIO. Underset -ethat ends the install before the first dependency check.-e /dev/ttytests that the device node is there, which is not the same question.2. There is no way to skip the wizard.
Past the dependency phase
run_initalways runs the interactivenerve init.NERVE_YES=1does not help — it makesconfirm "Run the setup wizard now?"return true, so the wizard runs with no one to answer it and the install hangs.offer_startthen does the same for "Start Nerve now?".nerve init --non-interactivealready exists and reads everything from the environment; the installer just had no way to reach it.Change
Adds
--non-interactive/NERVE_NON_INTERACTIVE=1:NERVE_YES=1and skips the tty reclaim entirelyrun_initcallsnerve init --non-interactiveand fails loudly rather than leaving a half-configured installoffer_startleaves the daemon stopped, since these installs are normally followed by a service manager that owns the process.NERVE_START=1starts it from the installer instead.The tty reclaim is now probed in a subshell before it is used, so an interactive run behaves as before and a run without a terminal continues instead of dying.
curl -fsSL https://raw.githubusercontent.com/ClickHouse/nerve/main/install.sh \ | NERVE_NON_INTERACTIVE=1 \ NERVE_INSTALL_DIR=/home/agent/nerve \ NERVE_MODE=worker \ NERVE_PROVIDER=bedrock \ NERVE_AWS_REGION=eu-central-1 \ bashdocs/setup.mdgets an "Unattended installation" section listing the variables the wizard would otherwise ask for.Notes
NON_INTERACTIVEdefaults to 0 and every new branch is behind it.NERVE_PASSWORDis read once bynerve initand stored only as a bcrypt hash, so an unattended caller can pass it in and drop it immediately. Called out in the docs.Testing
bash -n install.shNERVE_NON_INTERACTIVE=1setsAUTO_YES=1and thatconfirmthen returns without reading stdin.execredirection does not.🤖 Generated with Claude Code