Skip to content

fix(linux): host updates fail installing unit files via /dev/stdin - #64

Merged
gitcommit90 merged 1 commit into
mainfrom
fix/update-install-stdin
Aug 3, 2026
Merged

fix(linux): host updates fail installing unit files via /dev/stdin#64
gitcommit90 merged 1 commit into
mainfrom
fix/update-install-stdin

Conversation

@gitcommit90

@gitcommit90 gitcommit90 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

The first live host update (0.0.39→0.0.40) failed and rolled back. install /dev/stdin DEST <<EOF works from an operator shell but fails with ENOENT under systemd-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

    • Improved Linux service installation reliability when run through systemd-run.
    • Prevented installation failures caused by reopening standard input during setup.
  • Tests

    • Added coverage to verify Linux unit files avoid the unreliable standard-input installation method.

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>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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 /dev/stdin.

Changes

Linux unit installation

Layer / File(s) Summary
Heredoc materialization
site/public/install-linux-units.sh
Adds install_stdin, which stores heredoc input in a temporary file, installs it with root ownership and the requested mode, then removes the file.
Unit wiring and regression check
site/public/install-linux-units.sh, test/site.mjs
Routes the three systemd unit heredocs through install_stdin. Adds an assertion that rejects reopening /dev/stdin.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the failure and fix, but it omits most required template sections, including change type, release notes, and verification checkboxes. Add the required template headings and checklists, including change type, release notes, verification results, and applicable documentation status.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the Linux update failure caused by installing unit files through /dev/stdin.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/update-install-stdin

Comment @coderabbitai help to get the list of available commands.

@gitcommit90
gitcommit90 merged commit 67301fd into main Aug 3, 2026
5 of 6 checks passed
@gitcommit90
gitcommit90 deleted the fix/update-install-stdin branch August 3, 2026 18:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c2690ba and bf64409.

📒 Files selected for processing (2)
  • site/public/install-linux-units.sh
  • test/site.mjs

Comment on lines +40 to +46
install_stdin() {
local mode="$1" dest="$2" tmp
tmp="$(mktemp)"
cat >"$tmp"
install -o root -g root -m "$mode" "$tmp" "$dest"
rm -f "$tmp"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

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.

1 participant