A machine's role is now the interview's first question and the one answer Panama records. Servers get the same shell minus the screen: core packages, nvm, Bun, Claude Code and Codex (desktops get Codex too), linger, rootless ports from 80, firewalld, the nginx-bridge network, and a nightly image updater that replaced watchtower for cause. server/containers/ carries junior's 23 compose services -- secrets moved to per-machine .env files that never enter this public repo, every transformed compose proven to render byte-identical to what is live. 'panama server' enables, disables and relinks them; nothing here restarts a running service. 'boot --server' walks a fresh VPS from its root login to a normal install. Five new contracts pin the secrets rule, the catalog's shape, panama-server's behavior, the role plumbing, and the dotfile classification. Claude-Session: https://claude.ai/code/session_01NU5JGiN3JfzqrLQB6wmJ1E
156 lines
6.4 KiB
Bash
Executable File
156 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The desktop installs before anything that is allowed to fail.
|
|
#
|
|
# install-packages runs under `set -euo pipefail`, and the Hyprland block used
|
|
# to sit near the bottom of it, below a codec swap, two group updates and a
|
|
# GStreamer glob. Any one of those exiting non-zero ended the stage where it
|
|
# stood. On a machine whose spin never shipped ffmpeg-free, the swap failed, the
|
|
# stage died, and the install finished reporting one red line among twenty
|
|
# minutes of scrollback -- with no Hyprland on the disk at all.
|
|
#
|
|
# The rule that prevents it: anything that can fail for a reason outside this
|
|
# repository goes BELOW the desktop, and goes through `soft`. What is left above
|
|
# the desktop is only what the desktop needs.
|
|
#
|
|
# This pins the order, not the individual commands, because the next fragile
|
|
# thing somebody adds will not be a codec swap.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
installer="$repo_dir/setup/scripts/install-packages"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -x "$installer" ]] || { printf 'desktop first contract: %s is not executable\n' "$installer" >&2; exit 1; }
|
|
|
|
line_of() { grep -n "$1" "$installer" | head -1 | cut -d: -f1; }
|
|
|
|
hyprland_at="$(line_of '^HYPR_FILE=')"
|
|
[[ -n "$hyprland_at" ]] || { printf 'desktop first contract: no Hyprland block found\n' >&2; exit 1; }
|
|
|
|
# ── Nothing fragile above the desktop ───────────────────────────────────────
|
|
#
|
|
# Named individually rather than by pattern: each is a command whose failure is
|
|
# survivable, and each one that EXECUTES above the Hyprland block is a machine
|
|
# that boots to nothing.
|
|
#
|
|
# Executes, not appears: the fragile steps live in functions defined near the
|
|
# top (the server role calls them without ever reaching a desktop section),
|
|
# and a definition runs nothing. So function bodies are excluded from the
|
|
# position scan, and the desktop path's calls to those functions are required
|
|
# to sit below the desktop instead.
|
|
|
|
while IFS= read -r finding; do
|
|
[[ -n "$finding" ]] && note "$finding"
|
|
done < <(python3 - "$installer" "$hyprland_at" <<'PY'
|
|
import re, sys
|
|
|
|
path, hypr = sys.argv[1], int(sys.argv[2])
|
|
lines = open(path, encoding="utf-8").read().splitlines()
|
|
|
|
in_body = False
|
|
body = set()
|
|
for i, line in enumerate(lines, 1):
|
|
if not in_body and re.match(r'^[a-z_]+\(\)\s*\{', line):
|
|
in_body = True
|
|
body.add(i)
|
|
continue
|
|
if in_body:
|
|
body.add(i)
|
|
if line == '}':
|
|
in_body = False
|
|
|
|
fragile = ['dnf swap', 'groupupdate', 'group upgrade', 'gstreamer1-plugins',
|
|
'flatpak install', 'nvm install', 'curl -fsSL']
|
|
for i, line in enumerate(lines, 1):
|
|
if i >= hypr or i in body or line.strip().startswith('#'):
|
|
continue
|
|
for needle in fragile:
|
|
if needle in line:
|
|
print(f"'{needle}' runs at line {i}, above the desktop at line {hypr}")
|
|
|
|
# The desktop path still has to run the fragile helpers -- below the desktop.
|
|
# (The server path calls them above, inside a branch that exits before the
|
|
# desktop section; the exit is asserted back in bash.)
|
|
for call in ('setup_node', 'install_bun', 'install_claude_code', 'install_codex'):
|
|
calls = [i for i, line in enumerate(lines, 1)
|
|
if re.match(r'^\s*' + call + r'\s*$', line) and i not in body]
|
|
if not calls:
|
|
print(f"{call} is never called, so the desktop path skips it")
|
|
elif not any(i > hypr for i in calls):
|
|
print(f"{call} is only called above the desktop")
|
|
PY
|
|
)
|
|
|
|
# The server branch is what excuses fragile calls above the desktop, and only
|
|
# because it never falls through into the desktop section.
|
|
sed -n '/^if \[\[ "\$ROLE" == server \]\]; then/,/^fi/p' "$installer" | grep -q '^\s*exit 0' \
|
|
|| note 'the server branch does not exit before the desktop section'
|
|
|
|
# ── Everything fragile is actually tolerated ────────────────────────────────
|
|
#
|
|
# Being below the desktop is only half of it. A bare `dnf swap` below the
|
|
# Hyprland block still kills every step after it -- the flatpaks, the extras
|
|
# somebody explicitly chose.
|
|
|
|
# Comments dropped and backslash continuations joined, so a `soft` invocation
|
|
# wrapped across three lines reads as the one command it is.
|
|
uncommented() { grep -vE '^\s*#' "$installer" | sed -e :a -e '/\\$/N; s/\\\n\s*/ /; ta'; }
|
|
|
|
while read -r command; do
|
|
uncommented | grep -q "soft .*$command" \
|
|
|| note "'$command' runs without soft, so its failure still ends the stage"
|
|
done <<'FRAGILE'
|
|
dnf swap -y 'ffmpeg-free'
|
|
dnf swap -y mesa-va-drivers
|
|
groupupdate
|
|
group upgrade
|
|
gstreamer1-plugins
|
|
flatpak install -y flathub $FLATPAK_PACKAGES
|
|
FRAGILE
|
|
|
|
grep -q '^soft()' "$installer" \
|
|
|| note 'install-packages defines no soft helper, so nothing can be tolerated deliberately'
|
|
|
|
# ── The desktop failing is still fatal, and still said out loud ─────────────
|
|
#
|
|
# The inverse mistake: making everything survivable turns a machine with no
|
|
# desktop into a run that reports success.
|
|
|
|
python3 - "$installer" "$hyprland_at" <<'PY' || note 'a missing Hyprland does not stop the stage'
|
|
import sys
|
|
lines = open(sys.argv[1], encoding="utf-8").read().splitlines()
|
|
start = int(sys.argv[2])
|
|
after = "\n".join(lines[start:start + 40])
|
|
if "rpm -q hyprland" not in after or "exit 1" not in after:
|
|
raise SystemExit(1)
|
|
PY
|
|
|
|
uncommented | grep -q 'soft .*HYPR_PACKAGES' \
|
|
&& note 'the Hyprland install is tolerated, so a machine with no desktop reports success'
|
|
|
|
# ── Soft failures are reported ──────────────────────────────────────────────
|
|
|
|
grep -q 'softly_failed' "$installer" \
|
|
|| note 'nothing collects what was stepped over, so a tolerated failure is a silent one'
|
|
|
|
python3 - "$installer" <<'PY' || note 'the list of stepped-over steps is never printed'
|
|
import sys
|
|
text = open(sys.argv[1], encoding="utf-8").read()
|
|
tail = text[text.rindex("softly_failed"):]
|
|
if "printf" not in tail and "log" not in tail:
|
|
raise SystemExit(1)
|
|
PY
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
|
|
printf 'desktop first contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'desktop first contract: PASS (desktop at line %s, everything fragile below it)\n' "$hyprland_at"
|