#!/usr/bin/env bash # Versioned upgrades for settings.json. # # The schema says what a setting IS; it cannot say what a setting USED to be. # Rename a key, change its units, or split one setting into two, and the stored # value stops being recognized -- and unrecognized keys are deliberately carried # through untouched, so the user's choice silently stops taking effect with # nothing to explain it. # # The list of migrations is empty today, which is exactly why this is tested # now: the first time it runs for real will be against somebody's actual # settings during an upgrade, and that is a poor moment to discover how it # behaves. The harness supplies fixture steps, including one that throws. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" harness="$repo_dir/config/dot/quickshell/migrations-harness.qml" fail() { printf 'migrations contract: %s\n' "$1" >&2 exit 1 } [[ -r "$harness" ]] || fail "the harness is missing: $harness" out="$(timeout 60 qs -p "$harness" 2>&1 | grep -o 'PANAMA-MIGRATIONS .*' | sed 's/^PANAMA-MIGRATIONS //')" [[ -n "$out" ]] || fail 'the harness produced no result' jq -e . >/dev/null 2>&1 <<<"$out" || fail "the harness did not emit JSON: $out" check() { jq -e "$1" >/dev/null <<<"$out" || fail "$2 -- got $(jq -c "$3" <<<"$out")" } # A file written before versioning existed is stamped, NOT migrated. Running # the list against it would apply upgrades designed for schemas it never had. check '.unversioned.v == 1 and .unversioned.migrated == false and .unversioned.untouched == true' \ 'a file with no schemaVersion must be stamped at the baseline without being migrated' '.unversioned' # The stamp has to reach disk. Reported as changed-but-not-migrated, it would # otherwise live only in memory and be redone on every single launch. check '.unversioned.changed == true' \ 'stamping a pre-versioning file must be reported as a change so it gets written' '.unversioned' # A file from the future must not be rewritten at all. check '.future.changed == false' \ 'a file from a newer version must not be written back' '.future' # Every step above the stored version runs, in order. check '.upgrade.v == 3 and .upgrade.b == 2 and .upgrade.c == "three" and .upgrade.count == 2' \ 'an older file must run each pending step in order and end at the current version' '.upgrade' # Already current: nothing runs, nothing is touched. check '.current.migrated == false and .current.kept == true and .current.b == true' \ 'a file already at the current version must be left alone' '.current' # A file from a NEWER Panama is left completely alone. Downgrading keys is not # something this can do correctly, and unknown keys are already preserved. check '.future.v == 9 and .future.migrated == false and .future.kept == true' \ 'a file from a newer version must not be modified or downgraded' '.future' # A failing step stops at the last good version. Skipping past it would lose # the conversion forever; failing the whole load would cost every setting. check '.failure.v == 3 and .failure.count == 2 and .failure.kept == true' \ 'a failing step must stop at the last good version, keeping the steps that succeeded' '.failure' # The promise that makes rollback safe. check '.preserved.kept == true' \ 'a migration must not discard keys it does not recognize' '.preserved' printf 'migrations contract: PASS\n'