From 537957206d1085caa24c6cbac7f327d74152029b Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 14 Apr 2026 23:12:50 -0700 Subject: [PATCH 1/7] Add CI, via GHA --- .bin/dotfiles-test | 236 +++++++++++++++++++++++++++++++++++++++ .docs/GIT.md | 6 +- .github/workflows/ci.yml | 35 ++++++ .vim/.luacheckrc | 1 + README.md | 8 ++ 5 files changed, 283 insertions(+), 3 deletions(-) create mode 100755 .bin/dotfiles-test create mode 100644 .github/workflows/ci.yml diff --git a/.bin/dotfiles-test b/.bin/dotfiles-test new file mode 100755 index 0000000..8b3e53b --- /dev/null +++ b/.bin/dotfiles-test @@ -0,0 +1,236 @@ +#!/bin/zsh + +# Test this dotfiles repo without requiring project-local package manifests or a +# non-bare checkout in the real home directory. +# +# Default mode runs in a disposable normal clone so package-manager state, +# Neovim plugin bootstrap, and other temporary files stay out of the author's +# actual `$HOME`. `--here` runs the same checks in the current checkout, which +# is useful for CI and for explicit local debugging. +# +# Coverage: +# - Markdown formatting with Prettier +# - Markdown lint with remark (advisory warnings today) +# - Lua formatting and lint for the tracked Neovim config +# - Zsh syntax for the shell entrypoints +# - A headless Neovim startup smoke test with isolated XDG state +# +# The script installs Node-backed Markdown tools into a temporary directory on +# each run instead of relying on a tracked `package.json` or `node_modules`. + +set -euo pipefail + +script_dir=${0:A:h} +repo_root=${script_dir:h} +run_here=0 +assume_yes=0 +keep_clone=${DOTFILES_CI_KEEP_CLONE:-0} + +usage() { + cat <<'EOF' +Usage: + dotfiles-test [--here] [--yes] + +Without arguments, run the checks in a disposable normal clone. + +Options: + --here Run in the current checkout instead of a temp clone. + -y, --yes Skip the confirmation prompt for --here. + -h, --help Show this help. +EOF +} + +while (( $# > 0 )); do + case "$1" in + --here) + run_here=1 + ;; + -y|--yes) + assume_yes=1 + ;; + -h|--help) + usage + exit 0 + ;; + *) + print -u2 "dotfiles-test: unknown argument: $1" + usage >&2 + exit 2 + ;; + esac + shift +done + +if [[ -d "$repo_root/.dotfiles" ]]; then + git_cmd=(git --git-dir="$repo_root/.dotfiles" --work-tree="$repo_root") + clone_source=$repo_root/.dotfiles +elif git -C "$repo_root" rev-parse --show-toplevel >/dev/null 2>&1; then + git_cmd=(git -C "$repo_root") + clone_source=$repo_root +else + print -u2 "dotfiles-test: could not find the dotfiles repository from $repo_root" + exit 1 +fi + +cd "$repo_root" + +require_cmd() { + local cmd=$1 + local help=${2:-} + + if command -v "$cmd" >/dev/null 2>&1; then + return 0 + fi + + print -u2 "dotfiles-test: missing required command: $cmd" + if [[ -n "$help" ]]; then + print -u2 "dotfiles-test: $help" + fi + exit 127 +} + +run_checks_here() { + local work_root=$1 + + cd "$work_root" + + require_cmd git + require_cmd node "Install Node 22 via mise, for example: mise use -g node@22" + require_cmd npm "Install Node 22 via mise, for example: mise use -g node@22" + require_cmd nvim "Install Neovim, for example: brew install neovim" + require_cmd stylua "Install Stylua, for example: brew install stylua" + require_cmd luacheck "Install Luacheck, for example: brew install luacheck" + require_cmd zsh "Install zsh, for example: brew install zsh" + + local tmp_root + tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-test.XXXXXX") + + local tracked_md=() + local markdown_path + for markdown_path in "${(@0)$("${git_cmd[@]}" ls-files -z -- '*.md')}"; do + if [[ -n "$markdown_path" && ! -L "$work_root/$markdown_path" ]]; then + tracked_md+=("$markdown_path") + fi + done + + if (( ${#tracked_md[@]} > 0 )); then + npm install \ + --prefix "$tmp_root/npm-tools" \ + --no-package-lock \ + --no-save \ + prettier@3 \ + remark-cli@12 \ + remark-preset-lint-consistent \ + remark-preset-lint-markdown-style-guide \ + remark-preset-lint-recommended >/dev/null + + "$tmp_root/npm-tools/node_modules/.bin/prettier" --check -- "${tracked_md[@]}" + ( + mkdir -p "$tmp_root/remark-worktree" + cp "$work_root/.remarkrc" "$tmp_root/remark-worktree/.remarkrc" + ln -s "$tmp_root/npm-tools/node_modules" "$tmp_root/remark-worktree/node_modules" + + for markdown_path in "${tracked_md[@]}"; do + mkdir -p "$tmp_root/remark-worktree/${markdown_path:h}" + cp "$work_root/$markdown_path" "$tmp_root/remark-worktree/$markdown_path" + done + + cd "$tmp_root/remark-worktree" + "$tmp_root/npm-tools/node_modules/.bin/remark" --quiet -- "${tracked_md[@]}" + ) + fi + + stylua --check .vim/init.lua .vim/lua + luacheck --config .vim/.luacheckrc .vim/init.lua .vim/lua + zsh -n .zshenv .zshrc + + mkdir -p \ + "$tmp_root/xdg-cache" \ + "$tmp_root/xdg-data" \ + "$tmp_root/xdg-state" + + local nvim_log="$tmp_root/nvim.log" + if ! env \ + HOME="$work_root" \ + XDG_CACHE_HOME="$tmp_root/xdg-cache" \ + XDG_DATA_HOME="$tmp_root/xdg-data" \ + XDG_STATE_HOME="$tmp_root/xdg-state" \ + nvim --headless "+Lazy! restore" "+qa" >"$nvim_log" 2>&1; then + cat "$nvim_log" + rm -rf "$tmp_root" + exit 1 + fi + + rm -rf "$tmp_root" +} + +run_checks_in_temp_clone() { + require_cmd git + require_cmd rsync "Install rsync, for example: brew install rsync" + + local tmp_root + tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/dotfiles-test-clone.XXXXXX") + + cleanup_clone() { + if [[ "$keep_clone" == "1" ]]; then + print "dotfiles-test: kept temp clone at $tmp_root/repo" + return 0 + fi + + rm -rf "$tmp_root" + } + + git clone "$clone_source" "$tmp_root/repo" >/dev/null + + local overlay_files=("${(@0)$("${git_cmd[@]}" ls-files -z)}") + local extra_dir + for extra_dir in .bin .github; do + if [[ ! -d "$repo_root/$extra_dir" ]]; then + continue + fi + + local extra_files=("${(@0)$("${git_cmd[@]}" ls-files -z --others --exclude-standard -- "$extra_dir")}") + overlay_files+=("${extra_files[@]}") + done + + if (( ${#overlay_files[@]} > 0 )); then + rsync -a --files-from=<(printf '%s\0' "${overlay_files[@]}") --from0 "$repo_root"/ "$tmp_root/repo"/ + fi + + local deleted_files=("${(@0)$("${git_cmd[@]}" ls-files -z --deleted)}") + if (( ${#deleted_files[@]} > 0 )); then + (cd "$tmp_root/repo" && rm -f -- "${deleted_files[@]}") + fi + + (cd "$tmp_root/repo" && ./.bin/dotfiles-test --here --yes) + cleanup_clone +} + +confirm_here_run() { + if (( assume_yes == 1 )); then + return 0 + fi + + local prompt="dotfiles-test: run checks in the current checkout and allow temporary files under this checkout? [y/N] " + local reply + + if [[ -t 0 ]]; then + read -r "?$prompt" reply + else + print -u2 "$prompt" + read -r reply || return 1 + fi + + [[ "$reply" == [Yy] || "$reply" == [Yy][Ee][Ss] ]] +} + +if (( run_here == 1 )); then + if ! confirm_here_run; then + print -u2 "dotfiles-test: aborted" + exit 1 + fi + + run_checks_here "$repo_root" +else + run_checks_in_temp_clone +fi diff --git a/.docs/GIT.md b/.docs/GIT.md index 41c543b..7f9e5ef 100644 --- a/.docs/GIT.md +++ b/.docs/GIT.md @@ -64,15 +64,15 @@ Get latest -- ⚠️ This is destructive, for a couple reasons. +- ⚠️ This is destructive, for a couple reasons. 1. It defaults to rebasing. I think [a clean history is best. However, the backspace key does carry risk.](https://blog.izs.me/2012/12/git-rebase/) To get out of trouble, `git rebase --abort` or reference `git reflog`. 1. It cleans up deleted branches, e.g. a merged upstream pull request. A clean local repo is best. However, if you have local, unpushed commits on that _other_ branch, the command could drop your changes. -- If you want to be safer, drop down to `git fetch` and `git merge`, which - will modify only your current branch, and without rewriting history. +- If you want to be safer, drop down to `git fetch` and `git merge`, which will + modify only your current branch, and without rewriting history. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..786d482 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + pull_request: + push: + +jobs: + ci: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install --yes neovim luarocks zsh + + - uses: taiki-e/install-action@stylua + + - name: Install Luacheck + run: sudo luarocks install luacheck + + - name: Run dotfiles tests + env: + HOME: ${{ github.workspace }} + XDG_CACHE_HOME: ${{ runner.temp }}/xdg-cache + XDG_DATA_HOME: ${{ runner.temp }}/xdg-data + XDG_STATE_HOME: ${{ runner.temp }}/xdg-state + run: ./.bin/dotfiles-test --here --yes diff --git a/.vim/.luacheckrc b/.vim/.luacheckrc index 3a09011..dbd5104 100644 --- a/.vim/.luacheckrc +++ b/.vim/.luacheckrc @@ -1,3 +1,4 @@ globals = { + "Snacks", "vim", } diff --git a/README.md b/README.md index 87966c8..e425def 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,14 @@ chsh -s zsh dotfiles pull ``` +## Test + +```zsh +dotfiles-test # Run tests in a disposable normal clone +dotfiles-test --here # Run tests in the current checkout, with confirmation +dotfiles-test --here --yes # Run tests in the current checkout, without confirmation +``` + ## Usage The dotfiles are now in your home folder, in the conventional location. Your From 7cd1869b9c7ece5373f708d78034ab02bb6f3852 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 14 Apr 2026 23:40:39 -0700 Subject: [PATCH 2/7] Remove Remark support --- .bin/dotfiles-test | 39 ++++++++++++++++----------------------- .remarkrc | 7 ------- 2 files changed, 16 insertions(+), 30 deletions(-) delete mode 100644 .remarkrc diff --git a/.bin/dotfiles-test b/.bin/dotfiles-test index 8b3e53b..b0f2b35 100755 --- a/.bin/dotfiles-test +++ b/.bin/dotfiles-test @@ -10,13 +10,13 @@ # # Coverage: # - Markdown formatting with Prettier -# - Markdown lint with remark (advisory warnings today) # - Lua formatting and lint for the tracked Neovim config # - Zsh syntax for the shell entrypoints # - A headless Neovim startup smoke test with isolated XDG state # -# The script installs Node-backed Markdown tools into a temporary directory on -# each run instead of relying on a tracked `package.json` or `node_modules`. +# The script installs the Node-backed Markdown formatter into a temporary +# directory on each run instead of relying on a tracked `package.json` or +# `node_modules`. set -euo pipefail @@ -118,26 +118,9 @@ run_checks_here() { --prefix "$tmp_root/npm-tools" \ --no-package-lock \ --no-save \ - prettier@3 \ - remark-cli@12 \ - remark-preset-lint-consistent \ - remark-preset-lint-markdown-style-guide \ - remark-preset-lint-recommended >/dev/null + prettier@3 >/dev/null "$tmp_root/npm-tools/node_modules/.bin/prettier" --check -- "${tracked_md[@]}" - ( - mkdir -p "$tmp_root/remark-worktree" - cp "$work_root/.remarkrc" "$tmp_root/remark-worktree/.remarkrc" - ln -s "$tmp_root/npm-tools/node_modules" "$tmp_root/remark-worktree/node_modules" - - for markdown_path in "${tracked_md[@]}"; do - mkdir -p "$tmp_root/remark-worktree/${markdown_path:h}" - cp "$work_root/$markdown_path" "$tmp_root/remark-worktree/$markdown_path" - done - - cd "$tmp_root/remark-worktree" - "$tmp_root/npm-tools/node_modules/.bin/remark" --quiet -- "${tracked_md[@]}" - ) fi stylua --check .vim/init.lua .vim/lua @@ -183,6 +166,8 @@ run_checks_in_temp_clone() { git clone "$clone_source" "$tmp_root/repo" >/dev/null local overlay_files=("${(@0)$("${git_cmd[@]}" ls-files -z)}") + overlay_files=("${(@)overlay_files:#}") + overlay_files=("${(@)overlay_files:#(#m)(?)}") local extra_dir for extra_dir in .bin .github; do if [[ ! -d "$repo_root/$extra_dir" ]]; then @@ -193,8 +178,16 @@ run_checks_in_temp_clone() { overlay_files+=("${extra_files[@]}") done - if (( ${#overlay_files[@]} > 0 )); then - rsync -a --files-from=<(printf '%s\0' "${overlay_files[@]}") --from0 "$repo_root"/ "$tmp_root/repo"/ + local existing_overlay_files=() + local overlay_file + for overlay_file in "${overlay_files[@]}"; do + if [[ -e "$repo_root/$overlay_file" || -L "$repo_root/$overlay_file" ]]; then + existing_overlay_files+=("$overlay_file") + fi + done + + if (( ${#existing_overlay_files[@]} > 0 )); then + rsync -a --files-from=<(printf '%s\0' "${existing_overlay_files[@]}") --from0 "$repo_root"/ "$tmp_root/repo"/ fi local deleted_files=("${(@0)$("${git_cmd[@]}" ls-files -z --deleted)}") diff --git a/.remarkrc b/.remarkrc deleted file mode 100644 index c938caa..0000000 --- a/.remarkrc +++ /dev/null @@ -1,7 +0,0 @@ -{ - "plugins": [ - "remark-preset-lint-consistent", - "remark-preset-lint-markdown-style-guide", - "remark-preset-lint-recommended" - ] -} From a05459c696e9c0262303cd167ccb97a815d441bf Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 14 Apr 2026 23:43:23 -0700 Subject: [PATCH 3/7] Add dotfiles-test `--fix` flag --- .bin/dotfiles-test | 26 ++++++++++++++++++++++---- .vim/init.lua | 2 +- README.md | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.bin/dotfiles-test b/.bin/dotfiles-test index b0f2b35..b4fbae9 100755 --- a/.bin/dotfiles-test +++ b/.bin/dotfiles-test @@ -24,17 +24,19 @@ script_dir=${0:A:h} repo_root=${script_dir:h} run_here=0 assume_yes=0 +apply_fixes=0 keep_clone=${DOTFILES_CI_KEEP_CLONE:-0} usage() { cat <<'EOF' Usage: - dotfiles-test [--here] [--yes] + dotfiles-test [--here] [--fix] [--yes] Without arguments, run the checks in a disposable normal clone. Options: --here Run in the current checkout instead of a temp clone. + --fix Apply fixes where supported before running the remaining checks. -y, --yes Skip the confirmation prompt for --here. -h, --help Show this help. EOF @@ -45,6 +47,9 @@ while (( $# > 0 )); do --here) run_here=1 ;; + --fix) + apply_fixes=1 + ;; -y|--yes) assume_yes=1 ;; @@ -120,10 +125,18 @@ run_checks_here() { --no-save \ prettier@3 >/dev/null - "$tmp_root/npm-tools/node_modules/.bin/prettier" --check -- "${tracked_md[@]}" + if (( apply_fixes == 1 )); then + "$tmp_root/npm-tools/node_modules/.bin/prettier" --write -- "${tracked_md[@]}" + else + "$tmp_root/npm-tools/node_modules/.bin/prettier" --check -- "${tracked_md[@]}" + fi fi - stylua --check .vim/init.lua .vim/lua + if (( apply_fixes == 1 )); then + stylua .vim/init.lua .vim/lua + else + stylua --check .vim/init.lua .vim/lua + fi luacheck --config .vim/.luacheckrc .vim/init.lua .vim/lua zsh -n .zshenv .zshrc @@ -195,7 +208,12 @@ run_checks_in_temp_clone() { (cd "$tmp_root/repo" && rm -f -- "${deleted_files[@]}") fi - (cd "$tmp_root/repo" && ./.bin/dotfiles-test --here --yes) + local nested_args=(--here --yes) + if (( apply_fixes == 1 )); then + nested_args+=(--fix) + fi + + (cd "$tmp_root/repo" && ./.bin/dotfiles-test "${nested_args[@]}") cleanup_clone } diff --git a/.vim/init.lua b/.vim/init.lua index 2510321..5b2e42c 100644 --- a/.vim/init.lua +++ b/.vim/init.lua @@ -25,4 +25,4 @@ require("lazy").setup({ }) -- Load remaining VimScript config -vim.cmd('source ~/.vim/vimrc') +vim.cmd("source ~/.vim/vimrc") diff --git a/README.md b/README.md index e425def..1004e76 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ dotfiles pull ```zsh dotfiles-test # Run tests in a disposable normal clone dotfiles-test --here # Run tests in the current checkout, with confirmation +dotfiles-test --here --fix # Apply available fixes in the current checkout, with confirmation dotfiles-test --here --yes # Run tests in the current checkout, without confirmation ``` From 2ef7f1cf5b85c4709650c54ba7bbd7637ad61677 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 14 Apr 2026 23:46:18 -0700 Subject: [PATCH 4/7] Quiet Prettier --- .bin/dotfiles-test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.bin/dotfiles-test b/.bin/dotfiles-test index b4fbae9..57dba64 100755 --- a/.bin/dotfiles-test +++ b/.bin/dotfiles-test @@ -126,9 +126,9 @@ run_checks_here() { prettier@3 >/dev/null if (( apply_fixes == 1 )); then - "$tmp_root/npm-tools/node_modules/.bin/prettier" --write -- "${tracked_md[@]}" + "$tmp_root/npm-tools/node_modules/.bin/prettier" --list-different --write -- "${tracked_md[@]}" else - "$tmp_root/npm-tools/node_modules/.bin/prettier" --check -- "${tracked_md[@]}" + "$tmp_root/npm-tools/node_modules/.bin/prettier" --list-different -- "${tracked_md[@]}" fi fi From 39d4e9cc9c6e8830a60e3ad3d517d981e059516b Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 14 Apr 2026 23:52:58 -0700 Subject: [PATCH 5/7] Fix CI install --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 786d482..d58c9d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,9 @@ jobs: sudo apt-get update sudo apt-get install --yes neovim luarocks zsh - - uses: taiki-e/install-action@stylua + - uses: taiki-e/install-action@v2 + with: + tool: stylua - name: Install Luacheck run: sudo luarocks install luacheck From 611ea7db25fe871267af3e1d1cea231246dbf500 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 14 Apr 2026 23:55:13 -0700 Subject: [PATCH 6/7] Run CI on push on trunk only --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d58c9d9..ce9a1ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,8 @@ name: CI on: pull_request: push: + branches: + - master jobs: ci: From 1eeaeda6268c9e72db73614e0c5bab0393d6fe1d Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Sat, 18 Apr 2026 20:37:54 -0700 Subject: [PATCH 7/7] Defer contributor documentation --- README.md | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1004e76..81cc93a 100644 --- a/README.md +++ b/README.md @@ -19,21 +19,6 @@ dotfiles config --local status.showUntrackedFiles no dotfiles checkout ``` -### Machine-Local Commit Identity - -Set in `.mise.local.toml`: - -```toml -# $HOME/.mise.local.toml -[env] -GIT_AUTHOR_EMAIL = "you@company.com" -GIT_COMMITTER_EMAIL = "you@company.com" -JJ_EMAIL = "you@company.com" -``` - -Read more about per-machine settings in -[.docs/MACHINE_LOCAL_CONFIGURATION.md](./.docs/MACHINE_LOCAL_CONFIGURATION.md). - ### If zsh is not the default shell ```zsh @@ -46,15 +31,6 @@ chsh -s zsh dotfiles pull ``` -## Test - -```zsh -dotfiles-test # Run tests in a disposable normal clone -dotfiles-test --here # Run tests in the current checkout, with confirmation -dotfiles-test --here --fix # Apply available fixes in the current checkout, with confirmation -dotfiles-test --here --yes # Run tests in the current checkout, without confirmation -``` - ## Usage The dotfiles are now in your home folder, in the conventional location. Your @@ -73,6 +49,31 @@ lifecycle for you][the best way to store your dotfiles]. Same interface as `git`. No extra, bespoke tool. The repo layout stays in sync with how the files are used. +### Machine-Local Commit Identity + +Set in `.mise.local.toml`: + +```toml +# $HOME/.mise.local.toml +[env] +GIT_AUTHOR_EMAIL = "you@company.com" +GIT_COMMITTER_EMAIL = "you@company.com" +JJ_EMAIL = "you@company.com" +``` + +Read more about per-machine settings in +[.docs/MACHINE_LOCAL_CONFIGURATION.md](./.docs/MACHINE_LOCAL_CONFIGURATION.md). + +### Test + +The test command is globally added to `$PATH`: `$HOME/.bin/dotfiles-test`. + +```zsh +dotfiles-test # Run tests in a disposable normal clone +dotfiles-test --here # Run tests in the current checkout, with confirmation +dotfiles-test --here --fix # Apply available fixes in the current checkout, with confirmation +``` + ### Advanced Usage See [the docs folder](./.docs/).