|
| 1 | +#!/bin/zsh |
| 2 | + |
| 3 | +# Test this dotfiles repo without requiring project-local package manifests or a |
| 4 | +# non-bare checkout in the real home directory. |
| 5 | +# |
| 6 | +# Default mode runs in a disposable normal clone so package-manager state, |
| 7 | +# Neovim plugin bootstrap, and other temporary files stay out of the author's |
| 8 | +# actual `$HOME`. `--here` runs the same checks in the current checkout, which |
| 9 | +# is useful for CI and for explicit local debugging. |
| 10 | +# |
| 11 | +# Coverage: |
| 12 | +# - Markdown formatting with Prettier |
| 13 | +# - Lua formatting and lint for the tracked Neovim config |
| 14 | +# - Zsh syntax for the shell entrypoints |
| 15 | +# - A headless Neovim startup smoke test with isolated XDG state |
| 16 | +# |
| 17 | +# The script installs the Node-backed Markdown formatter into a temporary |
| 18 | +# directory on each run instead of relying on a tracked `package.json` or |
| 19 | +# `node_modules`. |
| 20 | + |
| 21 | +set -euo pipefail |
| 22 | + |
| 23 | +script_dir=${0:A:h} |
| 24 | +repo_root=${script_dir:h} |
| 25 | +run_here=0 |
| 26 | +assume_yes=0 |
| 27 | +apply_fixes=0 |
| 28 | +keep_clone=${DOTFILES_CI_KEEP_CLONE:-0} |
| 29 | + |
| 30 | +usage() { |
| 31 | + cat <<'EOF' |
| 32 | +Usage: |
| 33 | + dotfiles-test [--here] [--fix] [--yes] |
| 34 | +
|
| 35 | +Without arguments, run the checks in a disposable normal clone. |
| 36 | +
|
| 37 | +Options: |
| 38 | + --here Run in the current checkout instead of a temp clone. |
| 39 | + --fix Apply fixes where supported before running the remaining checks. |
| 40 | + -y, --yes Skip the confirmation prompt for --here. |
| 41 | + -h, --help Show this help. |
| 42 | +EOF |
| 43 | +} |
| 44 | + |
| 45 | +while (( $# > 0 )); do |
| 46 | + case "$1" in |
| 47 | + --here) |
| 48 | + run_here=1 |
| 49 | + ;; |
| 50 | + --fix) |
| 51 | + apply_fixes=1 |
| 52 | + ;; |
| 53 | + -y|--yes) |
| 54 | + assume_yes=1 |
| 55 | + ;; |
| 56 | + -h|--help) |
| 57 | + usage |
| 58 | + exit 0 |
| 59 | + ;; |
| 60 | + *) |
| 61 | + print -u2 "dotfiles-test: unknown argument: $1" |
| 62 | + usage >&2 |
| 63 | + exit 2 |
| 64 | + ;; |
| 65 | + esac |
| 66 | + shift |
| 67 | +done |
| 68 | + |
| 69 | +if [[ -d "$repo_root/.dotfiles" ]]; then |
| 70 | + git_cmd=(git --git-dir="$repo_root/.dotfiles" --work-tree="$repo_root") |
| 71 | + clone_source=$repo_root/.dotfiles |
| 72 | +elif git -C "$repo_root" rev-parse --show-toplevel >/dev/null 2>&1; then |
| 73 | + git_cmd=(git -C "$repo_root") |
| 74 | + clone_source=$repo_root |
| 75 | +else |
| 76 | + print -u2 "dotfiles-test: could not find the dotfiles repository from $repo_root" |
| 77 | + exit 1 |
| 78 | +fi |
| 79 | + |
| 80 | +cd "$repo_root" |
| 81 | + |
| 82 | +require_cmd() { |
| 83 | + local cmd=$1 |
| 84 | + local help=${2:-} |
| 85 | + |
| 86 | + if command -v "$cmd" >/dev/null 2>&1; then |
| 87 | + return 0 |
| 88 | + fi |
| 89 | + |
| 90 | + print -u2 "dotfiles-test: missing required command: $cmd" |
| 91 | + if [[ -n "$help" ]]; then |
| 92 | + print -u2 "dotfiles-test: $help" |
| 93 | + fi |
| 94 | + exit 127 |
| 95 | +} |
| 96 | + |
| 97 | +run_checks_here() { |
| 98 | + local work_root=$1 |
| 99 | + |
| 100 | + cd "$work_root" |
| 101 | + |
| 102 | + require_cmd git |
| 103 | + require_cmd node "Install Node 22 via mise, for example: mise use -g node@22" |
| 104 | + require_cmd npm "Install Node 22 via mise, for example: mise use -g node@22" |
| 105 | + require_cmd nvim "Install Neovim, for example: brew install neovim" |
| 106 | + require_cmd stylua "Install Stylua, for example: brew install stylua" |
| 107 | + require_cmd luacheck "Install Luacheck, for example: brew install luacheck" |
| 108 | + require_cmd zsh "Install zsh, for example: brew install zsh" |
| 109 | + |
| 110 | + local tmp_root |
| 111 | + tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-test.XXXXXX") |
| 112 | + |
| 113 | + local tracked_md=() |
| 114 | + local markdown_path |
| 115 | + for markdown_path in "${(@0)$("${git_cmd[@]}" ls-files -z -- '*.md')}"; do |
| 116 | + if [[ -n "$markdown_path" && ! -L "$work_root/$markdown_path" ]]; then |
| 117 | + tracked_md+=("$markdown_path") |
| 118 | + fi |
| 119 | + done |
| 120 | + |
| 121 | + if (( ${#tracked_md[@]} > 0 )); then |
| 122 | + npm install \ |
| 123 | + --prefix "$tmp_root/npm-tools" \ |
| 124 | + --no-package-lock \ |
| 125 | + --no-save \ |
| 126 | + prettier@3 >/dev/null |
| 127 | + |
| 128 | + if (( apply_fixes == 1 )); then |
| 129 | + "$tmp_root/npm-tools/node_modules/.bin/prettier" --list-different --write -- "${tracked_md[@]}" |
| 130 | + else |
| 131 | + "$tmp_root/npm-tools/node_modules/.bin/prettier" --list-different -- "${tracked_md[@]}" |
| 132 | + fi |
| 133 | + fi |
| 134 | + |
| 135 | + if (( apply_fixes == 1 )); then |
| 136 | + stylua .vim/init.lua .vim/lua |
| 137 | + else |
| 138 | + stylua --check .vim/init.lua .vim/lua |
| 139 | + fi |
| 140 | + luacheck --config .vim/.luacheckrc .vim/init.lua .vim/lua |
| 141 | + zsh -n .zshenv .zshrc |
| 142 | + |
| 143 | + mkdir -p \ |
| 144 | + "$tmp_root/xdg-cache" \ |
| 145 | + "$tmp_root/xdg-data" \ |
| 146 | + "$tmp_root/xdg-state" |
| 147 | + |
| 148 | + local nvim_log="$tmp_root/nvim.log" |
| 149 | + if ! env \ |
| 150 | + HOME="$work_root" \ |
| 151 | + XDG_CACHE_HOME="$tmp_root/xdg-cache" \ |
| 152 | + XDG_DATA_HOME="$tmp_root/xdg-data" \ |
| 153 | + XDG_STATE_HOME="$tmp_root/xdg-state" \ |
| 154 | + nvim --headless "+Lazy! restore" "+qa" >"$nvim_log" 2>&1; then |
| 155 | + cat "$nvim_log" |
| 156 | + rm -rf "$tmp_root" |
| 157 | + exit 1 |
| 158 | + fi |
| 159 | + |
| 160 | + rm -rf "$tmp_root" |
| 161 | +} |
| 162 | + |
| 163 | +run_checks_in_temp_clone() { |
| 164 | + require_cmd git |
| 165 | + require_cmd rsync "Install rsync, for example: brew install rsync" |
| 166 | + |
| 167 | + local tmp_root |
| 168 | + tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-test-clone.XXXXXX") |
| 169 | + |
| 170 | + cleanup_clone() { |
| 171 | + if [[ "$keep_clone" == "1" ]]; then |
| 172 | + print "dotfiles-test: kept temp clone at $tmp_root/repo" |
| 173 | + return 0 |
| 174 | + fi |
| 175 | + |
| 176 | + rm -rf "$tmp_root" |
| 177 | + } |
| 178 | + |
| 179 | + git clone "$clone_source" "$tmp_root/repo" >/dev/null |
| 180 | + |
| 181 | + local overlay_files=("${(@0)$("${git_cmd[@]}" ls-files -z)}") |
| 182 | + overlay_files=("${(@)overlay_files:#}") |
| 183 | + overlay_files=("${(@)overlay_files:#(#m)(?)}") |
| 184 | + local extra_dir |
| 185 | + for extra_dir in .bin .github; do |
| 186 | + if [[ ! -d "$repo_root/$extra_dir" ]]; then |
| 187 | + continue |
| 188 | + fi |
| 189 | + |
| 190 | + local extra_files=("${(@0)$("${git_cmd[@]}" ls-files -z --others --exclude-standard -- "$extra_dir")}") |
| 191 | + overlay_files+=("${extra_files[@]}") |
| 192 | + done |
| 193 | + |
| 194 | + local existing_overlay_files=() |
| 195 | + local overlay_file |
| 196 | + for overlay_file in "${overlay_files[@]}"; do |
| 197 | + if [[ -e "$repo_root/$overlay_file" || -L "$repo_root/$overlay_file" ]]; then |
| 198 | + existing_overlay_files+=("$overlay_file") |
| 199 | + fi |
| 200 | + done |
| 201 | + |
| 202 | + if (( ${#existing_overlay_files[@]} > 0 )); then |
| 203 | + rsync -a --files-from=<(printf '%s\0' "${existing_overlay_files[@]}") --from0 "$repo_root"/ "$tmp_root/repo"/ |
| 204 | + fi |
| 205 | + |
| 206 | + local deleted_files=("${(@0)$("${git_cmd[@]}" ls-files -z --deleted)}") |
| 207 | + if (( ${#deleted_files[@]} > 0 )); then |
| 208 | + (cd "$tmp_root/repo" && rm -f -- "${deleted_files[@]}") |
| 209 | + fi |
| 210 | + |
| 211 | + local nested_args=(--here --yes) |
| 212 | + if (( apply_fixes == 1 )); then |
| 213 | + nested_args+=(--fix) |
| 214 | + fi |
| 215 | + |
| 216 | + (cd "$tmp_root/repo" && ./.bin/dotfiles-test "${nested_args[@]}") |
| 217 | + cleanup_clone |
| 218 | +} |
| 219 | + |
| 220 | +confirm_here_run() { |
| 221 | + if (( assume_yes == 1 )); then |
| 222 | + return 0 |
| 223 | + fi |
| 224 | + |
| 225 | + local prompt="dotfiles-test: run checks in the current checkout and allow temporary files under this checkout? [y/N] " |
| 226 | + local reply |
| 227 | + |
| 228 | + if [[ -t 0 ]]; then |
| 229 | + read -r "?$prompt" reply |
| 230 | + else |
| 231 | + print -u2 "$prompt" |
| 232 | + read -r reply || return 1 |
| 233 | + fi |
| 234 | + |
| 235 | + [[ "$reply" == [Yy] || "$reply" == [Yy][Ee][Ss] ]] |
| 236 | +} |
| 237 | + |
| 238 | +if (( run_here == 1 )); then |
| 239 | + if ! confirm_here_run; then |
| 240 | + print -u2 "dotfiles-test: aborted" |
| 241 | + exit 1 |
| 242 | + fi |
| 243 | + |
| 244 | + run_checks_here "$repo_root" |
| 245 | +else |
| 246 | + run_checks_in_temp_clone |
| 247 | +fi |
0 commit comments