Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
- Docs: the keybinding reference describes the Lua configuration instead of the removed `userprefs.conf`, documents binds that were missing from it, corrects four descriptions that no longer matched the code, and explains why an override needs the original bind's flags
- Docs: the shipped `hypr/hyprland.lua` stub shows a working bind instead of pointing at a wiki that does not exist
- Docs: the keybinding links in the German, Arabic, French, Dutch and Turkish readmes resolve again
- Core: every migration that has not been applied yet now runs in version order and is recorded, instead of only the newest one running and retiring the rest unseen
- Core: app launchers no longer show a false error when an unrelated `DEBUG` variable contains a non-boolean value such as `release`
- Desktop: the generated battery notification startup command now launches `batterynotify.lua` instead of the removed shell implementation
- Hyprland: Lua keybinds again match the documented shortcuts for window management, screenshots, wallpapers, Waybar, selectors, workspaces and the scratchpad
Expand Down
35 changes: 35 additions & 0 deletions Scripts/global_fn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,38 @@ print_log() {
cat
fi
}

# Runs each migration in "$1" missing from the record in "$2", in version order,
# and records the ones that exit zero. Migrations must therefore be safe to run
# on a machine that has no record yet, which replays all of them once.
run_pending_migrations() {
local migrationDir="$1"
local stateFile="$2"
local migrationFile
local applied=0
local pending=0

[ -d "${migrationDir}" ] || return 0
find "${migrationDir}" -maxdepth 1 -type f | grep -q . || return 0

mkdir -p "$(dirname "${stateFile}")"
[ -f "${stateFile}" ] || : >"${stateFile}"

while read -r migrationFile; do
[ -n "${migrationFile}" ] || continue
grep -qxF "${migrationFile}" "${stateFile}" && continue
pending=$((pending + 1))
echo "Found migration file: ${migrationFile}"
# stdin is closed for the migration: inheriting the loop's stdin let one
# that reads input swallow the names of every migration after it.
if sh "${migrationDir}/${migrationFile}" </dev/null; then
printf '%s\n' "${migrationFile}" >>"${stateFile}"
applied=$((applied + 1))
else
print_log -warn "Migration" "Failed to execute ${migrationFile}"
fi
done < <(find "${migrationDir}" -maxdepth 1 -type f -printf '%f\n' | sort -V)

[ "${pending}" -gt 0 ] || echo "No outstanding migrations in ${migrationDir}."
return 0
}
11 changes: 2 additions & 9 deletions Scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -453,16 +453,9 @@ if has_operation "restore"; then

echo "Running migrations from: ${migrationDir}"

if [ -d "${migrationDir}" ] && find "${migrationDir}" -type f | grep -q .; then
migrationFile=$(find "${migrationDir}" -maxdepth 1 -type f -printf '%f\n' | sort -r | head -n 1)
migrationStateFile="${XDG_STATE_HOME:-${HOME}/.local/state}/hyde/migration/applied"

if [[ -n "${migrationFile}" && -f "${migrationDir}/${migrationFile}" ]]; then
echo "Found migration file: ${migrationFile}"
sh "${migrationDir}/${migrationFile}" || { true && print_log -warn "Migration" "Failed to execute ${migrationFile}"; }
else
echo "No migration file found in ${migrationDir}. Skipping migrations."
fi
fi
run_pending_migrations "${migrationDir}" "${migrationStateFile}"

fi

Expand Down
2 changes: 1 addition & 1 deletion Scripts/migrations/v26.4.3.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh

echo "Reloading hyprlock state"
hyde-shell hyprlock --reload
2 changes: 1 addition & 1 deletion Scripts/migrations/v26.7.4.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh

# The Lua release replaced a set of shell helpers with Lua and Python
# equivalents. Deployment overwrites files but does not delete the ones that
Expand Down
96 changes: 96 additions & 0 deletions tests/test_migrations.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Migrations have to run in version order, once each, and record what succeeded.

. "$(dirname -- "$0")/lib/common.sh"

