windows #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Install Dotfiles | |
| on: | |
| # No paths filter: bootstrap.sh touches most of the repo (.gitconfig, | |
| # .tmux.conf, .gitattributes, .claude/**, etc.). Listing every relevant | |
| # file is fragile and silently misses regressions when the list drifts. | |
| push: | |
| branches: [main] | |
| pull_request: | |
| schedule: | |
| - cron: '0 3 * * 6' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| install-ubuntu: | |
| name: Install (Ubuntu container) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| container: | |
| image: ubuntu:24.04 | |
| env: | |
| DEBIAN_FRONTEND: noninteractive | |
| steps: | |
| - name: Install prerequisites | |
| run: | | |
| apt-get update | |
| apt-get install -y git curl sudo tree xz-utils fontconfig gcc make python3 | |
| - name: Create test user | |
| run: | | |
| useradd -m -s /bin/bash clay | |
| echo "clay ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers | |
| su - clay -c "git config --global --add safe.directory '*'" | |
| - name: Checkout PR | |
| uses: actions/checkout@v4 | |
| with: | |
| path: dotfiles-pr | |
| - name: Stage PR repo at ~/dotfiles | |
| run: | | |
| cp -a dotfiles-pr /home/clay/dotfiles | |
| chown -R clay:clay /home/clay/dotfiles | |
| # restore-keys intentionally omitted: a loose fallback would let a | |
| # half-broken install from a prior run propagate. Cold rebuild on key | |
| # miss is slower but always reflects current bootstrap.sh state. | |
| - name: Cache pixi globals | |
| uses: actions/cache@v4 | |
| with: | |
| path: /home/clay/.pixi | |
| key: pixi-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('dotfiles-pr/scripts/bootstrap.sh') }} | |
| - name: Cache lazy.nvim plugins | |
| uses: actions/cache@v4 | |
| with: | |
| path: /home/clay/.local/share/nvim/lazy | |
| key: lazy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('dotfiles-pr/.config/nvim/lazy-lock.json') }} | |
| - name: Reset cached path ownership | |
| run: chown -R clay:clay /home/clay | |
| - name: Authenticated git for plugin clones (lift GH rate limit) | |
| if: github.actor == github.repository_owner | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| su - clay -c "git config --global url.\"https://x-access-token:${GH_TOKEN}@github.com/\".insteadOf \"https://github.com/\"" | |
| - name: Run bootstrap (first run) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: su - clay -c "export GITHUB_TOKEN='${GH_TOKEN}'; cd ~/dotfiles && ./scripts/bootstrap.sh" | |
| - name: Assert first bootstrap produced no tracked-file drift | |
| run: | | |
| su - clay -c ' | |
| set -e | |
| cd ~/dotfiles | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "::error::first bootstrap dirtied tracked files (would be invisible to a same-output two-run check)" | |
| git status --porcelain | |
| git diff | |
| exit 1 | |
| fi | |
| echo "✓ first run clean" | |
| ' | |
| - name: Run bootstrap (idempotency check) | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: su - clay -c "export GITHUB_TOKEN='${GH_TOKEN}'; cd ~/dotfiles && ./scripts/bootstrap.sh" | |
| - name: Assert second bootstrap did not modify tracked files | |
| run: | | |
| su - clay -c ' | |
| set -e | |
| cd ~/dotfiles | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "::error::second bootstrap modified tracked files" | |
| git status --porcelain | |
| git diff | |
| exit 1 | |
| fi | |
| echo "✓ no tracked-file changes after two bootstrap runs" | |
| ' | |
| - name: Verify symlinks | |
| run: | | |
| su - clay -c ' | |
| set -eo pipefail | |
| echo "Checking symlinks..." | |
| test_symlink() { | |
| local symlink=$1 | |
| local expected=$2 | |
| if [ -L "$symlink" ]; then | |
| local target=$(readlink -f "$symlink") | |
| if [[ "$target" == *"$expected"* ]]; then | |
| echo "✓ $symlink -> $expected" | |
| else | |
| echo "✗ $symlink points to wrong target: $target" | |
| return 1 | |
| fi | |
| else | |
| echo "✗ $symlink is not a symlink" | |
| return 1 | |
| fi | |
| } | |
| test_symlink ~/.bashrc ".bashrc" | |
| test_symlink ~/.gitignore ".gitignore" | |
| test_symlink ~/.gitlab_ci_skip ".gitlab_ci_skip" | |
| test_symlink ~/.claude ".claude" | |
| test_symlink ~/.config/nvim "nvim" | |
| test_symlink ~/.config/kitty "kitty" | |
| test_symlink ~/.config/starship.toml "starship.toml" | |
| if bash -n ~/.bashrc 2>/dev/null; then | |
| echo "✓ .bashrc syntax valid" | |
| else | |
| echo "✗ .bashrc has syntax errors" | |
| exit 1 | |
| fi | |
| ' | |
| - name: Verify pixi packages | |
| run: | | |
| su - clay -c ' | |
| export PATH="$HOME/.pixi/bin:$HOME/.local/bin:$PATH" | |
| set -eo pipefail | |
| echo "Checking pixi packages functionality..." | |
| pixi --version && echo "✓ pixi" | |
| nvim --version | head -1 && echo "✓ neovim" | |
| tmux -V && echo "✓ tmux" | |
| starship --version && echo "✓ starship" | |
| eza --version | head -1 && echo "✓ eza" | |
| bat --version && echo "✓ bat" | |
| fzf --version && echo "✓ fzf" | |
| rg --version | head -1 && echo "✓ ripgrep" | |
| fd --version && echo "✓ fd-find" | |
| fastfetch --version && echo "✓ fastfetch" | |
| stylua --version && echo "✓ stylua" | |
| selene --version && echo "✓ selene" | |
| jj --version && echo "✓ jujutsu" | |
| hyperfine --version && echo "✓ hyperfine" | |
| claude --version && echo "✓ claude code" | |
| ' | |
| - name: Verify NVM + Node | |
| run: | | |
| su - clay -c ' | |
| export NVM_DIR="$HOME/.local/share/nvm" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| set -eo pipefail | |
| nvm --version && echo "✓ nvm" | |
| node_version=$(node --version) | |
| if [[ "$node_version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+ ]]; then | |
| echo "✓ node $node_version" | |
| else | |
| echo "✗ node returned invalid version: $node_version" | |
| exit 1 | |
| fi | |
| npm --version && echo "✓ npm" | |
| ' | |
| - name: Verify kitty | |
| run: | | |
| su - clay -c ' | |
| if [ -x ~/.local/kitty.app/bin/kitty ]; then | |
| kitty_version=$(~/.local/kitty.app/bin/kitty --version 2>&1 || true) | |
| echo "✓ kitty binary: $kitty_version" | |
| else | |
| echo "✗ kitty binary not found or not executable" | |
| exit 1 | |
| fi | |
| if [ -L ~/.local/bin/kitty ]; then | |
| echo "✓ kitty symlink exists" | |
| else | |
| echo "⚠ kitty symlink missing (non-fatal)" | |
| fi | |
| ' | |
| - name: Verify nvim setup | |
| run: | | |
| su - clay -c ' | |
| set -eo pipefail | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| nvim_version=$(nvim --version | head -1) | |
| echo "✓ $nvim_version" | |
| if [ -d ~/.local/share/nvim/lazy ]; then | |
| plugin_count=$(find ~/.local/share/nvim/lazy -maxdepth 1 -type d | wc -l) | |
| if [ "$plugin_count" -gt 10 ]; then | |
| echo "✓ Lazy plugins installed ($plugin_count plugins)" | |
| else | |
| echo "⚠ Fewer than expected plugins ($plugin_count)" | |
| fi | |
| else | |
| echo "✗ Lazy plugin directory not found" | |
| exit 1 | |
| fi | |
| PARSER_DIR="$HOME/.local/share/nvim/site/parser" | |
| if [ ! -d "$PARSER_DIR" ]; then | |
| echo "✗ Treesitter parser directory not found at $PARSER_DIR" | |
| exit 1 | |
| fi | |
| parser_count=$(ls "$PARSER_DIR"/*.so 2>/dev/null | wc -l) | |
| for p in lua c bash markdown markdown_inline json yaml; do | |
| [ -f "$PARSER_DIR/$p.so" ] || { echo "✗ missing required parser: $p.so"; exit 1; } | |
| done | |
| if [ "$parser_count" -lt 12 ]; then | |
| echo "✗ only $parser_count parsers installed (expected >=12)" | |
| exit 1 | |
| fi | |
| echo "✓ Treesitter parsers ($parser_count parsers; lua/c/bash/markdown/json/yaml present)" | |
| if [ -d ~/.local/share/nvim/mason/packages ]; then | |
| mason_count=$(ls -d ~/.local/share/nvim/mason/packages/* 2>/dev/null | wc -l) | |
| echo "✓ Mason packages ($mason_count packages)" | |
| else | |
| echo "✗ Mason directory not found" | |
| exit 1 | |
| fi | |
| FZF_LIB=~/.local/share/nvim/lazy/telescope-fzf-native.nvim/build/libfzf.so | |
| if [ -f "$FZF_LIB" ]; then | |
| echo "✓ telescope-fzf-native built" | |
| else | |
| echo "✗ telescope-fzf-native build failed (missing $FZF_LIB)" | |
| exit 1 | |
| fi | |
| # Capture stdout+stderr and exit status independently — a non-zero | |
| # exit with no "error" string in output would otherwise pass. | |
| set +e | |
| output=$(timeout 30 nvim --headless -c "qa" 2>&1) | |
| status=$? | |
| set -e | |
| if [ "$status" -ne 0 ]; then | |
| echo "✗ nvim exited with status $status" | |
| echo "$output" | |
| exit 1 | |
| fi | |
| if echo "$output" | grep -qE "^E[0-9]+:|Error detected while processing|stack traceback"; then | |
| echo "✗ nvim startup output contains errors:" | |
| echo "$output" | |
| exit 1 | |
| fi | |
| echo "✓ nvim starts cleanly" | |
| ' | |
| - name: Measure nvim startup | |
| run: | | |
| su - clay -c ' | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| cd ~/dotfiles | |
| bash .github/scripts/benchmark-nvim.sh /tmp/result.json | |
| cat /tmp/result.json | |
| ' | |
| - name: Measure bash startup | |
| run: | | |
| su - clay -c ' | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| cd ~/dotfiles | |
| bash .github/scripts/benchmark-bash.sh /tmp/bash-result.json | |
| cat /tmp/bash-result.json | |
| ' | |
| - name: Restore benchmark history | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /tmp/cache | |
| key: ${{ runner.os }}-benchmark-history-${{ github.run_id }} | |
| restore-keys: | | |
| ${{ runner.os }}-benchmark-history- | |
| - name: Compare against rolling baseline (nvim + bash) | |
| env: | |
| RECORD: ${{ github.event_name == 'push' && '--record' || '' }} | |
| GITHUB_SHA: ${{ github.sha }} | |
| run: | | |
| chown -R clay:clay /tmp/cache 2>/dev/null || true | |
| su - clay -c " | |
| export GITHUB_SHA='${GITHUB_SHA}' | |
| cd ~/dotfiles | |
| python3 .github/scripts/compare-benchmark.py /tmp/result.json /tmp/cache 'nvim startup (Ubuntu)' ${RECORD} | |
| python3 .github/scripts/compare-benchmark.py /tmp/bash-result.json /tmp/cache 'bash startup (Ubuntu)' ${RECORD} | |
| " | |
| - name: Save benchmark history | |
| if: github.event_name == 'push' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /tmp/cache | |
| key: ${{ runner.os }}-benchmark-history-${{ github.run_id }} | |
| - name: Collect failure diagnostics | |
| if: failure() | |
| run: | | |
| mkdir -p /tmp/failure-bundle | |
| su - clay -c ' | |
| cd ~/dotfiles | |
| git status --porcelain > /tmp/failure-bundle/git-status.txt 2>&1 || true | |
| git diff > /tmp/failure-bundle/git-diff.txt 2>&1 || true | |
| cp -a ~/.local/state/nvim /tmp/failure-bundle/nvim-state 2>/dev/null || true | |
| pixi global list > /tmp/failure-bundle/pixi-globals.txt 2>&1 || true | |
| ls -la ~ > /tmp/failure-bundle/home-listing.txt 2>&1 || true | |
| ' || true | |
| - name: Upload failure diagnostics | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: failure-ubuntu-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: /tmp/failure-bundle | |
| if-no-files-found: ignore | |
| - name: Test summary | |
| if: always() | |
| run: | | |
| { | |
| echo "## Install Dotfiles — Ubuntu" | |
| echo "" | |
| echo "| Component | Version |" | |
| echo "|-----------|---------|" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| su - clay -c ' | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| export NVM_DIR="$HOME/.local/share/nvm" | |
| [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" | |
| echo "| pixi | $(pixi --version 2>/dev/null || echo "N/A") |" | |
| echo "| nvim | $(nvim --version 2>/dev/null | head -1 || echo "N/A") |" | |
| echo "| node | $(node --version 2>/dev/null || echo "N/A") |" | |
| echo "| kitty | $(~/.local/kitty.app/bin/kitty --version 2>/dev/null || echo "N/A") |" | |
| ' >> "$GITHUB_STEP_SUMMARY" | |
| install-macos: | |
| name: Install (macOS) | |
| runs-on: macos-latest | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| path: dotfiles-pr | |
| - name: Stage dotfiles at $HOME | |
| run: | | |
| mkdir -p "$HOME/dotfiles" | |
| rsync -a --delete dotfiles-pr/ "$HOME/dotfiles/" | |
| cd "$HOME/dotfiles" | |
| git config --global --add safe.directory "$HOME/dotfiles" | |
| - name: Cache pixi globals | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.pixi | |
| key: pixi-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('dotfiles-pr/scripts/bootstrap.sh') }} | |
| - name: Cache lazy.nvim plugins | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.local/share/nvim/lazy | |
| key: lazy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('dotfiles-pr/.config/nvim/lazy-lock.json') }} | |
| - name: Authenticated git for plugin clones | |
| if: github.actor == github.repository_owner | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "https://github.com/" | |
| - name: Run bootstrap (first run) | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: cd "$HOME/dotfiles" && ./scripts/bootstrap.sh | |
| - name: Assert first bootstrap produced no tracked-file drift | |
| run: | | |
| set -e | |
| cd "$HOME/dotfiles" | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "::error::first bootstrap dirtied tracked files" | |
| git status --porcelain | |
| git diff | |
| exit 1 | |
| fi | |
| echo "✓ first run clean" | |
| - name: Run bootstrap (idempotency check) | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: cd "$HOME/dotfiles" && ./scripts/bootstrap.sh | |
| - name: Assert second bootstrap did not modify tracked files | |
| run: | | |
| set -e | |
| cd "$HOME/dotfiles" | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "::error::second bootstrap modified tracked files" | |
| git status --porcelain | |
| git diff | |
| exit 1 | |
| fi | |
| echo "✓ no tracked-file changes after two bootstrap runs" | |
| - name: Verify symlinks (macOS) | |
| run: | | |
| set -eo pipefail | |
| test_symlink() { | |
| local s=$1 expected=$2 | |
| [ -L "$s" ] || { echo "✗ $s not a symlink"; return 1; } | |
| local target=$(readlink "$s") | |
| [[ "$target" == *"$expected"* ]] || { echo "✗ $s -> $target (expected *$expected*)"; return 1; } | |
| echo "✓ $s -> $expected" | |
| } | |
| test_symlink ~/.bashrc ".bashrc" | |
| test_symlink ~/.gitignore ".gitignore" | |
| test_symlink ~/.gitlab_ci_skip ".gitlab_ci_skip" | |
| test_symlink ~/.claude ".claude" | |
| test_symlink ~/.config/nvim "nvim" | |
| test_symlink ~/.config/kitty "kitty" | |
| test_symlink ~/.config/starship.toml "starship.toml" | |
| test_symlink ~/.config/git/config "gitconfig" | |
| [ -f ~/.bash_profile ] && echo "✓ .bash_profile present (Darwin)" | |
| bash -n ~/.bashrc && echo "✓ .bashrc syntax valid" | |
| - name: Verify pixi tools (macOS) | |
| run: | | |
| export PATH="$HOME/.pixi/bin:$HOME/.local/bin:$PATH" | |
| set -eo pipefail | |
| pixi --version && echo "✓ pixi" | |
| nvim --version | head -1 && echo "✓ neovim" | |
| tmux -V && echo "✓ tmux" | |
| starship --version && echo "✓ starship" | |
| eza --version | head -1 && echo "✓ eza" | |
| bat --version && echo "✓ bat" | |
| fzf --version && echo "✓ fzf" | |
| rg --version | head -1 && echo "✓ ripgrep" | |
| fd --version && echo "✓ fd-find" | |
| fastfetch --version && echo "✓ fastfetch" | |
| stylua --version && echo "✓ stylua" | |
| selene --version && echo "✓ selene" | |
| jj --version && echo "✓ jujutsu" | |
| hyperfine --version && echo "✓ hyperfine" | |
| claude --version && echo "✓ claude code" | |
| - name: Verify modern bash is default shell (macOS) | |
| run: | | |
| set -eo pipefail | |
| login_shell=$(dscl . -read /Users/"$USER" UserShell | awk '{print $2}') | |
| want="$HOME/.pixi/bin/bash" | |
| if [ "$login_shell" = "$want" ]; then | |
| echo "✓ login shell is pixi bash ($login_shell)" | |
| else | |
| echo "✗ login shell is $login_shell, expected $want" | |
| exit 1 | |
| fi | |
| - name: Verify nvim starts (macOS) | |
| run: | | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| # perl's alarm + exec gives a portable 30s timeout (no coreutils on | |
| # stock macOS; SIGALRM kills the child, status 142 surfaces below). | |
| set +e | |
| output=$(perl -e 'alarm 30; exec @ARGV' nvim --headless -c "qa" 2>&1) | |
| status=$? | |
| set -e | |
| if [ "$status" -ne 0 ]; then | |
| echo "✗ nvim exited with status $status" | |
| echo "$output" | |
| exit 1 | |
| fi | |
| if echo "$output" | grep -qE "^E[0-9]+:|Error detected while processing|stack traceback"; then | |
| echo "✗ nvim startup output contains errors:" | |
| echo "$output" | |
| exit 1 | |
| fi | |
| echo "✓ nvim starts cleanly" | |
| - name: Measure nvim startup | |
| run: | | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| cd "$HOME/dotfiles" | |
| bash .github/scripts/benchmark-nvim.sh /tmp/result.json | |
| cat /tmp/result.json | |
| - name: Measure bash startup | |
| run: | | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| cd "$HOME/dotfiles" | |
| bash .github/scripts/benchmark-bash.sh /tmp/bash-result.json | |
| cat /tmp/bash-result.json | |
| - name: Restore benchmark history | |
| uses: actions/cache/restore@v4 | |
| with: | |
| path: /tmp/cache | |
| key: ${{ runner.os }}-benchmark-history-${{ github.run_id }} | |
| restore-keys: | | |
| ${{ runner.os }}-benchmark-history- | |
| - name: Compare against rolling baseline (nvim + bash) | |
| env: | |
| RECORD: ${{ github.event_name == 'push' && '--record' || '' }} | |
| run: | | |
| cd "$HOME/dotfiles" | |
| python3 .github/scripts/compare-benchmark.py /tmp/result.json /tmp/cache 'nvim startup (macOS)' ${RECORD} | |
| python3 .github/scripts/compare-benchmark.py /tmp/bash-result.json /tmp/cache 'bash startup (macOS)' ${RECORD} | |
| - name: Save benchmark history | |
| if: github.event_name == 'push' | |
| uses: actions/cache/save@v4 | |
| with: | |
| path: /tmp/cache | |
| key: ${{ runner.os }}-benchmark-history-${{ github.run_id }} | |
| - name: Collect failure diagnostics (macOS) | |
| if: failure() | |
| run: | | |
| mkdir -p /tmp/failure-bundle | |
| cd "$HOME/dotfiles" | |
| git status --porcelain > /tmp/failure-bundle/git-status.txt 2>&1 || true | |
| git diff > /tmp/failure-bundle/git-diff.txt 2>&1 || true | |
| cp -a ~/.local/state/nvim /tmp/failure-bundle/nvim-state 2>/dev/null || true | |
| "$HOME/.pixi/bin/pixi" global list > /tmp/failure-bundle/pixi-globals.txt 2>&1 || true | |
| ls -la ~ > /tmp/failure-bundle/home-listing.txt 2>&1 || true | |
| - name: Upload failure diagnostics (macOS) | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: failure-macos-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: /tmp/failure-bundle | |
| if-no-files-found: ignore | |
| install-windows: | |
| name: Install (Windows) | |
| runs-on: windows-latest | |
| timeout-minutes: 60 | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| path: dotfiles-pr | |
| - name: Enable Developer Mode (native symlinks) | |
| shell: pwsh | |
| run: | | |
| New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" ` | |
| -Name AllowDevelopmentWithoutDevLicense -PropertyType DWord -Value 1 -Force | Out-Null | |
| Write-Host "Developer Mode enabled" | |
| - name: Stage PR repo at ~/dotfiles | |
| run: | | |
| cp -a dotfiles-pr "$HOME/dotfiles" | |
| git config --global --add safe.directory '*' | |
| git config --global core.symlinks true | |
| - name: Cache pixi globals | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.pixi | |
| key: pixi-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('dotfiles-pr/scripts/bootstrap.sh') }} | |
| - name: Cache lazy.nvim plugins | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.local/share/nvim-data/lazy | |
| key: lazy-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('dotfiles-pr/.config/nvim/lazy-lock.json') }} | |
| - name: Authenticated git for plugin clones (lift GH rate limit) | |
| if: github.actor == github.repository_owner | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| git config --global url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf "https://github.com/" | |
| - name: Run bootstrap (first run) | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: cd "$HOME/dotfiles" && ./scripts/bootstrap.sh | |
| - name: Assert first bootstrap produced no tracked-file drift | |
| run: | | |
| set -e | |
| cd "$HOME/dotfiles" | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "::error::first bootstrap dirtied tracked files" | |
| git status --porcelain | |
| git diff | |
| exit 1 | |
| fi | |
| echo "✓ first run clean" | |
| - name: Run bootstrap (idempotency check) | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: cd "$HOME/dotfiles" && ./scripts/bootstrap.sh | |
| - name: Assert second bootstrap did not modify tracked files | |
| run: | | |
| set -e | |
| cd "$HOME/dotfiles" | |
| if [ -n "$(git status --porcelain)" ]; then | |
| echo "::error::second bootstrap modified tracked files" | |
| git status --porcelain | |
| git diff | |
| exit 1 | |
| fi | |
| echo "✓ no tracked-file changes after two bootstrap runs" | |
| - name: Verify symlinks | |
| run: | | |
| set -eo pipefail | |
| test_link() { | |
| local s=$1 expected=$2 | |
| if [ -e "$s" ] && { [ -L "$s" ] || [ -d "$s" ]; }; then | |
| echo "✓ $s ($expected)" | |
| else | |
| echo "✗ $s missing or not a link" | |
| exit 1 | |
| fi | |
| } | |
| test_link ~/.bashrc ".bashrc" | |
| test_link ~/.gitignore ".gitignore" | |
| test_link ~/.config/nvim "nvim" | |
| test_link ~/.config/wezterm "wezterm" | |
| test_link ~/.config/git/config "gitconfig" | |
| test_link ~/.claude ".claude" | |
| bash -n ~/.bashrc && echo "✓ .bashrc syntax valid" | |
| - name: Verify pixi tools | |
| run: | | |
| export PATH="$HOME/.pixi/bin:$HOME/.local/bin:$PATH" | |
| set -eo pipefail | |
| nvim --version | head -1 && echo "✓ neovim" | |
| starship --version && echo "✓ starship" | |
| eza --version | head -1 && echo "✓ eza" | |
| bat --version && echo "✓ bat" | |
| fzf --version && echo "✓ fzf" | |
| rg --version | head -1 && echo "✓ ripgrep" | |
| fd --version && echo "✓ fd-find" | |
| stylua --version && echo "✓ stylua" | |
| selene --version && echo "✓ selene" | |
| jj --version && echo "✓ jujutsu" | |
| hyperfine --version && echo "✓ hyperfine" | |
| zig version && echo "✓ zig" | |
| fnm --version && echo "✓ fnm" | |
| claude --version && echo "✓ claude code" | |
| - name: Verify nvim setup | |
| run: | | |
| set -eo pipefail | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| export XDG_CONFIG_HOME="$HOME/.config" | |
| export XDG_DATA_HOME="$HOME/.local/share" | |
| if [ -d ~/.local/share/nvim-data/lazy ]; then | |
| plugin_count=$(find ~/.local/share/nvim-data/lazy -maxdepth 1 -type d | wc -l) | |
| echo "✓ Lazy plugins installed ($plugin_count)" | |
| else | |
| echo "✗ Lazy plugin directory not found" | |
| exit 1 | |
| fi | |
| PARSER_DIR="$HOME/.local/share/nvim-data/site/parser" | |
| if [ ! -d "$PARSER_DIR" ]; then | |
| echo "✗ Treesitter parser directory not found at $PARSER_DIR (zig-cc pipeline likely broken)" | |
| exit 1 | |
| fi | |
| # Parsers compile to *.so even on Windows (loaded by libuv). | |
| parser_count=$(find "$PARSER_DIR" -name '*.so' | wc -l) | |
| for p in lua c bash markdown markdown_inline json yaml; do | |
| [ -f "$PARSER_DIR/$p.so" ] || { echo "✗ missing required parser: $p.so"; exit 1; } | |
| done | |
| if [ "$parser_count" -lt 12 ]; then | |
| echo "✗ only $parser_count parsers installed (expected >=12)" | |
| exit 1 | |
| fi | |
| echo "✓ Treesitter parsers ($parser_count; lua/c/bash/markdown/json/yaml present)" | |
| if [ -d ~/.local/share/nvim-data/mason/packages ]; then | |
| mason_count=$(find ~/.local/share/nvim-data/mason/packages -maxdepth 1 -mindepth 1 -type d | wc -l) | |
| echo "✓ Mason packages ($mason_count)" | |
| else | |
| echo "⚠ Mason directory not found" | |
| fi | |
| FZF_DLL=~/.local/share/nvim-data/lazy/telescope-fzf-native.nvim/build/libfzf.dll | |
| if [ -f "$FZF_DLL" ]; then | |
| echo "✓ telescope-fzf-native built" | |
| else | |
| echo "⚠ telescope-fzf-native not built (telescope falls back to its Lua sorter; needs MSVC)" | |
| fi | |
| # Git Bash ships /usr/bin/timeout (MSYS coreutils) — use it directly. | |
| set +e | |
| output=$(timeout 30 nvim --headless -c "qa" 2>&1) | |
| status=$? | |
| set -e | |
| if [ "$status" -ne 0 ]; then | |
| echo "✗ nvim exited with status $status (124 = timeout)" | |
| echo "$output" | |
| exit 1 | |
| fi | |
| if echo "$output" | grep -qE "^E[0-9]+:|Error detected while processing|stack traceback"; then | |
| echo "✗ nvim startup output contains errors:" | |
| echo "$output" | |
| exit 1 | |
| fi | |
| echo "✓ nvim starts cleanly" | |
| - name: Collect failure diagnostics (Windows) | |
| if: failure() | |
| run: | | |
| mkdir -p /tmp/failure-bundle | |
| cd "$HOME/dotfiles" | |
| git status --porcelain > /tmp/failure-bundle/git-status.txt 2>&1 || true | |
| git diff > /tmp/failure-bundle/git-diff.txt 2>&1 || true | |
| cp -a "$HOME/.local/state/nvim-data" /tmp/failure-bundle/nvim-state 2>/dev/null || true | |
| "$HOME/.pixi/bin/pixi.exe" global list > /tmp/failure-bundle/pixi-globals.txt 2>&1 || true | |
| ls -la "$HOME" > /tmp/failure-bundle/home-listing.txt 2>&1 || true | |
| - name: Upload failure diagnostics (Windows) | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: failure-windows-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: /tmp/failure-bundle | |
| if-no-files-found: ignore | |
| - name: Test summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| export PATH="$HOME/.pixi/bin:$PATH" | |
| { | |
| echo "## Install Dotfiles — Windows" | |
| echo "" | |
| echo "| Component | Version |" | |
| echo "|-----------|---------|" | |
| echo "| pixi | $(pixi --version 2>/dev/null || echo N/A) |" | |
| echo "| nvim | $(nvim --version 2>/dev/null | head -1 || echo N/A) |" | |
| echo "| zig | $(zig version 2>/dev/null || echo N/A) |" | |
| echo "| pwsh | $(pwsh --version 2>/dev/null || echo N/A) |" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| plugin-freshness: | |
| name: Lazy.nvim plugin archive check | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| issues: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| # Pinned to commit SHA — third-party action, v1 tag is mutable. | |
| - uses: yutkat/lazy-nvim-archived-checker@37335f7b997e03ecd574641c0ee400b76932ba0e # v1 | |
| with: | |
| nvim-config-path: .config/nvim | |
| github-token: ${{ secrets.GITHUB_TOKEN }} |