diff --git a/.agents/docs/hk.md b/.agents/docs/hk.md new file mode 100644 index 000000000..937ae61f6 --- /dev/null +++ b/.agents/docs/hk.md @@ -0,0 +1,102 @@ +# hk + +Git hooks and code quality checks for the yadm dotfiles repo. + +## Configuration + +**Main config**: `~/hk.pkl` (requires hk v1.44.1+ for yadm bare-repo support) + +hk is installed globally via `hk install --global`, using Git 2.54+ config-based hooks. It runs as a +silent no-op in repos without an `hk.pkl`. + +### yadm-specific settings + +Two settings in `hk.pkl` are needed to make hk work cleanly in the yadm context +(`GIT_WORK_TREE=$HOME`): + +- **`env { ["HK_STASH_UNTRACKED"] = "false" }`** — prevents hk from running + `git status --untracked-files=all` which would scan the entire home directory (~45s). + See [jdx/hk#860](https://github.com/jdx/hk/discussions/860). +- **`stash = "none"`** on pre-commit and fix hooks — avoids a recursive hook invocation. + `hk install --global` registers a hook on every git operation, so the internal `git stash push` + hk runs during pre-commit would re-trigger hk and cause an infinite loop. Downside: if you have + unstaged changes, auto-fixers may modify them alongside staged content. + +## Running Hooks Manually + +```bash +# Run all checks (all files) +yadm enter hk check --all + +# Run all fixers (all files) +yadm enter hk fix --all + +# Via mise tasks +mise run dotfiles:lint +mise run dotfiles:fix + +# Dry-run: see what would run and why +yadm enter hk check --plan +``` + +## Configured Steps + +| Step | Purpose | +| ------------------------------- | ---------------------------------------------- | +| trailing-whitespace | Remove trailing whitespace | +| newlines | Ensure files end with a newline | +| check-merge-conflict | Detect leftover conflict markers | +| typos | Spell checking for source code | +| gitleaks | Secret detection | +| yamllint | YAML linting | +| editorconfig-checker | EditorConfig conformance | +| shellcheck | Shell script linting (bash/sh only) | +| shfmt | Shell script formatting | +| zsh-syntax-check | Zsh syntax validation (`zsh -n`) | +| stylua | Lua formatting | +| luacheck | Lua linting | +| prettier | JSON/Markdown/YAML formatting | +| standardrb | Ruby linting/formatting | +| check-json | JSON parse validation | +| check-executables-have-shebangs | Executables must have shebangs | +| check-added-large-files | Block files over 500KB | +| no-yadm-alt-symlinks | Reject staged yadm alt symlinks | +| mise-fmt | Mise config formatting | +| renovate-config-validator | Renovate config validation | +| commitlint | Conventional commit messages (commit-msg hook) | + +## CI Integration + +The `hk-check` job in `.github/workflows/ci.yml` runs `hk check --all` via `jdx/mise-action`, +which installs all tools from the Linux mise config. Steps skipped in CI: + +- `standardrb` — requires Ruby gems not in CI +- `renovate-config-validator` — requires npm:renovate +- `no-yadm-alt-symlinks` — requires yadm + +## Bypassing Hooks + +```bash +HK=0 yadm commit -m "wip" # skip all hk hooks +HK_SKIP_STEPS=typos yadm commit -m "" # skip a specific step +``` + +## Debugging + +```bash +yadm enter hk check -v # verbose output +yadm enter hk check --plan # show what would run +yadm enter hk check --step stylua # single step +``` + +## Adding New Steps + +Edit `~/hk.pkl`. Add to the `fast_steps` mapping — it is shared across `pre-commit`, `fix`, and +`check` hooks. Run `yadm enter hk validate` to verify syntax. + +## Version Management + +hk version is managed by mise (`hk = "latest"` in `~/.config/mise/conf.d/global.toml`). +When upgrading, bump the `amends` and `import` URLs in `~/hk.pkl` to match the new version. + +Check current version: `hk --version` diff --git a/.agents/docs/pre-commit.md b/.agents/docs/pre-commit.md deleted file mode 100644 index d10618d65..000000000 --- a/.agents/docs/pre-commit.md +++ /dev/null @@ -1,72 +0,0 @@ -# Pre-commit - -Pre-commit hooks and code quality checks. - -## Configuration - -**Main config**: `~/.pre-commit-config.yaml` - -## Running Hooks - -```bash -# In YADM-managed repo (home directory) -yadm enter pre-commit run --all-files - -# Run specific hook -yadm enter pre-commit run --all-files - -# Run with auto-fix (manual stage) -yadm enter pre-commit run --all-files --hook-stage manual -``` - -## Configured Hooks - -| Hook | Purpose | -| ------------------------- | --------------------------------------------------- | -| pre-commit-hooks | Basic file checks (trailing whitespace, YAML, JSON) | -| shellcheck | Shell script linting | -| yamllint | YAML linting | -| stylua | Lua formatting | -| prettier | JSON/Markdown/YAML formatting | -| standardrb | Ruby linting | -| markdownlint-cli2 | Markdown linting | -| editorconfig-checker | EditorConfig validation | -| zsh-syntax-check | Zsh syntax validation | -| mise-fmt | Mise configuration formatting | -| renovate-config-validator | Renovate config validation | -| commitlint | Conventional commit message validation (commit-msg) | - -## Mise Task Integration - -```bash -mise run dotfiles:lint # Run all hooks -mise run dotfiles:fix # Run with manual fixes -``` - -## Common Issues - -**Hook fails to install:** - -```bash -pre-commit clean -pre-commit install-hooks -``` - -**Stale hooks after update:** - -```bash -pre-commit autoupdate -``` - -## Adding New Hooks - -1. Add hook to `~/.pre-commit-config.yaml` -2. Run `pre-commit autoupdate` to get latest versions -3. Test with `yadm enter pre-commit run --all-files` - -## Guidelines - -- Keep hooks fast (use native tools when possible) -- Group related hooks together in config -- Use `stages: [manual]` for slow or optional hooks -- Hook versions are updated via Renovate bot diff --git a/.config/mise/conf.d/global.toml b/.config/mise/conf.d/global.toml index 3c09272ae..24f99b201 100644 --- a/.config/mise/conf.d/global.toml +++ b/.config/mise/conf.d/global.toml @@ -11,10 +11,29 @@ dua = "latest" # ── Developer workflow ──────────────────────────────────────────────── # Git hook manager -pre-commit = "latest" +hk = "latest" +# Pkl configuration language (required by hk) +pkl = "latest" # Utilities for working with patches (filterdiff, etc.) "github:twaugh/patchutils" = "latest" +# ── Linting & formatting ────────────────────────────────────────────── +# Secrets scanner for git repos +"aqua:gitleaks/gitleaks" = "latest" +# Shell script linter +shellcheck = "latest" +# Shell script formatter +shfmt = "latest" +# Lua code formatter +stylua = "latest" +# Lua linter: on macOS via Homebrew; on Linux declared in linux.toml +# Spell checker for source code +typos = "latest" +# EditorConfig file format checker +"aqua:editorconfig-checker/editorconfig-checker" = "latest" +# JSON/Markdown/YAML formatter +prettier = "latest" + # ── AI tooling ──────────────────────────────────────────────────────── # Lazy-loading MCP server proxy for AI tools "npm:lazy-mcp" = "latest" diff --git a/.config/mise/conf.d/linux.toml##os.Linux b/.config/mise/conf.d/linux.toml##os.Linux index 1818cb4f1..5a5f10062 100644 --- a/.config/mise/conf.d/linux.toml##os.Linux +++ b/.config/mise/conf.d/linux.toml##os.Linux @@ -53,12 +53,9 @@ btop = "latest" "aqua:sharkdp/hyperfine" = "latest" # ── Linting & formatting ────────────────────────────────────────────── -# Secrets scanner for git repos -"aqua:gitleaks/gitleaks" = "latest" -# Shell script linter -shellcheck = "latest" -# Lua code formatter -stylua = "latest" +# (most linting tools declared in global.toml; luacheck only available via mise on Linux) +# Lua linter +luacheck = "latest" # ── npm ─────────────────────────────────────────────────────────────── # Auto-generates table of contents in Markdown diff --git a/.config/mise/conf.d/personal.toml##class.Personal b/.config/mise/conf.d/personal.toml##class.Personal index 9996f86dd..e233bb978 100644 --- a/.config/mise/conf.d/personal.toml##class.Personal +++ b/.config/mise/conf.d/personal.toml##class.Personal @@ -4,4 +4,4 @@ cargo-binstall = "latest" # ── Automation ──────────────────────────────────────────────────────── # Automated dependency update bot -"npm:renovate" = "43.138.0" +"npm:renovate" = "43.138.1" diff --git a/.config/mise/conf.d/tasks.toml b/.config/mise/conf.d/tasks.toml index 3da14b942..2a7d9941d 100644 --- a/.config/mise/conf.d/tasks.toml +++ b/.config/mise/conf.d/tasks.toml @@ -26,6 +26,14 @@ run = "~/.config/yadm/scripts/update.zsh" description = "Run dotfile health checks" run = "~/.config/yadm/scripts/run-checks.zsh" +[tasks."dotfiles:lint"] +description = "Run all linters on dotfiles" +run = "yadm enter hk check --all" + +[tasks."dotfiles:fix"] +description = "Run all fixers on dotfiles" +run = "yadm enter hk fix --all" + # ── Git ─────────────────────────────────────────────────────────────── [tasks."git:push"] diff --git a/.config/opencode/AGENTS.md b/.config/opencode/AGENTS.md index b17fd8469..9298646e6 100644 --- a/.config/opencode/AGENTS.md +++ b/.config/opencode/AGENTS.md @@ -38,7 +38,7 @@ the task needs them. - [Mise Tasks](~/.agents/docs/mise-tasks.md) - Task automation for dotfiles - [Neovim](~/.agents/docs/neovim.md) - Configuration structure and plugin management - [Mise](~/.agents/docs/mise.md) - Runtime versions and CLI tools -- [Pre-commit](~/.agents/docs/pre-commit.md) - Hooks and code quality checks +- [hk](~/.agents/docs/hk.md) - Git hooks and code quality checks - [Renovate Bot](~/.agents/docs/renovate.md) - Dependency update configuration and troubleshooting - [Writing Style](~/.agents/docs/writing-style.md) - Pedro's tone, formatting, and MR conventions - [GDK Dotfiles](~/.agents/docs/gdk-dotfiles.md) - Personal files synced into `$GDK_ROOT/gitlab` diff --git a/.config/yadm/scripts/quiet-on-success.sh b/.config/yadm/scripts/quiet-on-success.sh new file mode 100755 index 000000000..cdb5c90d7 --- /dev/null +++ b/.config/yadm/scripts/quiet-on-success.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Runs a command silently; only shows output on failure +output=$("$@" 2>&1) +code=$? +[ $code -ne 0 ] && printf '%s\n' "$output" +exit $code diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3aac294d..6d5ba1d4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: +"on": pull_request: push: branches: @@ -14,34 +14,25 @@ concurrency: cancel-in-progress: true jobs: - # Job 1: Pre-commit hooks - pre-commit: - name: Pre-commit hooks + # Job 1: hk check + hk-check: + name: hk check runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 - with: - python-version: "3.14" - - # Cache pre-commit environments - - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 - with: - path: ~/.cache/pre-commit - key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - uses: jdx/mise-action@v2 # Install zsh for syntax checking - name: Install zsh run: sudo apt-get update && sudo apt-get install -y zsh - # Run pre-commit on all files - # Skip hooks that require tools not installed in CI (mise, ruby) - - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 + # Run hk check on all files + # Skip steps that require tools not available in CI (ruby, renovate, yadm) + - name: Run hk check + run: hk check --all env: - SKIP: standardrb,renovate-config-validator - with: - extra_args: --all-files + HK_SKIP_STEPS: standardrb,renovate-config-validator,no-yadm-alt-symlinks # Job 2: Neovim config validation neovim-validate: @@ -75,20 +66,7 @@ jobs: echo "Neovim config validation passed" - # Job 3: Luacheck for Neovim Lua files - luacheck: - name: Luacheck (Neovim Lua) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Run luacheck - uses: nebularg/actions-luacheck@d4137dd840545b28521ed21931e06913d93c0bc1 # v1 - with: - files: ".config/nvim" - args: "--config .luacheckrc --formatter plain" - - # Job 4: Bootstrap script validation + # Job 3: Bootstrap script validation bootstrap-validate: name: Bootstrap scripts syntax runs-on: ubuntu-latest @@ -96,7 +74,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # Validate bash syntax for all bootstrap scripts - # Note: shellcheck runs in pre-commit job + # Note: shellcheck runs in hk-check job - name: Bash syntax check run: | echo "Checking bash syntax for bootstrap scripts..." @@ -124,7 +102,7 @@ jobs: exit $EXIT_CODE - # Job 5: Secret scanning + # Job 4: Secret scanning secret-scanning: name: Secret scanning (Gitleaks) runs-on: ubuntu-latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 231110883..000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,135 +0,0 @@ ---- -exclude: '^\.config/.*|^\.local/.*' - -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: check-added-large-files - - id: check-executables-have-shebangs - - id: check-json - - id: check-symlinks - - id: check-yaml - exclude: '^\.local/share/.*' - - id: double-quote-string-fixer - - id: end-of-file-fixer - exclude: '^\.local/share/.*' - - id: trailing-whitespace - exclude: '^\.local/share/.*' - - - repo: local - hooks: - - id: no-yadm-alt-symlinks - name: reject yadm alt symlinks - entry: .config/yadm/scripts/check-no-alt-symlinks.zsh - language: script - types: [symlink] - - - repo: https://github.com/shellcheck-py/shellcheck-py - rev: v0.10.0.1 - hooks: - - id: shellcheck - exclude: '^\.config/.*|^\.local/.*|^\.shellrc/zshrc\.d/.*|\.zsh(##.*)?$|^\.zlogin$|^\.zshenv$|\.sh##default$' - - - repo: local - hooks: - - id: renovate-config-validator - name: Renovate Config Validator - entry: mise x npm:renovate -- renovate-config-validator --strict - language: system - files: (^|/)\.?renovate(rc)?\.json5?$|^renovate\.json5?$ - - - repo: https://github.com/adrienverge/yamllint - rev: v1.38.0 - hooks: - - id: yamllint - args: ["-c", ".yamllint.yml"] - language_version: python3.14 - - - repo: https://github.com/JohnnyMorganz/StyLua - rev: v2.4.1 - hooks: - - id: stylua-github - alias: stylua - name: stylua (check) - args: ["--check", "--config-path", ".config/nvim/.stylua.toml"] - - id: stylua-github - alias: stylua-fix - name: stylua (fix) - args: ["--config-path", ".config/nvim/.stylua.toml"] - stages: [manual] - - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 - hooks: - - id: prettier - name: prettier (check) - exclude: '^\.agents/skills/.*' - types_or: [json, markdown, yaml] - language_version: system - - id: prettier - name: prettier (fix) - language_version: system - alias: prettier-fix - args: ["--write", "--list-different"] - exclude: '^\.agents/skills/.*' - types_or: [json, markdown, yaml] - stages: [manual] - - - repo: local - hooks: - - id: standardrb - name: standardrb (check) - entry: standardrb - language: system - types: [ruby] - - id: standardrb-fix - name: standardrb (fix) - entry: standardrb --fix - language: system - types: [ruby] - stages: [manual] - - id: markdownlint-cli2 - name: markdownlint-cli2 - entry: npx markdownlint-cli2 - language: system - types: [markdown] - stages: [manual] - - id: markdownlint-cli2-fix - name: markdownlint-cli2-fix - entry: npx markdownlint-cli2 --fix - language: system - types: [markdown] - stages: [manual] - - id: zsh-syntax-check - name: zsh syntax check - entry: zsh -n - language: system - files: '\.zsh(##.*)?$|^\.zlogin$|^\.shellrc/zshrc\.d/functions/[^/]+$' - - id: mise-fmt - name: mise fmt (check) - entry: mise fmt --check - language: system - files: '(^|/)\.?mise(\.[\w-]+)*\.toml$|(^|/)\.?mise/config(\.local)?\.toml$|(^|/)\.config/mise(\.toml|/config(\.local)?\.toml|/conf\.d/.*\.toml)$' - pass_filenames: false - - id: mise-fmt-fix - name: mise fmt (fix) - entry: mise fmt - language: system - files: '(^|/)\.?mise(\.[\w-]+)*\.toml$|(^|/)\.?mise/config(\.local)?\.toml$|(^|/)\.config/mise(\.toml|/config(\.local)?\.toml|/conf\.d/.*\.toml)$' - pass_filenames: false - stages: [manual] - - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: 3.6.1 - hooks: - - id: editorconfig-checker - exclude: '^\.config/.*|^\.local/.*|^\.shellrc/zshrc\.d/functions/scripts/.*\.rb$|\.md$' - - - repo: local - hooks: - - id: commitlint - name: commitlint - entry: sh -c 'cat "$1" | npx --yes --package @commitlint/cli -- commitlint' -- - language: system - stages: [commit-msg] diff --git a/hk.pkl b/hk.pkl new file mode 100644 index 000000000..e8b5dec33 --- /dev/null +++ b/hk.pkl @@ -0,0 +1,147 @@ +// hk configuration — https://hk.jdx.dev/ +// Dotfiles (yadm) pre-commit hooks +amends "package://github.com/jdx/hk/releases/download/v1.44.1/hk@1.44.1#/Config.pkl" +import "package://github.com/jdx/hk/releases/download/v1.44.1/hk@1.44.1#/Builtins.pkl" + +exclude = List(".config", ".local", ".git", "node_modules", ".cache", ".npm", ".cargo", ".rustup") + +display_skip_reasons = List() +terminal_progress = false + +// Prevent scanning all of $HOME for untracked files (GIT_WORK_TREE=$HOME in yadm). +// Requires hk v1.44.1+. See https://github.com/jdx/hk/discussions/860 +env { + ["HK_STASH_UNTRACKED"] = "false" +} + +local binary_excludes = List( + "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp", "*.ico", + "*.woff", "*.woff2", "*.ttf", "*.eot", "*.pdf", "*.zip" +) + +local fast_steps = new Mapping { + // ── Tier 1: Universal ──────────────────────────────────────────────── + ["trailing-whitespace"] = (Builtins.trailing_whitespace) { + exclude = binary_excludes + } + ["newlines"] = (Builtins.newlines) { + exclude = binary_excludes + } + ["check-merge-conflict"] = (Builtins.check_merge_conflict) {} + + // ── Tier 2: Common tools ───────────────────────────────────────────── + ["typos"] = (Builtins.typos) { + exclude = binary_excludes + } + ["gitleaks"] { + check = "gitleaks protect --staged --redact --no-banner" + } + ["yamllint"] = (Builtins.yamllint) {} + ["editorconfig-checker"] = (Builtins.editorconfig_checker) { + exclude = List( + ".config/*", ".local/*", + ".shellrc/zshrc.d/functions/scripts/*.rb", + "*.md" + ) + } + + // ── Shell ──────────────────────────────────────────────────────────── + ["shellcheck"] = (Builtins.shellcheck) { + exclude = List( + "*.zsh", "*.zsh##*", ".zlogin", ".zshenv", + ".shellrc/zshrc.d/*" + ) + batch = true + } + ["shfmt"] = (Builtins.shfmt) { + batch = true + } + ["zsh-syntax-check"] { + glob = List( + "*.zsh", "*.zsh##*", ".zlogin", + ".shellrc/zshrc.d/functions/*" + ) + check = "zsh -n {{files}}" + } + + // ── Lua ────────────────────────────────────────────────────────────── + ["stylua"] = (Builtins.stylua) {} + ["luacheck"] = (Builtins.luacheck) {} + + // ── JSON / Markdown / YAML formatting ──────────────────────────────── + ["prettier"] = (Builtins.prettier) { + glob = List("*.json", "*.md", "*.yaml", "*.yml") + exclude = List(".agents/skills/*") + } + + // ── Ruby ───────────────────────────────────────────────────────────── + ["standardrb"] { + glob = List("*.rb") + check = "standardrb {{files}}" + fix = "standardrb --fix {{files}}" + } + + // ── Validation ─────────────────────────────────────────────────────── + ["check-json"] { + glob = List("*.json") + check = "python3 -c 'import json,sys; [json.load(open(f)) for f in sys.argv[1:]]' {{files}}" + } + ["check-executables-have-shebangs"] { + check = """ + for f in {{files}}; do + if [ -x "$f" ] && [ -f "$f" ] && ! head -c2 "$f" | grep -q '^#!'; then + echo "$f: executable without shebang" + exit 1 + fi + done + """ + } + ["check-added-large-files"] { + check = """ + max_kb=500 + for f in {{files}}; do + if [ -f "$f" ]; then + size_kb=$(( $(wc -c < "$f") / 1024 )) + if [ "$size_kb" -gt "$max_kb" ]; then + echo "$f is ${size_kb}KB (max ${max_kb}KB)" + exit 1 + fi + fi + done + """ + } + + // ── Dotfiles-specific ──────────────────────────────────────────────── + ["no-yadm-alt-symlinks"] { + glob = List("*") + check = ".config/yadm/scripts/check-no-alt-symlinks.zsh {{files}}" + } + ["mise-fmt"] = (Builtins.mise) {} + ["renovate-config-validator"] { + glob = List("*renovate*") + check = "mise x npm:renovate -- renovate-config-validator --strict" + } +} + +hooks { + ["pre-commit"] { + fix = true + stash = "none" + steps = fast_steps + } + ["fix"] { + fix = true + stash = "none" + steps = fast_steps + } + ["check"] { + steps = fast_steps + } + ["commit-msg"] { + steps { + ["commitlint"] { + check = "npx --yes --package @commitlint/cli -- commitlint < {{commit_msg_file}}" + } + } + } +}