feat(dotfiles): import env layer as stow-style packages with deploy.sh
Import Dankware's environment layer into the repo as home-relative package trees (niri, gtk-3/4, ghostty, alacritty, fish, zsh, bash, profile, systemd-user, dconf). scripts/deploy.sh implements stow-equivalent semantics (check|adopt|sync modes): identical files adopt to symlinks, new files link, divergent files quarantine-and-replace under .quarantine/<host>/<ts>/ (gitignored). zz-<hostname> override packages deploy last with override semantics. Secrets never enter git: GITEA_TOKEN moved from .zshrc into untracked ~/.config/environment.d/secrets.env (0600); tracked .zshrc sources it guarded. core.hooksPath armed by deploy.sh so the pre-commit secret gate is live on every machine that deploys.
This commit is contained in:
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh — stow-equivalent deployment of the dotfiles/ packages in this repo.
|
||||
#
|
||||
# Model: the repo checkout IS the deployment. Packages are home-relative trees
|
||||
# under dotfiles/<pkg>/; deploying replaces live files with symlinks into the
|
||||
# repo so `git pull` + `deploy.sh` is the whole propagation story.
|
||||
#
|
||||
# Modes:
|
||||
# deploy.sh sync (default): adopt identicals, link new, quarantine-and-replace divergents
|
||||
# deploy.sh --check dry-run report only; writes nothing
|
||||
# deploy.sh --adopt like sync but divergents are reported and SKIPPED, not quarantined
|
||||
#
|
||||
# Host divergence: a package named zz-$(hostname) is stowed LAST with override
|
||||
# semantics (its files replace symlinks from base packages).
|
||||
#
|
||||
# Special cases:
|
||||
# - dconf package: dump.txt is loaded via `dconf load /` instead of symlinked.
|
||||
# - systemd-user: after linking, daemon-reload + enable --now units whose
|
||||
# ExecStart binary resolves on THIS machine (silent no-op elsewhere).
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(git -C "$(dirname "$(realpath "$0")")" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
REPO="${DEPLOY_REPO:-$REPO}"
|
||||
PKGS_DIR="$REPO/dotfiles"
|
||||
HOST="${DEPLOY_HOST:-$(hostname)}"
|
||||
QUAR="$REPO/.quarantine"
|
||||
|
||||
MODE="sync"
|
||||
case "${1:-}" in
|
||||
--check) MODE="check" ;;
|
||||
--adopt) MODE="adopt" ;;
|
||||
""|sync) MODE="sync" ;;
|
||||
*) echo "usage: deploy.sh [--check|--adopt|sync]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
[ -d "$PKGS_DIR" ] || { echo "deploy.sh: no dotfiles/ under $REPO" >&2; exit 1; }
|
||||
|
||||
# packages = all dirs in dotfiles/, override package last
|
||||
mapfile -t PKGS < <(find "$PKGS_DIR" -maxdepth 1 -mindepth 1 -type d ! -name "zz-*" | sort)
|
||||
OVR="$PKGS_DIR/zz-$HOST"
|
||||
[ -d "$OVR" ] && PKGS+=("$OVR")
|
||||
|
||||
actions=0; quarantined=0; skipped=0
|
||||
|
||||
deploy_file() { # $1 = repo file path, $2 = home-relative target path
|
||||
local src="$1" tgt="$HOME/$2"
|
||||
if [ -L "$tgt" ]; then
|
||||
if [ "$(realpath "$tgt")" = "$(realpath "$src")" ]; then return 0; fi # already deployed
|
||||
case "$MODE" in
|
||||
check) echo "DRIFT $2 (symlink points elsewhere: $(realpath "$tgt"))" ;;
|
||||
adopt) echo "SKIP $2 (foreign symlink, --adopt does not overwrite)" ; skipped=$((skipped+1)) ;;
|
||||
sync)
|
||||
ts="$(date +%Y%m%d-%H%M%S)-$$"
|
||||
dest="$QUAR/$HOST/$ts/$2"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
mv -- "$tgt" "$dest"
|
||||
ln -sfn "$(realpath "$src")" "$tgt"
|
||||
actions=$((actions+1)); quarantined=$((quarantined+1))
|
||||
echo "QUAR $2 -> .quarantine/$HOST/$ts/$2"
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
if [ -e "$tgt" ]; then
|
||||
if cmp -s "$src" "$tgt"; then
|
||||
if [ "$MODE" = "check" ]; then echo "ADOPT $2 (identical, will symlink)"; return 0; fi
|
||||
ln -sfn "$(realpath "$src")" "$tgt"; actions=$((actions+1)); return 0
|
||||
fi
|
||||
case "$MODE" in
|
||||
check) echo "DRIFT $2 (differs from repo; sync would quarantine)" ;;
|
||||
adopt) echo "SKIP $2 (divergent, --adopt does not overwrite)" ; skipped=$((skipped+1)) ;;
|
||||
sync)
|
||||
ts="$(date +%Y%m%d-%H%M%S)-$$"
|
||||
dest="$QUAR/$HOST/$ts/$2"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
mv -- "$tgt" "$dest"
|
||||
ln -sfn "$(realpath "$src")" "$tgt"
|
||||
actions=$((actions+1)); quarantined=$((quarantined+1))
|
||||
echo "QUAR $2 -> .quarantine/$HOST/$ts/$2"
|
||||
;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
if [ "$MODE" = "check" ]; then echo "LINK $2 (missing, will symlink)"; return 0; fi
|
||||
mkdir -p "$(dirname "$tgt")"
|
||||
ln -sfn "$(realpath "$src")" "$tgt"; actions=$((actions+1))
|
||||
}
|
||||
|
||||
for pkg in "${PKGS[@]}"; do
|
||||
pkgname="$(basename "$pkg")"
|
||||
# dconf package: load dump instead of symlinking
|
||||
if [ "$pkgname" = "dconf" ] && [ -f "$pkg/dump.txt" ]; then
|
||||
if [ "$MODE" != "check" ]; then
|
||||
if command -v dconf >/dev/null; then dconf load / < "$pkg/dump.txt" && echo "LOAD $pkgname/dump.txt -> dconf"; fi
|
||||
else
|
||||
echo "LOAD $pkgname/dump.txt (dconf load / would run on sync)"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
while IFS= read -r -d '' f; do
|
||||
deploy_file "$f" "${f#"$pkg"/}"
|
||||
done < <(find "$pkg" -type f ! -name 'dump.txt' -print0)
|
||||
done
|
||||
|
||||
if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user" ]; then
|
||||
systemctl --user daemon-reload || true
|
||||
for unit in "$PKGS_DIR"/systemd-user/.config/systemd/user/*.service; do
|
||||
[ -e "$unit" ] || continue
|
||||
bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$unit" | head -1)"
|
||||
if [ -n "$bin" ] && command -v "$bin" >/dev/null 2>&1; then
|
||||
systemctl --user enable --now "$(basename "$unit")" || true
|
||||
echo "ENABLE $(basename "$unit") (ExecStart resolves here)"
|
||||
else
|
||||
echo "SKIP $(basename "$unit") (ExecStart '$bin' absent on this host)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# keep the secret gate armed on every machine that deploys
|
||||
git -C "$REPO" config core.hooksPath .githooks
|
||||
|
||||
echo "deploy.sh [$MODE] host=$HOST repo=$REPO — actions=$actions quarantined=$quarantined skipped=$skipped"
|
||||
Reference in New Issue
Block a user