Skip to content

Commit e1cf8d8

Browse files
Add CI (#5)
- Add `~/.bin/dotfiles-test` test command - To avoid polluting the home directory in the repo's designed bare checkout, this PR does not define a package.json or lockfile - Run safely in a disposable, non-bare clone, by default - Flags: - `--here` to run in the current checkout (so a little less safely) - `--yes` to do this without confirmation - `--fix` to apply available fixes - Cover: - Markdown formatting with Prettier - Lua formatting and lint for the Neovim config - Zsh syntax checks - Headless Neovim startup smoke test - Apply fixes - Configure GitHub Actions - Remove unused Remark - Prettier does most of what I stylistic enforcement I originally needed Remark for
1 parent 4d64c30 commit e1cf8d8

7 files changed

Lines changed: 316 additions & 26 deletions

File tree

.bin/dotfiles-test

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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

.docs/GIT.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ Get latest
6464
</td>
6565
<td>
6666

67-
- ⚠️ This is destructive, for a couple reasons.
67+
- ⚠️ This is destructive, for a couple reasons.
6868
1. It defaults to rebasing. I think
6969
[a clean history is best. However, the backspace key does carry risk.](https://blog.izs.me/2012/12/git-rebase/)
7070
To get out of trouble, `git rebase --abort` or reference `git reflog`.
7171
1. It cleans up deleted branches, e.g. a merged upstream pull request. A
7272
clean local repo is best. However, if you have local, unpushed commits on
7373
that _other_ branch, the command could drop your changes.
74-
- If you want to be safer, drop down to `git fetch` and `git merge`, which
75-
will modify only your current branch, and without rewriting history.
74+
- If you want to be safer, drop down to `git fetch` and `git merge`, which will
75+
modify only your current branch, and without rewriting history.
7676

7777
</td>
7878
</tr>

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- master
8+
9+
jobs:
10+
ci:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: "22"
20+
21+
- name: Install system dependencies
22+
run: |
23+
sudo apt-get update
24+
sudo apt-get install --yes neovim luarocks zsh
25+
26+
- uses: taiki-e/install-action@v2
27+
with:
28+
tool: stylua
29+
30+
- name: Install Luacheck
31+
run: sudo luarocks install luacheck
32+
33+
- name: Run dotfiles tests
34+
env:
35+
HOME: ${{ github.workspace }}
36+
XDG_CACHE_HOME: ${{ runner.temp }}/xdg-cache
37+
XDG_DATA_HOME: ${{ runner.temp }}/xdg-data
38+
XDG_STATE_HOME: ${{ runner.temp }}/xdg-state
39+
run: ./.bin/dotfiles-test --here --yes

.remarkrc

Lines changed: 0 additions & 7 deletions
This file was deleted.

.vim/.luacheckrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
globals = {
2+
"Snacks",
23
"vim",
34
}

.vim/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ require("lazy").setup({
2525
})
2626

2727
-- Load remaining VimScript config
28-
vim.cmd('source ~/.vim/vimrc')
28+
vim.cmd("source ~/.vim/vimrc")

README.md

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,6 @@ dotfiles config --local status.showUntrackedFiles no
1919
dotfiles checkout
2020
```
2121

22-
### Machine-Local Commit Identity
23-
24-
Set in `.mise.local.toml`:
25-
26-
```toml
27-
# $HOME/.mise.local.toml
28-
[env]
29-
GIT_AUTHOR_EMAIL = "you@company.com"
30-
GIT_COMMITTER_EMAIL = "you@company.com"
31-
JJ_EMAIL = "you@company.com"
32-
```
33-
34-
Read more about per-machine settings in
35-
[.docs/MACHINE_LOCAL_CONFIGURATION.md](./.docs/MACHINE_LOCAL_CONFIGURATION.md).
36-
3722
### If zsh is not the default shell
3823

3924
```zsh
@@ -64,6 +49,31 @@ lifecycle for you][the best way to store your dotfiles]. Same interface as
6449
`git`. No extra, bespoke tool. The repo layout stays in sync with how the files
6550
are used.
6651

52+
### Machine-Local Commit Identity
53+
54+
Set in `.mise.local.toml`:
55+
56+
```toml
57+
# $HOME/.mise.local.toml
58+
[env]
59+
GIT_AUTHOR_EMAIL = "you@company.com"
60+
GIT_COMMITTER_EMAIL = "you@company.com"
61+
JJ_EMAIL = "you@company.com"
62+
```
63+
64+
Read more about per-machine settings in
65+
[.docs/MACHINE_LOCAL_CONFIGURATION.md](./.docs/MACHINE_LOCAL_CONFIGURATION.md).
66+
67+
### Test
68+
69+
The test command is globally added to `$PATH`: `$HOME/.bin/dotfiles-test`.
70+
71+
```zsh
72+
dotfiles-test # Run tests in a disposable normal clone
73+
dotfiles-test --here # Run tests in the current checkout, with confirmation
74+
dotfiles-test --here --fix # Apply available fixes in the current checkout, with confirmation
75+
```
76+
6777
### Advanced Usage
6878

6979
See [the docs folder](./.docs/).

0 commit comments

Comments
 (0)