fix(linux): host updates fail installing unit files via /dev/stdin - #64
Conversation
The first live 1Helm host update (0.0.39 -> 0.0.40) failed and rolled back.
The cause was not the health check (the server was coming up fine) but an
earlier step: install-linux-units.sh writes four files - the tmpfiles.d
config and the three systemd units - with
install -m 0644 /dev/stdin DEST <<EOF ... EOF
That reopens fd 0 through /proc. It works when the script runs from an
operator shell (every fresh install), but fails with "install: No such
file or directory" when the script runs inside a systemd-run oneshot -
which is exactly and only how the UPDATE path invokes it. So every fresh
install succeeded and the first update could never have succeeded. Under
set -e the ENOENT aborted the apply transaction, which correctly rolled
back to the prior healthy release.
Each heredoc is now captured to a temp file and installed from there via a
small install_stdin helper - robust in every execution context, and the
literal unit contents and mode are unchanged.
Adds a regression guard that fails if any command line in
install-linux-units.sh installs from /dev/stdin again. Verified non-vacuous:
reintroducing the pattern fails the test with this message.
Because apply-linux-release.sh runs the TARGET release's copy of this
script, the fix only takes effect once shipped, so it must go out in the
next release for updates to that release to succeed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe Linux installer now writes heredoc content to a temporary file before installation. The tmpfiles configuration and three systemd units use this helper. A regression assertion rejects installation commands that reopen ChangesLinux unit installation
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@site/public/install-linux-units.sh`:
- Around line 40-46: Update install_stdin to execute its temporary-file workflow
in a subshell, install an EXIT trap immediately after mktemp to remove tmp, and
guard cat with || exit 1 so cleanup runs when cat or install fails. Preserve the
existing install arguments and success behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 31e6c024-5cba-4683-a7f1-68f5ad23780d
📒 Files selected for processing (2)
site/public/install-linux-units.shtest/site.mjs
| install_stdin() { | ||
| local mode="$1" dest="$2" tmp | ||
| tmp="$(mktemp)" | ||
| cat >"$tmp" | ||
| install -o root -g root -m "$mode" "$tmp" "$dest" | ||
| rm -f "$tmp" | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/sh
set -eu
file="$(fd -t f -a 'install-linux-units\.sh$' . | head -n 1)"
[ -n "$file" ]
printf '%s\n' "== file: $file =="
cat -n "$file" | sed -n '1,150p'
printf '%s\n' '== relevant shell constructs =='
rg -n -C 3 'install_stdin|mktemp|trap|set -|tmpfiles|systemctl|install ' "$file"Repository: gitcommit90/1Helm
Length of output: 9880
🏁 Script executed:
#!/usr/bin/env bash
set -u
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
tmpdir="$work/tmp"
mkdir "$tmpdir"
run_case() {
local name="$1" implementation="$2"
local case_dir="$work/$name" output status
mkdir "$case_dir"
if [[ "$implementation" == current ]]; then
function install_stdin() {
local mode="$1" dest="$2" tmp
tmp="$(TMPDIR="$tmpdir" mktemp)"
cat >"$tmp"
install -o root -g root -m "$mode" "$tmp" "$dest"
rm -f "$tmp"
}
else
function install_stdin() (
local mode="$1" dest="$2" tmp
tmp="$(TMPDIR="$tmpdir" mktemp)" || exit 1
trap 'rm -f -- "$tmp"' EXIT
cat >"$tmp" || exit 1
install -o root -g root -m "$mode" "$tmp" "$dest"
)
fi
set +e
printf '%s\n' content | install_stdin 0644 "$case_dir/missing/output"
status=$?
set -e
output="$(find "$tmpdir" -maxdepth 1 -type f -printf '%f\n' | sort | paste -sd, -)"
printf '%s: status=%s remaining_temp_files=%s\n' "$name" "$status" "${output:-none}"
}
run_case current current
run_case proposed proposed
printf '%s\n' 'signal test: proposed implementation'
signal_dir="$work/signal"
mkdir "$signal_dir"
function install_stdin() (
local mode="$1" dest="$2" tmp
tmp="$(TMPDIR="$tmpdir" mktemp)" || exit 1
trap 'rm -f -- "$tmp"' EXIT
printf '%s\n' content >"$tmp"
kill -TERM "$BASHPID"
sleep 10
install -o root -g root -m "$mode" "$tmp" "$dest"
)
set +e
install_stdin 0644 "$signal_dir/missing/output"
signal_status=$?
set -e
signal_files="$(find "$tmpdir" -maxdepth 1 -type f -printf '%f\n' | sort | paste -sd, -)"
printf 'signal: status=%s remaining_temp_files=%s\n' "$signal_status" "${signal_files:-none}"Repository: gitcommit90/1Helm
Length of output: 241
🏁 Script executed:
#!/usr/bin/env bash
set -u
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
run_case() {
local name="$1" implementation="$2" case_dir="$work/$1" status count
mkdir "$case_dir"
if [[ "$implementation" == current ]]; then
bash -c '
set -e
install() { return 42; }
install_stdin() {
local mode="$1" dest="$2" tmp
tmp="$(mktemp)"
cat >"$tmp"
install -o root -g root -m "$mode" "$tmp" "$dest"
rm -f "$tmp"
}
printf "%s\n" content | install_stdin 0644 "$1/missing/output"
' bash "$case_dir" >/dev/null 2>&1
else
bash -c '
set -e
install() { return 42; }
install_stdin() (
local mode="$1" dest="$2" tmp
tmp="$(mktemp)" || exit 1
trap '\''rm -f -- "$tmp"'\'' EXIT
cat >"$tmp" || exit 1
install -o root -g root -m "$mode" "$tmp" "$dest"
)
printf "%s\n" content | install_stdin 0644 "$1/missing/output"
' bash "$case_dir" >/dev/null 2>&1
fi
status=$?
count="$(python3 - "$case_dir" <<'PY'
import glob
import sys
print(len(glob.glob(sys.argv[1] + "/*")))
PY
)"
printf '%s: status=%s remaining_files=%s\n' "$name" "$status" "$count"
}
run_case current current
run_case proposed proposed
printf '%s\n' 'signal test: proposed implementation'
signal_dir="$work/signal"
mkdir "$signal_dir"
set +e
bash -c '
set -e
install() { return 42; }
install_stdin() (
local mode="$1" dest="$2" tmp
tmp="$(mktemp)" || exit 1
trap '\''rm -f -- "$tmp"'\'' EXIT
printf "%s\n" content >"$tmp"
kill -TERM "$BASHPID"
sleep 10
install -o root -g root -m "$mode" "$tmp" "$dest"
)
install_stdin 0644 "$1/missing/output"
' bash "$signal_dir" >/dev/null 2>&1
signal_status=$?
set -e
signal_count="$(python3 - "$tmpdir" <<'PY'
import glob
import sys
print(len(glob.glob(sys.argv[1] + "/*")))
PY
)"
printf 'signal: status=%s remaining_temp_files=%s\n' "$signal_status" "$signal_count"Repository: gitcommit90/1Helm
Length of output: 307
🏁 Script executed:
#!/usr/bin/env bash
set -u
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
run_case() {
local name="$1" implementation="$2" case_dir="$work/$1" status count
mkdir "$case_dir"
if [[ "$implementation" == current ]]; then
TMPDIR="$case_dir" bash -c '
set -e
install() { return 42; }
install_stdin() {
local mode="$1" dest="$2" tmp
tmp="$(mktemp)"
cat >"$tmp"
install -o root -g root -m "$mode" "$tmp" "$dest"
rm -f "$tmp"
}
printf "%s\n" content | install_stdin 0644 "$1/missing/output"
' bash "$case_dir" >/dev/null 2>&1
else
TMPDIR="$case_dir" bash -c '
set -e
install() { return 42; }
install_stdin() (
local mode="$1" dest="$2" tmp
tmp="$(mktemp)" || exit 1
trap '\''rm -f -- "$tmp"'\'' EXIT
cat >"$tmp" || exit 1
install -o root -g root -m "$mode" "$tmp" "$dest"
)
printf "%s\n" content | install_stdin 0644 "$1/missing/output"
' bash "$case_dir" >/dev/null 2>&1
fi
status=$?
count="$(python3 - "$case_dir" <<'PY'
import glob
import sys
print(len(glob.glob(sys.argv[1] + "/*")))
PY
)"
printf '%s: status=%s remaining_files=%s\n' "$name" "$status" "$count"
}
run_case current current
run_case proposed proposed
signal_dir="$work/signal"
mkdir "$signal_dir"
set +e
TMPDIR="$signal_dir" bash -c '
set -e
install() { return 42; }
install_stdin() (
local mode="$1" dest="$2" tmp
tmp="$(mktemp)" || exit 1
trap '\''rm -f -- "$tmp"'\'' EXIT
printf "%s\n" content >"$tmp"
kill -TERM "$BASHPID"
sleep 10
install -o root -g root -m "$mode" "$tmp" "$dest"
)
install_stdin 0644 "$1/missing/output"
' bash "$signal_dir" >/dev/null 2>&1
signal_status=$?
set -e
signal_count="$(python3 - "$signal_dir" <<'PY'
import glob
import sys
print(len(glob.glob(sys.argv[1] + "/*")))
PY
)"
printf 'signal: status=%s remaining_files=%s\n' "$signal_status" "$signal_count"Repository: gitcommit90/1Helm
Length of output: 267
Clean up the temporary file when install_stdin fails.
With set -e, a failed cat or install exits before rm -f "$tmp". Run the helper in a subshell, set an EXIT trap after mktemp, and check cat with || exit 1.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/public/install-linux-units.sh` around lines 40 - 46, Update
install_stdin to execute its temporary-file workflow in a subshell, install an
EXIT trap immediately after mktemp to remove tmp, and guard cat with || exit 1
so cleanup runs when cat or install fails. Preserve the existing install
arguments and success behavior.
The first live host update (0.0.39→0.0.40) failed and rolled back.
install /dev/stdin DEST <<EOFworks from an operator shell but fails with ENOENT undersystemd-run— the exact and only path the updater takes. Fix: heredocs to temp files. Regression-guarded and mutation-tested.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
systemd-run.Tests