From 4d65702868f32aa54260d5739d82bf1d96cba18b Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 30 Jan 2026 15:29:13 -0800 Subject: [PATCH] Sort host lists in state file for consistent output (#174) * Sort host lists in state file for consistent output Multi-host stacks (like glances) now have their host lists sorted alphabetically when saving state. This makes git diffs cleaner by avoiding spurious reordering changes. --- README.md | 60 +++++++++++++++++++-------------------- src/compose_farm/state.py | 7 +++-- tests/test_state.py | 10 +++++++ 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index e9ae533..0a42e46 100644 --- a/README.md +++ b/README.md @@ -486,32 +486,32 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a │ --help -h Show this message and exit. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Configuration ────────────────────────────────────────────────────────────────────────╮ -│ traefik-file Generate a Traefik file-provider fragment from compose Traefik labels. │ -│ refresh Update local state from running stacks. │ -│ check Validate configuration, traefik labels, mounts, and networks. │ -│ init-network Create Docker network on hosts with consistent settings. │ -│ config Manage compose-farm configuration files. │ -│ ssh Manage SSH keys for passwordless authentication. │ +│ traefik-file Generate a Traefik file-provider fragment from compose Traefik labels. │ +│ refresh Update local state from running stacks. │ +│ check Validate configuration, traefik labels, mounts, and networks. │ +│ init-network Create Docker network on hosts with consistent settings. │ +│ config Manage compose-farm configuration files. │ +│ ssh Manage SSH keys for passwordless authentication. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Lifecycle ────────────────────────────────────────────────────────────────────────────╮ -│ up Start stacks (docker compose up -d). Auto-migrates if host changed. │ -│ down Stop stacks (docker compose down). │ -│ stop Stop services without removing containers (docker compose stop). │ -│ pull Pull latest images (docker compose pull). │ -│ restart Restart running containers (docker compose restart). │ -│ update Update stacks (pull + build + up). Shorthand for 'up --pull --build'. │ -│ apply Make reality match config (start, migrate, stop strays/orphans as │ -│ needed). │ -│ compose Run any docker compose command on a stack. │ +│ up Start stacks (docker compose up -d). Auto-migrates if host changed. │ +│ down Stop stacks (docker compose down). │ +│ stop Stop services without removing containers (docker compose stop). │ +│ pull Pull latest images (docker compose pull). │ +│ restart Restart running containers (docker compose restart). │ +│ update Update stacks (pull + build + up). Shorthand for 'up --pull --build'. │ +│ apply Make reality match config (start, migrate, stop strays/orphans as │ +│ needed). │ +│ compose Run any docker compose command on a stack. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Monitoring ───────────────────────────────────────────────────────────────────────────╮ -│ logs Show stack logs. With --service, shows logs for just that service. │ -│ ps Show status of stacks. │ -│ stats Show overview statistics for hosts and stacks. │ -│ list List all stacks and their assigned hosts. │ +│ logs Show stack logs. With --service, shows logs for just that service. │ +│ ps Show status of stacks. │ +│ stats Show overview statistics for hosts and stacks. │ +│ list List all stacks and their assigned hosts. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Server ───────────────────────────────────────────────────────────────────────────────╮ -│ web Start the web UI server. │ +│ web Start the web UI server. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ``` @@ -1017,13 +1017,13 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a │ --help -h Show this message and exit. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ─────────────────────────────────────────────────────────────────────────────╮ -│ init Create a new config file with documented example. │ -│ edit Open the config file in your default editor. │ -│ show Display the config file location and contents. │ -│ path Print the config file path (useful for scripting). │ -│ validate Validate the config file syntax and schema. │ -│ symlink Create a symlink from the default config location to a config file. │ -│ init-env Generate a .env file for Docker deployment. │ +│ init Create a new config file with documented example. │ +│ edit Open the config file in your default editor. │ +│ show Display the config file location and contents. │ +│ path Print the config file path (useful for scripting). │ +│ validate Validate the config file syntax and schema. │ +│ symlink Create a symlink from the default config location to a config file. │ +│ init-env Generate a .env file for Docker deployment. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ``` @@ -1056,9 +1056,9 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a │ --help -h Show this message and exit. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─ Commands ─────────────────────────────────────────────────────────────────────────────╮ -│ keygen Generate SSH key (does not distribute to hosts). │ -│ setup Generate SSH key and distribute to all configured hosts. │ -│ status Show SSH key status and host connectivity. │ +│ keygen Generate SSH key (does not distribute to hosts). │ +│ setup Generate SSH key and distribute to all configured hosts. │ +│ status Show SSH key status and host connectivity. │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ``` diff --git a/src/compose_farm/state.py b/src/compose_farm/state.py index 08b1224..766a8e5 100644 --- a/src/compose_farm/state.py +++ b/src/compose_farm/state.py @@ -64,8 +64,11 @@ def load_state(config: Config) -> dict[str, str | list[str]]: def _sorted_dict(d: dict[str, str | list[str]]) -> dict[str, str | list[str]]: - """Return a dictionary sorted by keys.""" - return dict(sorted(d.items(), key=lambda item: item[0])) + """Return a dictionary sorted by keys, with list values also sorted.""" + return { + k: sorted(v) if isinstance(v, list) else v + for k, v in sorted(d.items(), key=lambda item: item[0]) + } def save_state(config: Config, deployed: dict[str, str | list[str]]) -> None: diff --git a/tests/test_state.py b/tests/test_state.py index 46f8442..d2d4bdc 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -67,6 +67,16 @@ class TestSaveState: assert "plex: nas01" in content assert "jellyfin: nas02" in content + def test_save_state_sorts_host_lists(self, config: Config) -> None: + """Saves state with sorted host lists for consistent output.""" + # Pass hosts in unsorted order + save_state(config, {"glances": ["pc", "nas", "hp", "anton"]}) + + state_file = config.get_state_path() + content = state_file.read_text() + # Hosts should be sorted alphabetically + assert "- anton\n - hp\n - nas\n - pc" in content + class TestGetStackHost: """Tests for get_stack_host function."""