Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion bin/omarchy-update
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,26 @@ cleanup_update() {
}

if [[ -z ${OMARCHY_UPDATE_LOGGED:-} ]]; then
# The transcript holds the whole update session — package names, service
# restarts, anything the terminal echoed — and omarchy-update-analyze-logs
# reads it back afterwards. A fixed name in world-writable /tmp lets any
# other local account pre-plant a symlink at it and have script(1) write the
# session through to a file of their choosing (script opens the transcript
# without O_NOFOLLOW), or read the log from outside the default umask leaves
# it world-readable. Keep it under the per-user runtime directory (0700),
# falling back to the state directory where Omarchy keeps everything else of
# its own when there is no session runtime dir. The resolution here must
# stay in sync with omarchy-update-analyze-logs.
update_log_dir="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}"
update_log="$update_log_dir/omarchy-update.log"

if ! mkdir -p "$update_log_dir" || ! install -m 600 /dev/null "$update_log"; then
echo "Error: cannot create $update_log for the update transcript" >&2
exit 1
fi

script_command=$(printf '%q ' "$0" "$@")
exec env OMARCHY_UPDATE_LOGGED=1 OMARCHY_UPDATE_USER_PATH="$user_path" script -qefc "$script_command" "/tmp/omarchy-update.log"
exec env OMARCHY_UPDATE_LOGGED=1 OMARCHY_UPDATE_USER_PATH="$user_path" script -qefc "$script_command" "$update_log"
fi

if ! omarchy-update-lock held; then
Expand Down
9 changes: 8 additions & 1 deletion bin/omarchy-update-analyze-logs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

# omarchy:summary=Check the update log for known failure conditions

update_log="/tmp/omarchy-update.log"
# Must resolve to the same file omarchy-update writes its transcript to: the
# per-user runtime directory (0700), or the state directory when there is no
# session runtime dir. See the comment there for why this is not a /tmp name.
update_log_dir="${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/omarchy}"
update_log="$update_log_dir/omarchy-update.log"

# A standalone run with no transcript behind it is a no-op, not a grep error.
[[ -r $update_log ]] || exit 0

# Check for initramfs generation failure
if grep -q "Updating linux initcpios" "$update_log"; then
Expand Down
10 changes: 5 additions & 5 deletions docs/update-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The design goal is:
| --- | --- | --- |
| `${XDG_RUNTIME_DIR:-/tmp}/omarchy-update.lock` | user | Prevent overlapping update runs. Owned by `omarchy-update-lock`; compatibility wrappers inherit/respect it. |
| `${XDG_RUNTIME_DIR}/omarchy-update-stay-awake/` | user | Private mode-0700 inhibitor coordination state. If no runtime directory is available, the helper uses the validated mode-0700 `/tmp/omarchy-$UID/` fallback. |
| `/tmp/omarchy-update.log` | user | Transcript of `omarchy update`, used by `omarchy-update-analyze-logs`. |
| `${XDG_RUNTIME_DIR:-${XDG_STATE_HOME:-~/.local/state}/omarchy}/omarchy-update.log` | user | Transcript of `omarchy update`, used by `omarchy-update-analyze-logs`. Staged 0600 under the per-user runtime directory (or the state directory without a session) rather than a predictable world-writable name. |
| `~/.local/state/omarchy/current/` | user | Generated active theme, selected theme name, and current background symlink. |
| `~/.local/state/omarchy/migrations/` | user | Per-user migration markers. |
| `~/.local/state/omarchy/reboot-required` | user | Optional reboot marker checked by `omarchy-update-restart`. |
Expand Down Expand Up @@ -120,7 +120,8 @@ High-level flow:

