Skip to content
Open
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
75 changes: 74 additions & 1 deletion scripts/check-deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,79 @@ def run(script, *args, expected=0):
assert result.returncode == expected, result.stdout + result.stderr
return result

def snapshot(directory):
result = {}
for path in directory.rglob("*"):
if path.is_symlink():
value = ("link", str(path.readlink()))
elif path.is_file():
value = ("file", path.read_bytes())
else:
value = ("directory",)
result[str(path.relative_to(directory))] = value
return result

alias = root / "checkout alias"
alias.symlink_to(repo, target_is_directory=True)
shared_before = snapshot(repo)
for name, destination, shared in (
("shared-config", ".config", "home/.config"),
("shared-outside-home", ".config", "agent"),
("shared-headless", ".config/mise", "headless"),
("shared-units", ".config/systemd/user", "systemd/user"),
("shared-ssh", ".ssh", "ssh"),
("shared-backups", ".local/share/config-link-backups", "agent"),
("shared-backup-parent", ".local/share", "agent"),
):
isolated = root / name
isolated.mkdir()
(isolated / ".zshrc").write_text("original shell\n")
target = isolated / destination
target.parent.mkdir(parents=True, exist_ok=True)
target.symlink_to(repo / shared, target_is_directory=True)
before = snapshot(isolated)
safety_env = dict(env, CONFIG_LINK_HOME=str(isolated))
command = ["bash", str(alias / "scripts/link-home.sh"), "homelab-dev"]
for mode in ("--dry-run", "--check"):
subprocess.run(command + [mode], env=safety_env, capture_output=True, text=True)
assert snapshot(isolated) == before, name
assert snapshot(repo) == shared_before, name
result = subprocess.run(command, env=safety_env, capture_output=True, text=True)
assert result.returncode == 1, name + result.stdout + result.stderr
assert "Destination parent points into shared repo" in result.stderr, name
assert snapshot(isolated) == before, name
assert snapshot(repo) == shared_before, name

safety_env = dict(env, CONFIG_LINK_HOME=str(alias / "home"))
result = subprocess.run(
["bash", str(alias / "scripts/link-home.sh"), "homelab-dev"],
env=safety_env, capture_output=True, text=True,
)
assert result.returncode == 1, result.stdout + result.stderr
assert snapshot(repo) == shared_before

# Local aliases and relative paths remain supported, including backups.
custom = root / "custom home"
custom.mkdir()
local_config = root / "local config"
local_config.mkdir()
local_backups = root / "local backups"
local_backups.mkdir()
(custom / ".config").symlink_to(local_config, target_is_directory=True)
(custom / ".local/share").mkdir(parents=True)
(custom / ".local/share/config-link-backups").symlink_to(local_backups, target_is_directory=True)
(custom / ".zshrc").write_text("local shell\n")
custom_env = dict(env, CONFIG_LINK_HOME="missing/../custom home")
command = ["bash", str(alias / "scripts/link-home.sh"), "homelab-dev"]
for args in ([], ["--check"], []):
result = subprocess.run(command + args, cwd=root, env=custom_env, capture_output=True, text=True)
assert result.returncode == 0, result.stdout + result.stderr
assert (custom / ".zshrc").resolve() == repo / "home/.zshrc"
backups = list(local_backups.iterdir())
assert len(backups) == 1
assert (backups[0] / ".zshrc").read_text() == "local shell\n"
assert snapshot(repo) == shared_before

for machine in ("laptop", "desktop"):
home = root / f"{machine} home"
home.mkdir()
Expand Down Expand Up @@ -189,7 +262,7 @@ def run(script, *args, expected=0):
run("link-home.sh", "laptop", expected=1)
run("install.sh", "laptop", expected=1)
assert list(untouched.iterdir()) == []
print("PASS: workstation/headless/system profiles, preview/check, invalid input, backups, broken links, idempotency, missing sources, no service changes, package failures stop installation")
print("PASS: workstation/headless/system profiles, preview/check, shared-parent/backup safety, local aliases, invalid input, backups, broken links, idempotency, missing sources, no service changes, package failures stop installation")


if __name__ == "__main__":
Expand Down
36 changes: 27 additions & 9 deletions scripts/link-home.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
# Usage: bash scripts/link-home.sh [laptop|desktop|homelab-dev] [--dry-run|--check]
set -euo pipefail

repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
home_root="$repo_root/home"
target_home="${CONFIG_LINK_HOME:-$HOME}"
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
home_root="$(realpath -m -- "$repo_root/home")"
target_home="$(realpath -m -- "${CONFIG_LINK_HOME:-$HOME}")"
mode=install
machine=""
for arg in "$@"; do
Expand Down Expand Up @@ -98,6 +98,28 @@ done

backup_root=""
failures=0
ensure_local_destination() {
local parent
parent="$(realpath -m -- "$(dirname -- "$1")")"
case "$parent/" in
"$repo_root/"*|"$home_root/"*) echo "Destination parent points into shared repo: $1" >&2; exit 1 ;;
esac
}

# Reject shared parents and backup aliases before changing any existing links.
if [[ "$mode" == install ]]; then
for rel in "${links[@]}"; do ensure_local_destination "$target_home/$rel"; done
if [[ "$machine" == homelab-dev ]]; then
ensure_local_destination "$target_home/.zshenv"
ensure_local_destination "$target_home/.config/mise/config.toml"
else
ensure_local_destination "$target_home/.config/alacritty/machine.toml"
fi
ensure_local_destination "$target_home/.config/systemd/user/t3code.service.d/path.conf"
ensure_local_destination "$target_home/.ssh/config"
ensure_local_destination "$target_home/.local/share/config-link-backups/new-backup"
fi

link_file() {
local rel="$1" src="${2:-$home_root/$1}" dst="$target_home/$1"
if [[ -L "$dst" && "$(readlink -- "$dst")" == "$src" ]]; then
Expand All @@ -108,15 +130,11 @@ link_file() {
--check) echo "MISSING or different link: $rel"; failures=$((failures + 1)); return ;;
--dry-run) echo "LINK $dst -> $src (back up existing destination)"; return ;;
esac
# A parent link into home/ would write machine-local files into the shared repo.
local parent
parent="$(realpath -m -- "$(dirname -- "$dst")")"
case "$parent/" in
"$home_root/"*) echo "Destination parent points into shared home/: $dst" >&2; exit 1 ;;
esac
ensure_local_destination "$dst"
mkdir -p -- "$(dirname -- "$dst")"
if [[ -e "$dst" || -L "$dst" ]]; then
if [[ -z "$backup_root" ]]; then
ensure_local_destination "$target_home/.local/share/config-link-backups/new-backup"
mkdir -p -- "$target_home/.local/share/config-link-backups"
backup_root="$(mktemp -d "$target_home/.local/share/config-link-backups/$(date +%Y%m%d-%H%M%S)-XXXXXX")"
fi
Expand Down
Loading