# shellcheck source=/dev/null
if ! . "$REPO_ROOT/Scripts/global_fn.sh" 2>/dev/null; then
fail "unable to source global_fn.sh"
finish
fi

if ! command -v run_pending_migrations >/dev/null 2>&1 && ! type run_pending_migrations >/dev/null 2>&1; then
fail "run_pending_migrations is not defined in global_fn.sh"
finish
fi

work_dir=$(mktemp -d)
trap 'rm -rf "$work_dir"' EXIT

migration_dir="$work_dir/migrations"
state_file="$work_dir/state/applied"
order_log="$work_dir/order.log"
mkdir -p "$migration_dir"

for version in v25.9.1 v26.10.1; do
printf '#!/usr/bin/env sh\nprintf "%%s\\n" "%s" >>"%s"\n' "$version" "$order_log" \
>"$migration_dir/$version.sh"
done
# Reads stdin: inheriting the runner's stdin would let it swallow the names of
# the migrations queued after it.
printf '#!/usr/bin/env sh\ncat >/dev/null\nprintf "%%s\\n" "v26.4.3" >>"%s"\n' "$order_log" \
>"$migration_dir/v26.4.3.sh"
failure_log="$work_dir/failures.log"
printf '#!/usr/bin/env sh\nprintf "%%s\\n" "ran" >>"%s"\nexit 1\n' "$failure_log" \
>"$migration_dir/v26.11.0.sh"
chmod +x "$migration_dir"/*.sh

run_pending_migrations "$migration_dir" "$state_file" >/dev/null 2>&1

expected_order='v25.9.1
v26.4.3
v26.10.1'
actual_order=$(cat "$order_log" 2>/dev/null || true)
[ "$actual_order" = "$expected_order" ] ||
fail "migrations ran out of version order: $(echo "$actual_order" | tr '\n' ' ')"

for version in v25.9.1 v26.4.3 v26.10.1; do
grep -qxF "$version.sh" "$state_file" ||
fail "$version.sh succeeded but was not recorded as applied"
done

grep -qxF 'v26.11.0.sh' "$state_file" &&
fail "a migration that exited non-zero was recorded as applied"

: >"$order_log"
run_pending_migrations "$migration_dir" "$state_file" >/dev/null 2>&1

[ -s "$order_log" ] &&
fail "already applied migrations ran a second time: $(tr '\n' ' ' <"$order_log")"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

failure_runs=$(wc -l <"$failure_log" 2>/dev/null || echo 0)
[ "$failure_runs" -eq 2 ] ||
fail "a failed migration ran $failure_runs time(s) across two passes, expected 2"

second_pass=$(run_pending_migrations "$migration_dir" "$state_file" 2>&1 || true)
case $second_pass in
*"No outstanding migrations"*)
fail "a migration was still pending but the run reported none outstanding"
;;
esac

# A mention in a comment is not a call: the name has to sit where a command goes.
grep -qE '^[[:space:]]*run_pending_migrations[[:space:]]+' "$REPO_ROOT/Scripts/install.sh" ||
fail "install.sh does not invoke run_pending_migrations as a command"

missing_dir_output=$(run_pending_migrations "$work_dir/absent" "$state_file" 2>&1)
[ -z "$missing_dir_output" ] ||
fail "a missing migration directory produced output: $missing_dir_output"

# The runner invokes each migration with sh, so the shebang decides nothing.
# A migration that promises bash and then uses a bashism breaks under dash;
# declaring sh also puts it under the dialect pass in test_shell.
shipped_count=0
for migration in "$REPO_ROOT"/Scripts/migrations/*.sh; do
[ -f "$migration" ] || continue
shipped_count=$((shipped_count + 1))

case $(head -n 1 "$migration") in
'#!'*[!a-z]sh | '#!'*[!a-z]sh' '*) ;;
*) fail "${migration#"$REPO_ROOT"/} is run with sh but does not declare it" ;;
esac
done

printf ' %d shipped migration(s) checked\n' "$shipped_count"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
finish
Loading