Simplify the Quattro upgrade, and stop it leaving an error bar behind (#6716)

* Accept either name for the lock authentication command

omarchy-setup-lock was renamed to omarchy-apply-lock in 536fcd5c, but
the upgrade calls into whatever the channel just installed, and every
released package still ships the old name. The rename only moves in
lockstep for the ISO, which installs the runtime from the mirror it
ships with; the upgrade has no such guarantee, so it aborted every run
with "omarchy-apply-lock is unavailable" right before the point of no
return.

Prefer the new name and fall back to the old one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Leave the Omarchy 3 session alone until the reboot

The upgrade tried to start Quickshell in the live session, and only
stopped waybar/walker/elephant if that worked. Every branch of it could
fail, so it needed a warning for each, and those warnings were the first
thing users read at the end of an upgrade that had otherwise succeeded.

The reboot is the cutover. Swapping the UI out underneath a running
session buys nothing, so drop the attempt and both functions with it.
The Omarchy 3 bar, launcher, and notifications keep working until the
reboot, which is what happened anyway whenever the start failed.

Also warn up front when the live Hyprland session cannot be reached, and
drop the package_mode label that was set on both branches of the
dev-package check and never read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Shim the legacy Hyprland defaults from the backup, not from GitHub

populate_legacy_hypr_defaults tried a sparse git clone of master, then a
curl of the master tarball, and only if both failed the backup taken a
few lines earlier. That backup is the checkout the running session is
sourcing right now: it is the correct content, it is already on disk,
and it needs no network in the middle of an upgrade. master is a guess
that is wrong for any machine not on master.

Try the backup first and drop the git clone, which fetched the same
thing as the tarball by a longer route. The network path stays for the
case that has no backup, where the legacy root was already a symlink.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Clear the Hyprland error bar the upgrade leaves behind

The two settings were applied through both the keyword and the Lua API,
in four calls on the way out and three on the way in, split differently
each time. Fold that into one helper that takes the value.

The error bar needed more than suppression. An explicit hyprctl reload
re-reads the config from disk, which resets both keywords before it
reports what it found, so suppression cannot survive one; anything that
reloaded during the swap left the bar on screen. Hyprland then keeps it
up until a later clean reload, which this script deliberately never
performs, so it was still there when the upgrade finished. Clear the
overlay on the way out, once the shims have made the legacy config
resolve again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Run the packaged firewall config instead of a copy of it

configure_snapper_policy and configure_lock_authentication already call
into the installed tree; apply_firewall_defaults reimplemented
install/config/firewall.sh inline instead, and had already drifted from
it. The packaged script also installs the ufw-docker rules, so upgraded
machines came up without the Docker firewall protections a fresh install
gets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Stop the Voxtype toggle migration reloading a mid-swap session

This is the reload behind the "source= globbing error" bar users see
during a Quattro upgrade. The upgrade runs the packaged migrations
against a still-running Omarchy 3 session, and this one ends with
hyprctl reload. That re-parses a legacy config whose theme source= has
nothing to resolve to yet, so Hyprland paints an error bar and keeps it
up until a later clean reload the upgrade deliberately never performs.
It was still on screen when the upgrade finished.

The upgrade already exports OMARCHY_UPGRADE_TO_QUATTRO_LIVE for exactly
this, and 1782002156 honors it. Do the same here. Nothing in that
session reads the toggle being removed; the reboot applies it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Assert the retired session entry points stay gone

The ordering check would still pass if either came back, while the
comment above it claims they cannot. Name them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Upgrade onto the channel the machine is already on

The upgrade always defaulted to stable, so an rc machine taking it
landed on production packages from the stable repo. rc callers worked
around that by passing --channel rc, which then forced every caller
onto rc, stable machines included.

Read the channel off the mirrorlist the way omarchy-version-channel
does and follow it: stable machines get omarchy and omarchy-settings
from stable, and rc or edge machines get omarchy-dev and
omarchy-settings-dev from edge, which is what --dev already selects.
An explicit --channel or --dev still wins, and an unrecognized
mirrorlist still falls back to stable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Keep rc machines on the rc mirror when taking dev packages

--dev forced the edge channel, so an rc machine following its own
channel was moved onto the edge Arch mirror as well. The constraint is
narrower than that: the dev packages are only published to the edge
package repo, which the rc and edge channels both already point at.
Only stable is incompatible.

Reject --dev only for stable, and default to edge just when no channel
was chosen. rc machines now upgrade against rc-mirror with the dev
packages out of the edge repo, which is where Quattro lives until it
ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-08-11 22:47:39 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 1bb6600c76
commit b99fd91cf1
3 changed files with 137 additions and 115 deletions
+114 -111
View File
@@ -51,6 +51,23 @@ valid_channel() {
normalize_channel "$1" >/dev/null 2>&1
}
# The channel the legacy install is already on, read the way
# omarchy-version-channel reads it. Order matters: mirror.omarchy.org is a
# substring of both of the others.
detect_installed_channel() {
[[ -f /etc/pacman.d/mirrorlist ]] || return 1
if grep -q 'https://stable-mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
echo stable
elif grep -q 'https://rc-mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
echo rc
elif grep -q 'https://mirror.omarchy.org/' /etc/pacman.d/mirrorlist; then
echo edge
else
return 1
fi
}
channel_override="${OMARCHY_UPGRADE_CHANNEL:-}"
channel_override_cli=0
target_user="${OMARCHY_INSTALL_USER:-}"
@@ -98,16 +115,32 @@ case "$use_dev_packages" in
*) fail "Invalid OMARCHY_UPGRADE_DEV value '$use_dev_packages'. Use 1/0, true/false, or pass --dev." ;;
esac
# Land on the Quattro equivalent of wherever the machine already is, unless the
# caller said otherwise. Stable machines get the production packages off the
# stable channel. rc and edge machines stay on their own mirror, but take the
# dev packages, because that is where Quattro lives until it ships.
if [[ -z $channel_override ]] && (( ! use_dev_packages )); then
case "$(detect_installed_channel || true)" in
stable) channel_override=stable ;;
rc) channel_override=rc use_dev_packages=1 ;;
edge) channel_override=edge use_dev_packages=1 ;;
esac
fi
if (( use_dev_packages )); then
if (( channel_override_cli )); then
# The dev packages are only published to the edge package repo, which the rc
# and edge channels both point at; only stable is incompatible. rc keeps its
# own Arch mirror either way.
if [[ -n $channel_override ]]; then
if ! valid_channel "$channel_override"; then
fail "Invalid channel '$channel_override'. Use stable, rc, or edge."
fi
if [[ $(normalize_channel "$channel_override") != edge ]]; then
fail "--dev uses the edge channel; remove --channel or pass --channel edge."
if [[ $(normalize_channel "$channel_override") == stable ]]; then
fail "--dev needs the edge package repo; use --channel rc or --channel edge."
fi
else
channel_override=edge
fi
channel_override=edge
fi
if { [[ -n $channel_override ]] || (( channel_override_cli )); } && ! valid_channel "$channel_override"; then
@@ -296,9 +329,6 @@ case "$channel" in
esac
if (( ! yes )); then
package_mode="production Omarchy packages"
(( use_dev_packages )) && package_mode="dev packages [omarchy-dev / omarchy-settings-dev]"
printf '\033[2J\033[3J\033[H' >/dev/tty
cat <<EOF
████████▄ ███ █▄ ▄████████ ███ ███ ▄████████ ▄██████▄
@@ -612,9 +642,17 @@ configure_snapper_policy() {
}
configure_lock_authentication() {
local apply_lock=/usr/share/omarchy/bin/omarchy-apply-lock
# The upgrade runs against whatever the channel currently ships, which can
# still predate the rename of omarchy-setup-lock to omarchy-apply-lock.
local apply_lock="" candidate
for candidate in /usr/share/omarchy/bin/omarchy-{apply,setup}-lock; do
if [[ -x $candidate ]]; then
apply_lock=$candidate
break
fi
done
[[ -x $apply_lock ]] || fail "$apply_lock is unavailable; lock screen authentication could not be configured."
[[ -n $apply_lock ]] || fail "Neither omarchy-apply-lock nor omarchy-setup-lock is available under /usr/share/omarchy/bin; lock screen authentication could not be configured."
log "Configuring lock screen authentication"
as_root env \
@@ -907,40 +945,27 @@ legacy_hypr_config_active() {
}
populate_legacy_hypr_defaults() {
local shim_hypr_dir="$1" backup="$2" tmp_dir clone_dir archive_dir
local shim_hypr_dir="$1" backup="$2" tmp_dir archive_dir
rm -rf "$shim_hypr_dir"
mkdir -p "$shim_hypr_dir"
tmp_dir=$(mktemp -d)
clone_dir="$tmp_dir/omarchy"
archive_dir="$tmp_dir/archive"
if command -v git >/dev/null 2>&1; then
if git clone --depth 1 --branch master --single-branch --filter=blob:none --sparse https://github.com/basecamp/omarchy.git "$clone_dir" >/dev/null 2>&1 && \
git -C "$clone_dir" sparse-checkout set default/hypr >/dev/null 2>&1 && \
[[ -d $clone_dir/default/hypr ]]; then
cp -a "$clone_dir/default/hypr/." "$shim_hypr_dir/"
# The backup is the checkout the running session is sourcing right now, so it
# is both the correct content and already on disk. Only reach for the network
# when there is no backup, which means the legacy root was already a symlink.
if [[ -n $backup && -d $backup/default/hypr ]]; then
cp -a "$backup/default/hypr/." "$shim_hypr_dir/"
elif command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1; then
tmp_dir=$(mktemp -d)
archive_dir="$tmp_dir/archive"
mkdir -p "$archive_dir"
if curl -fsSL https://github.com/basecamp/omarchy/archive/refs/heads/master.tar.gz | tar -xz -C "$archive_dir" &&
[[ -d $archive_dir/omarchy-master/default/hypr ]]; then
cp -a "$archive_dir/omarchy-master/default/hypr/." "$shim_hypr_dir/"
fi
rm -rf "$tmp_dir"
fi
if ! find "$shim_hypr_dir" -type f -name '*.conf' -print -quit | grep -q .; then
if command -v curl >/dev/null 2>&1 && command -v tar >/dev/null 2>&1; then
mkdir -p "$archive_dir"
if curl -fsSL https://github.com/basecamp/omarchy/archive/refs/heads/master.tar.gz | tar -xz -C "$archive_dir"; then
if [[ -d $archive_dir/omarchy-master/default/hypr ]]; then
cp -a "$archive_dir/omarchy-master/default/hypr/." "$shim_hypr_dir/"
fi
fi
fi
fi
if ! find "$shim_hypr_dir" -type f -name '*.conf' -print -quit | grep -q .; then
if [[ -n $backup && -d $backup/default/hypr ]]; then
cp -a "$backup/default/hypr/." "$shim_hypr_dir/"
fi
fi
rm -rf "$tmp_dir"
find "$shim_hypr_dir" -type f -name '*.conf' -print -quit | grep -q .
}
@@ -1072,50 +1097,9 @@ cleanup_retired_services() {
fi
}
stop_retired_session_processes() {
log "Stopping retired Omarchy 3 session processes"
if [[ -S $target_runtime_dir/bus ]]; then
run_as_user_session systemctl --user stop "${retired_user_units[@]}" >/dev/null 2>&1 || true
fi
# Removed packages do not stop already-running processes. Stop the old UI
# only after the new Quickshell has been verified, so users are not left
# without a bar/launcher if current-session shell startup fails.
run_as_user pkill -x waybar >/dev/null 2>&1 || true
run_as_user pkill -x walker >/dev/null 2>&1 || true
run_as_user pkill -x elephant >/dev/null 2>&1 || true
run_as_user pkill -x mako >/dev/null 2>&1 || true
run_as_user pkill -x swayosd-server >/dev/null 2>&1 || true
run_as_user_session systemctl --user daemon-reload >/dev/null 2>&1 || true
}
start_omarchy_shell_session() {
# Omarchy quattro starts Quickshell from Hyprland autostart, not a user service.
# Reboot is still the real cutover, but start the new shell in the current
# Wayland session when possible so users do not sit without a bar after the
# retired waybar/walker/elephant processes are stopped.
local restart_shell_bin shell_log
command -v quickshell >/dev/null || { warn "quickshell is not available; Omarchy shell will start after reboot."; return 1; }
restart_shell_bin=/usr/share/omarchy/bin/omarchy-restart-shell
[[ -x $restart_shell_bin ]] || { warn "omarchy-restart-shell is not available; Omarchy shell will start after reboot."; return 1; }
[[ -f /usr/share/omarchy/shell/shell.qml ]] || { warn "/usr/share/omarchy/shell is missing; Omarchy shell will start after reboot."; return 1; }
shell_log="$target_home/.local/state/omarchy/omarchy-shell-upgrade.log"
run_as_user mkdir -p "$target_home/.local/state/omarchy" >/dev/null 2>&1 || true
log "Starting Omarchy shell in the current session"
run_as_user_wayland_session \
QT_QPA_PLATFORM=wayland \
'GDK_BACKEND=wayland,x11,*' \
bash -c '
restart_shell_bin=$1
shell_log=$2
: >"$shell_log"
"$restart_shell_bin" >>"$shell_log" 2>&1
' bash "$restart_shell_bin" "$shell_log" \
|| { warn "Could not start Omarchy shell in the current session (no live Hyprland session, or shell startup failed); it will start after reboot. See $shell_log"; return 1; }
}
# The Omarchy 3 session keeps its bar, launcher, and notifications until the
# reboot, which is the real cutover. Swapping the UI out underneath a running
# session buys nothing and only creates ways for the upgrade to look broken.
# Run a command inside the user's live Wayland session. Fails if no session
# can be discovered. Extra NAME=VALUE args extend the environment.
@@ -1145,31 +1129,49 @@ run_hyprctl_session() {
}
suppress_hyprland_config_reload() {
# Say so up front when the session cannot be reached at all. Otherwise this
# silently does nothing and the only evidence is the config error bar that
# shows up minutes later.
if ! run_as_user_wayland_session true >/dev/null 2>&1; then
warn "Could not reach the live Hyprland session; it will report config errors during the swap, and the Omarchy shell will start after reboot."
return 0
fi
log "Suppressing live Hyprland reloads during the config swap"
set_hyprland_reload_state true
hyprland_config_reload_suppressed=1
}
local changed=0
if run_hyprctl_session keyword misc:disable_autoreload true >/dev/null 2>&1; then
changed=1
fi
if run_hyprctl_session eval 'hl.config({ misc = { disable_autoreload = true }, debug = { suppress_errors = true } })' >/dev/null 2>&1; then
changed=1
fi
run_hyprctl_session keyword debug:suppress_errors true >/dev/null 2>&1 || true
# Which API answers depends on the parser the running Hyprland was started
# with: the legacy .conf parser only takes keyword, and Quattro's Lua parser
# rejects it outright with "keyword can't work with non-legacy parsers". The
# session being upgraded is the legacy one, but the hyprctl binary is whichever
# the package transaction has installed by then, so ask both ways.
set_hyprland_reload_state() {
local value="$1"
if (( changed )); then
hyprland_config_reload_suppressed=1
else
warn "Could not disable Hyprland auto-reload; the live session may briefly report config errors."
fi
run_hyprctl_session keyword misc:disable_autoreload "$value" >/dev/null 2>&1 || true
run_hyprctl_session keyword debug:suppress_errors "$value" >/dev/null 2>&1 || true
run_hyprctl_session eval \
"hl.config({ misc = { disable_autoreload = $value }, debug = { suppress_errors = $value } })" \
>/dev/null 2>&1 || true
}
restore_hyprland_config_reload() {
(( hyprland_config_reload_suppressed )) || return 0
run_hyprctl_session keyword misc:disable_autoreload false >/dev/null 2>&1 || true
run_hyprctl_session eval 'hl.config({ misc = { disable_autoreload = false } })' >/dev/null 2>&1 || true
run_hyprctl_session keyword debug:suppress_errors false >/dev/null 2>&1 || true
run_hyprctl_session eval 'hl.config({ debug = { suppress_errors = false } })' >/dev/null 2>&1 || true
set_hyprland_reload_state false
# Backstop. An explicit hyprctl reload re-reads the config from disk, which
# resets both keywords above before it reports what it found, so suppression
# cannot survive one, and Hyprland keeps the resulting error bar up until a
# later clean reload that this script deliberately never performs. Callers
# that reload during the swap are supposed to check
# OMARCHY_UPGRADE_TO_QUATTRO_LIVE, but one slipping through should not leave
# an error bar on a finished upgrade. By now the shims have made the legacy
# config resolve again, so clearing the overlay is safe.
run_hyprctl_session seterror disable >/dev/null 2>&1 || true
hyprland_config_reload_suppressed=0
}
@@ -1250,27 +1252,33 @@ run_post_upgrade_update_steps() {
fi
}
enable_system_service() {
local unit="$1"
as_root systemctl enable "$unit" >/dev/null 2>&1 || warn "Could not enable $unit; it may not be installed on this system."
}
apply_firewall_defaults() {
# Run the packaged config script rather than a copy of it, the same way
# configure_snapper_policy and configure_lock_authentication already do. The
# copy this replaces had drifted: it never installed the ufw-docker rules, so
# upgraded machines came up without the Docker firewall protections a fresh
# install gets.
local firewall_script=/usr/share/omarchy/install/config/firewall.sh
if ! command -v ufw >/dev/null 2>&1; then
warn "ufw is not installed; skipping firewall defaults."
return 0
fi
if ! as_root test -f "$firewall_script"; then
warn "$firewall_script is unavailable; firewall defaults were not applied."
return 0
fi
log "Applying Omarchy firewall defaults"
as_root ufw default deny incoming >/dev/null || true
as_root ufw default allow outgoing >/dev/null || true
as_root ufw allow 53317/udp >/dev/null || true
as_root ufw allow 53317/tcp >/dev/null || true
as_root ufw allow in proto udp from 172.16.0.0/12 to 172.17.0.1 port 53 comment 'allow-docker-dns' >/dev/null || true
as_root ufw allow in proto udp from 192.168.0.0/16 to 172.17.0.1 port 53 comment 'allow-docker-dns' >/dev/null || true
as_root sed -i 's/^ENABLED=.*/ENABLED=yes/' /etc/ufw/ufw.conf 2>/dev/null || true
enable_system_service ufw.service
as_root env OMARCHY_PATH=/usr/share/omarchy PATH="$package_path" \
bash -euo pipefail "$firewall_script" ||
warn "Could not apply firewall defaults; run 'sudo bash $firewall_script' after reboot."
}
root_filesystem_encrypted() {
@@ -2418,11 +2426,6 @@ refresh_current_theme_after_upgrade
# pre-reboot session can keep running safely; the reboot performs the real
# Omarchy quattro Lua-config cutover.
restore_hyprland_config_reload
if start_omarchy_shell_session; then
stop_retired_session_processes
else
warn "Leaving retired Omarchy 3 session processes running until reboot."
fi
upgrade_completed=1
+9 -1
View File
@@ -1,4 +1,12 @@
echo "Remove the obsolete Voxtype Hyprland toggle"
rm -f "$HOME/.local/state/omarchy/toggles/hypr/voxtype.lua"
hyprctl reload >/dev/null 2>&1 || true
# The Quattro upgrade runs the packaged migrations against a still-running
# Omarchy 3 session whose config is mid-swap, so a reload here only re-parses a
# config that cannot resolve yet. That paints an error bar Hyprland then keeps
# up until something reloads cleanly, which the upgrade deliberately never does.
# Nothing is reading this toggle in that session anyway; the reboot applies it.
if [[ ${OMARCHY_UPGRADE_TO_QUATTRO_LIVE:-0} != "1" ]]; then
hyprctl reload >/dev/null 2>&1 || true
fi
+14 -3
View File
@@ -104,13 +104,24 @@ grep -F '>&2' <<<"$cleanup_body" >/dev/null ||
started_line=$(grep -n '^upgrade_started=1$' "$upgrade_to_quattro" | cut -d: -f1)
completed_line=$(grep -n '^upgrade_completed=1$' "$upgrade_to_quattro" | cut -d: -f1)
suppress_line=$(grep -n '^suppress_hyprland_config_reload$' "$upgrade_to_quattro" | cut -d: -f1)
shell_line=$(grep -n '^if start_omarchy_shell_session; then$' "$upgrade_to_quattro" | cut -d: -f1)
[[ -n $started_line && -n $completed_line && -n $suppress_line && -n $shell_line ]] ||
# The reboot is the cutover, so the last mutating step hands the live session
# back rather than swapping the shell out underneath it.
last_step_line=$(grep -n '^restore_hyprland_config_reload$' "$upgrade_to_quattro" | cut -d: -f1)
[[ -n $started_line && -n $completed_line && -n $suppress_line && -n $last_step_line ]] ||
fail "upgrade progress markers and the mutating step range exist"
(( started_line < suppress_line )) || fail "the upgrade is marked started before the first mutation"
(( completed_line > shell_line )) || fail "the upgrade is marked complete only after the last step"
(( completed_line > last_step_line )) || fail "the upgrade is marked complete only after the last step"
pass "Omarchy 4 upgrade reports an aborted run instead of exiting silently"
# Ordering alone would still pass if either retired entry point came back, so
# name them: the reboot is the cutover, and nothing may swap the shell out from
# under the session being replaced.
! grep -q 'start_omarchy_shell_session' "$upgrade_to_quattro" ||
fail "Omarchy 4 upgrade does not start the shell in the session it is replacing"
! grep -q 'stop_retired_session_processes' "$upgrade_to_quattro" ||
fail "Omarchy 4 upgrade leaves the retired session processes running until reboot"
pass "Omarchy 4 upgrade leaves the Omarchy 3 session alone until the reboot"
grep -F 'omarchy-bar defaults' "$upgrade_to_quattro" >/dev/null
pass "Omarchy 4 upgrade restores service-aware bar defaults"