```text
omarchy-update
├─ ensure transcript logging through script(1) → /tmp/omarchy-update.log
├─ ensure transcript logging through script(1) → $XDG_RUNTIME_DIR/omarchy-update.log
│ └─ or ${XDG_STATE_HOME:-~/.local/state}/omarchy/omarchy-update.log when XDG_RUNTIME_DIR is unset
├─ omarchy-update-lock
│ └─ acquire the update lock and run omarchy-update inside it
├─ omarchy-update-requires-free-space
Expand Down Expand Up @@ -164,8 +165,7 @@ Important behavior:
check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check.
- `omarchy update` checks/runs migrations in the same visible terminal via
`omarchy-migrate` after pacman finishes.
- A failure should leave enough output in `/tmp/omarchy-update.log` and the
terminal transcript to debug.
- A failure should leave enough output in the update transcript (`$XDG_RUNTIME_DIR/omarchy-update.log`, or `${XDG_STATE_HOME:-~/.local/state}/omarchy/omarchy-update.log` without a session) and the terminal transcript to debug.

## Path 2: direct `sudo pacman -Syu` attempt

Expand Down Expand Up @@ -307,7 +307,7 @@ scripts.
| `omarchy-update-aur-pkgs` | Updates AUR packages with `yay -Sua` if foreign packages exist and AUR is reachable. | **Question.** Omarchy is package-backed now, but users may still install AUR packages. Keep for now. |
| `omarchy-update-mise` | Runs `MISE_MINIMUM_RELEASE_AGE=0 mise up` for mise-managed tools — the override of mise's release-age cooldown is the point. | **Keep.** Mise-managed tools are intentionally part of the blessed update path. |
| `omarchy-update-orphan-pkgs` | Lists orphans and prompts before removal; noninteractive mode never removes. | **Keep for now.** Safe because it is prompt-only. |
| `omarchy-update-analyze-logs` | Scans `/tmp/omarchy-update.log` for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
| `omarchy-update-analyze-logs` | Scans the update transcript for known failure patterns, currently initramfs generation. | **Keep/expand.** Useful safety net; should grow only for high-signal checks. |
| `omarchy-update-restart` | Restarts components selected by `restart-*-required` markers, always restarts the shell, and prompts for reboot after kernel/Hyprland updates. Internal phase flags let the update finish sudo-capable restarts before user hooks and defer only the unprivileged reboot prompt. | **Keep.** Important final step; may eventually include service-restart checks. |
| `omarchy-update-firmware` | Manual firmware update command using fwupd. Not part of the normal update pipeline. | **Keep separate.** Firmware is not a routine system update step. |
| `omarchy-update-time` | Restarts `systemd-timesyncd`. | **Question.** Not really an update command. Consider renaming/moving under system/time maintenance. |
Expand Down
121 changes: 121 additions & 0 deletions test/shell.d/update-transcript-staging-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/bash

set -euo pipefail

source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"

source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-update
tmp_dir="$boundary_tmp"
mkdir -p "$tmp_dir/run"

# omarchy-update re-execs itself through script(1) from its fixed PATH, so the
# stub goes in the boundary tree. It records the transcript path and exits,
# which exercises the wrapper without running an update.
cat >"$SUDO_TEST_ROOT/bin/script" <<'SH'
#!/bin/bash
printf '%s' "${@: -1}" >"$OMARCHY_TEST_TRANSCRIPT_PATH"
exit 0
SH
chmod +x "$SUDO_TEST_ROOT/bin/script"

# The boundary copy of omarchy-update reads $SUDO_TEST_HOME for $HOME.
export HOME="$SUDO_TEST_HOME"
export XDG_RUNTIME_DIR="$tmp_dir/run"
export OMARCHY_TEST_TRANSCRIPT_PATH="$tmp_dir/transcript-path"
unset XDG_STATE_HOME OMARCHY_UPDATE_LOGGED

"$SUDO_TEST_ROOT/bin/omarchy-update" >/dev/null 2>&1

transcript=$(<"$OMARCHY_TEST_TRANSCRIPT_PATH")

[[ $transcript == "$XDG_RUNTIME_DIR/omarchy-update.log" ]] ||
fail "the update transcript lives under the per-user runtime directory" "$transcript"
pass "the update transcript lives under the per-user runtime directory"

[[ -f $transcript ]] ||
fail "the transcript file exists before script(1) opens it" "$transcript"
pass "the transcript file exists before script(1) opens it"

mode=$(stat -c '%a' "$transcript" 2>/dev/null || stat -f '%Lp' "$transcript")
[[ $mode == "600" ]] ||
fail "the transcript is readable only by its owner" "mode: $mode"
pass "the transcript is readable only by its owner"

# Without a session runtime dir the transcript falls back to the state
# directory, and omarchy-update-analyze-logs must resolve to the same file.
env -u XDG_RUNTIME_DIR "$SUDO_TEST_ROOT/bin/omarchy-update" >/dev/null 2>&1

transcript=$(<"$OMARCHY_TEST_TRANSCRIPT_PATH")

[[ $transcript == "$HOME/.local/state/omarchy/omarchy-update.log" ]] ||
fail "without a runtime dir the transcript falls back to the state directory" "$transcript"
pass "without a runtime dir the transcript falls back to the state directory"

for file in bin/omarchy-update bin/omarchy-update-analyze-logs; do
if grep -q '/tmp/omarchy-update.log' "$ROOT/$file"; then
fail "$file no longer names a world-writable transcript path"
fi
done
pass "neither update script names a world-writable transcript path"

# analyze-logs reads the same file the transcript was written to.
printf '%s\n' "(1/3) Updating linux initcpios" >"$XDG_RUNTIME_DIR/omarchy-update.log"

output=$("$ROOT/bin/omarchy-update-analyze-logs" 2>&1)
[[ $output == *"Initramfs"* ]] ||
fail "analyze-logs flags an initramfs failure in the transcript" "$output"
pass "analyze-logs flags an initramfs failure in the transcript"

printf '%s\n' "Initcpio image generation successful" >>"$XDG_RUNTIME_DIR/omarchy-update.log"

output=$("$ROOT/bin/omarchy-update-analyze-logs" 2>&1)
[[ -z $output ]] ||
fail "analyze-logs stays quiet once the initramfs succeeded" "$output"
pass "analyze-logs stays quiet once the initramfs succeeded"

# The fallback branch has to agree too, or an update without a session would
# write a transcript its own analyzer cannot find.
printf '%s\n' "(1/3) Updating linux initcpios" >"$HOME/.local/state/omarchy/omarchy-update.log"

output=$(env -u XDG_RUNTIME_DIR -u XDG_STATE_HOME "$ROOT/bin/omarchy-update-analyze-logs" 2>&1)
[[ $output == *"Initramfs"* ]] ||
fail "analyze-logs resolves the fallback transcript the update wrote" "$output"
pass "analyze-logs resolves the fallback transcript the update wrote"

# A standalone run with no transcript behind it is a no-op, not a grep error.
fresh_run="$tmp_dir/run-empty"
mkdir -p "$fresh_run"

output=$(XDG_RUNTIME_DIR="$fresh_run" "$ROOT/bin/omarchy-update-analyze-logs" 2>&1)
[[ -z $output ]] ||
fail "analyze-logs without a transcript is silent" "$output"
pass "analyze-logs without a transcript is silent"

docs="$ROOT/docs/update-process.md"
python3 - "$docs" <<'PY' || fail "the failure-output list item names both transcript paths on one line"
import sys

items = [
line for line in open(sys.argv[1])
if line.startswith("- A failure should leave")
]
if len(items) != 1:
raise SystemExit(1)
if "XDG_RUNTIME_DIR" not in items[0] or "XDG_STATE_HOME" not in items[0]:
raise SystemExit(1)
PY
pass "the failure-output list item names both transcript paths on one line"

python3 - "$docs" <<'PY' || fail "the omarchy-update flow documents the no-session transcript fallback"
import sys

text = open(sys.argv[1]).read()
start = text.find("## Path 1:")
end = text.find("## Path 2:")
block = text[start:end]
fence = block.split("```text", 1)[1].split("```", 1)[0]
if "XDG_RUNTIME_DIR" not in fence or "XDG_STATE_HOME" not in fence:
raise SystemExit(1)
PY
pass "the omarchy-update flow documents the no-session transcript fallback"