aboutsummaryrefslogtreecommitdiff
path: root/scripts/post-rebuild-check
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/post-rebuild-check')
-rwxr-xr-xscripts/post-rebuild-check124
1 files changed, 113 insertions, 11 deletions
diff --git a/scripts/post-rebuild-check b/scripts/post-rebuild-check
index 24e99a4..2013b26 100755
--- a/scripts/post-rebuild-check
+++ b/scripts/post-rebuild-check
@@ -1,6 +1,6 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-3.0-or-later
-# post-rebuild-check - the five checks a rebuilt machine actually needs.
+# post-rebuild-check - the eight checks a rebuilt machine actually needs.
#
# A rebuilt machine looks finished and isn't. Five gaps surfaced on velox
# within two days of the 2026-08-13 reinstall, and three of them LOOKED
@@ -25,6 +25,14 @@
# DoT/DNSSEC validation this machine's DNS runs on, so nothing
# resolves -- including the NTP pool that would fix the clock; velox
# deadlocked exactly this way 2026-08-19 and needed a second device)
+# 7. hypridle installed but not running (nothing then triggers idle lock
+# or suspend, so a laptop runs until its battery is gone -- which is
+# how velox reset the RTC that caused check 6's deadlock in the first
+# place; a caffeine remembered from an earlier boot is the known cause)
+# 8. a working repo cloned from the read-only https endpoint (correct
+# for a stranger with no key on the server, wrong for this machine,
+# which finds out at the first push with a 403 -- velox's dotfiles
+# remote sat that way for four days after its rebuild)
#
# The .gitignore rule in check 4 is what scopes it: a tooling path is only
# expected where the project's own .gitignore names it, so a project that
@@ -58,6 +66,10 @@
# the special value MISSING = no NTP daemon active
# PRC_CHRONY_CONF path to chrony.conf (a fixture, under test) -- the
# confdir it names is what decides which drop-ins count
+# PRC_IDLE_DAEMON pgrep output for hypridle; "" = installed but not
+# running, the special value MISSING = not installed
+# PRC_REPO_REMOTES newline list of "path origin-url"; an empty URL
+# means origin could not be read
# PRC_SYSTEMCTL path to the systemctl binary (a fake, under test)
# PRC_SYSTEMCTL_TIMEOUT seconds to allow each systemctl call (default 5)
#
@@ -69,10 +81,11 @@ usage() {
cat <<'EOF'
post-rebuild-check - verify a rebuilt machine is actually finished
-Runs the six checks that caught velox's 2026-08 reinstall gaps: failed
+Runs the eight checks that caught velox's 2026-08 reinstall gaps: failed
units, present-but-inert user units, orphaned *.example configs, missing
-per-project tooling state, the signal-cli registration, and whether time
-sync can recover from a wrong clock without DNS.
+per-project tooling state, the signal-cli registration, whether time sync
+can recover from a wrong clock without DNS, whether anything still
+triggers idle lock and suspend, and whether the working repos can push.
Usage: post-rebuild-check [--help]
@@ -94,6 +107,7 @@ CHECK_FINDINGS=0
FINDING_LINES=""
signal_missing=""
ntp_missing=""
+idle_absent=""
# Every systemctl call is bounded. A wedged user manager spins and answers
# nothing -- seen live on velox 2026-08-17, where `is-enabled`, `cat`, and
@@ -190,7 +204,7 @@ while IFS= read -r line; do
unit=${line#*:}
finding "$scope unit failed: $unit"
done < "$STAGE"
-report "check 1/6: failed units"
+report "check 1/8: failed units"
# --- 2. user unit files present but not enabled ---------------------------
@@ -280,7 +294,7 @@ while read -r name state; do
esac
finding "unit file present but not enabled: $name ($state)"
done < "$STAGE"
-report "check 2/6: unit files"
+report "check 2/8: unit files"
# --- 3. *.example files whose real sibling is missing ---------------------
@@ -325,7 +339,7 @@ while IFS= read -r root; do
[ -e "${ex%.example}" ] || finding "example without its real file: $ex"
done < "$WORK/examples"
done < "$WORK/roots"
-report "check 3/6: local files"
+report "check 3/8: local files"
# --- 4. gitignore-mode projects missing their tooling ---------------------
@@ -379,7 +393,7 @@ todo.org todo\.org
inbox inbox
EOF
done < "$WORK/projects"
-report "check 4/6: project tooling"
+report "check 4/8: project tooling"
# --- 5. signal-cli registration -------------------------------------------
@@ -406,7 +420,7 @@ if [ "$signal_missing" = 1 ]; then
elif [ -z "$signal_missing" ] && [ -z "$accounts" ]; then
finding "no signal account registered — agent-text relays into this machine, so paging breaks for the whole fleet"
fi
-report "check 5/6: signal registration"
+report "check 5/8: signal registration"
# --- 6. NTP can recover a wrong clock without DNS -------------------------
#
@@ -483,7 +497,95 @@ else
finding "every NTP source is named by hostname — a wrong clock breaks DNS, so nothing can resolve them and the clock stays wrong"
fi
fi
-report "check 6/6: NTP bootstrap"
+report "check 6/8: NTP bootstrap"
+
+# --- 7. the idle daemon survives session start ----------------------------
+#
+# A laptop that never sleeps has no symptom until the battery is gone, so
+# nothing surfaces this without being asked. On velox 2026-08-19 hypridle
+# started cleanly at 15:29:48 and `settings restore` killed it six seconds
+# later, replaying a caffeine stored in an earlier boot. The machine ran
+# 11h40m fully awake on battery, died when it flattened, and reset its RTC --
+# which took DNS down with it, the same deadlock check 6 exists for. The
+# desktop looked correct throughout.
+#
+# Behavioural on purpose: this asks whether the daemon is alive, not why it
+# might not be, so a stale caffeine, a crash, and a broken config all surface
+# the same way. Gated on hypridle being installed, because that is what marks
+# a machine as using it -- archsetup installs it only for Hyprland, so a
+# headless or dwm box would otherwise report a finding on every run.
+
+if [ -n "${PRC_IDLE_DAEMON+set}" ]; then
+ idle_pids=$PRC_IDLE_DAEMON
+ if [ "$idle_pids" = "MISSING" ]; then
+ idle_pids=""
+ idle_absent=1
+ fi
+elif command -v hypridle >/dev/null 2>&1; then
+ # pgrep exits non-zero with no match, which is the not-running case rather
+ # than a probe failure, so the || keeps `set -e`-style callers out of it.
+ idle_pids=$(pgrep -x hypridle 2>/dev/null) || idle_pids=""
+else
+ idle_pids=""
+ idle_absent=1
+fi
+
+if [ "$idle_absent" = 1 ]; then
+ : # hypridle is not part of this machine -- nothing to check
+elif [ -z "$idle_pids" ]; then
+ finding "hypridle is installed but not running — nothing triggers idle lock or suspend, so this machine stays awake until its battery is gone; a caffeine remembered from an earlier boot is the known cause"
+fi
+report "check 7/8: idle daemon"
+
+# --- 8. working repos cloned from the read-only endpoint ------------------
+#
+# archsetup clones the user's own archsetup and dotfiles from
+# https://git.cjennings.net/..., which serves anonymous clones and refuses
+# pushes. That default is correct for a stranger installing archsetup -- they
+# have no key on the server -- and wrong for this machine, which has to push.
+# ARCHSETUP_REPO / DOTFILES_REPO override it, but only where they are
+# configured: a curl|bash install, or a rebuild from a stock ISO, takes the
+# default straight back.
+#
+# Nothing about the tree shows it. The clone is complete and ordinary, and the
+# machine finds out at the first push, with a 403 -- which is how velox's
+# dotfiles remote was found on 2026-08-17, four days after its rebuild, by
+# which time the same rebuild's shallow clone had already answered a
+# credential-history question wrongly.
+#
+# Only the read-only endpoint is flagged. An https remote elsewhere may be
+# perfectly pushable through a credential helper, and guessing about hosts
+# this machine does not own would stand noise in front of the real findings.
+
+if [ -n "${PRC_REPO_REMOTES+set}" ]; then
+ repo_remotes=$PRC_REPO_REMOTES
+else
+ repo_remotes=""
+ for repo in "$HOME/code/archsetup" "$HOME/.dotfiles"; do
+ # -e not -d: a worktree or submodule .git is a file naming the gitdir.
+ [ -e "$repo/.git" ] || continue
+ # A repo with no origin still gets a line, with an empty URL, so the
+ # loop below reports it rather than skipping it into a silent pass.
+ repo_url=$(git -C "$repo" remote get-url origin 2>/dev/null)
+ repo_remotes="${repo_remotes}${repo} ${repo_url}
+"
+ done
+fi
+
+stage "$repo_remotes"
+while IFS= read -r repo_line; do
+ [ -n "$repo_line" ] || continue
+ repo_path=${repo_line%% *}
+ repo_url=${repo_line#"$repo_path"}
+ repo_url=${repo_url# }
+ case "$repo_url" in
+ "")
+ finding "$repo_path: origin could not be read — the remote was not checked" ;;
+ https://git.cjennings.net/*|https://cjennings.net/*)
+ finding "$repo_path: origin is the read-only endpoint ($repo_url) — git push returns 403; set the ssh form, or ARCHSETUP_REPO/DOTFILES_REPO before installing" ;;
+ esac
+done < "$STAGE"
+report "check 8/8: repo remotes"
# --- summary --------------------------------------------------------------
@@ -491,5 +593,5 @@ if [ "$TOTAL_FINDINGS" -eq 0 ]; then
echo "all checks clean"
exit 0
fi
-echo "$TOTAL_FINDINGS finding(s) across 6 checks"
+echo "$TOTAL_FINDINGS finding(s) across 8 checks"
exit 1