From 82e289efd886dc0b611b3a1ac91394799dd156ae Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:44:50 -0400 Subject: [PATCH 01/14] ci: validate dotfiles installs on macOS and Linux --- .github/dependabot.yml | 6 ++ .github/workflows/ci.yml | 125 ++++++++++++++++++++++++++++++++ .gitignore | 10 +++ .pre-commit-config.yaml | 5 ++ Brewfile | 1 + assimilate.sh | 15 +++- bashrc | 3 + scripts/ci/verify-assimilate.sh | 45 ++++++++++++ 8 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .pre-commit-config.yaml create mode 100755 scripts/ci/verify-assimilate.sh diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b20010a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,125 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: {} + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + UV_MALWARE_CHECK: "1" + +jobs: + static-checks: + name: Static checks + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Check shell syntax + run: | + bash -n assimilate.sh bashrc bash_profile claude/statusline.sh scripts/ci/verify-assimilate.sh + zsh -n zshrc + - name: Check Git configuration + run: git config --file gitconfig --list >/dev/null + - name: Check strict JSON + run: | + jq empty nvim/coc-settings.json + jq empty vscode/code_settings.json + jq empty zed/keymap.json + + pre-commit: + name: Pre-commit + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Install pre-commit + run: python3 -m pip install --user pre-commit==4.6.2 + - name: Run repository hooks + run: python3 -m pre_commit run --all-files --show-diff-on-failure + + actions-security: + name: Actions security + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Validate workflows with actionlint + run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + - name: Audit workflows with zizmor + run: pipx run zizmor==1.29.0 . + + secret-history: + name: Secret history + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Check out full history + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install pinned Gitleaks + env: + GOBIN: ${{ runner.temp }}/bin + run: go install github.com/zricethezav/gitleaks/v8@v8.30.1 + - name: Scan Git history + run: '"${RUNNER_TEMP}/bin/gitleaks" git --redact --verbose' + + install: + name: Install (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: + - macos-26 + - ubuntu-24.04 + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Install dotfiles twice in an isolated home + env: + HOMEBREW_NO_ANALYTICS: "1" + run: | + test_home="${RUNNER_TEMP}/dotfiles-home" + mkdir -p "$test_home" + ln -s "$GITHUB_WORKSPACE" "$test_home/dotfiles" + + HOME="$test_home" ./assimilate.sh + HOME="$test_home" DOTFILES="$GITHUB_WORKSPACE" scripts/ci/verify-assimilate.sh + + HOME="$test_home" ./assimilate.sh + HOME="$test_home" DOTFILES="$GITHUB_WORKSPACE" scripts/ci/verify-assimilate.sh + + test -z "$(find "$test_home/backups" -mindepth 1 -maxdepth 1 -print -quit)" diff --git a/.gitignore b/.gitignore index d713c1d..b5f4f2b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,13 @@ nvim/plugin/ gh/ .claude .wrangler + +# Secrets and machine-local tool state +*.pem +*.key +.env +.env.* +credentials.json +*.secret +Brewfile.lock.json +homebrew/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..0f37bf7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.30.1 + hooks: + - id: gitleaks diff --git a/Brewfile b/Brewfile index 4df7c68..8054604 100644 --- a/Brewfile +++ b/Brewfile @@ -12,6 +12,7 @@ brew "neovim" brew "mosh" brew "node" brew "postgresql@17" +brew "pre-commit" brew "tmux" brew "tree" brew "wget" diff --git a/assimilate.sh b/assimilate.sh index cb94c0e..4134951 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -16,9 +16,16 @@ function sym () { # Ensure parent directory exists mkdir -p "$(dirname "$dest")" + # An already-correct link is installed. Leaving it alone makes repeated + # assimilation safe and avoids filling the backup directory on every run. + if [ -L "$dest" ] && [ "$(readlink "$dest")" = "$src" ]; then + return + fi + # Save existing dotfiles (also matches dangling symlinks, where -e alone returns false) if [ -e "$dest" ] || [ -L "$dest" ]; then - backup="$BACKUPS/$(basename $dest)-$(date +%s)" + backup="$(mktemp -d "$BACKUPS/$(basename "$dest").XXXXXX")" + rmdir "$backup" mv "$dest" "$backup" echo "> Moved $dest to $backup" fi @@ -96,6 +103,12 @@ else echo "WARN: yq or codex is unavailable — skipping Codex settings sync" >&2 fi +# Install repository security hooks when pre-commit is available (Homebrew +# supplies it on macOS). Linux users can install pre-commit independently. +if command -v pre-commit >/dev/null 2>&1; then + (cd "$DOTFILES" && pre-commit install --allow-missing-config) +fi + # Install oh-my-zsh (clone repo directly; install.sh is just `git clone` once # its zshrc/runzsh/chsh side-effects are disabled) clone_pinned https://github.com/ohmyzsh/ohmyzsh "$HOME/.oh-my-zsh" e7aa0c56e68348afefdd6af4c5bdb314a2bd6640 # 2026-04 master HEAD diff --git a/bashrc b/bashrc index 8de032c..1726056 100644 --- a/bashrc +++ b/bashrc @@ -7,6 +7,9 @@ export EDITOR='nvim' export TERM='xterm-256color' export LANG='en_US.UTF-8' +# Ask uv to check resolved packages against malicious-package advisories. +export UV_MALWARE_CHECK=1 + # ---- # Aliases diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh new file mode 100755 index 0000000..efc6cc7 --- /dev/null +++ b/scripts/ci/verify-assimilate.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +set -euo pipefail + +: "${DOTFILES:?DOTFILES must point to the checked-out repository}" + +assert_link() { + local source="$DOTFILES/$1" + local target="$HOME/$2" + + test -L "$target" + test "$(readlink "$target")" = "$source" +} + +assert_link bash_profile .bash_profile +assert_link gitconfig .gitconfig +assert_link tmux.conf .tmux.conf +assert_link zshrc .zshrc +assert_link nvim .config/nvim +assert_link tmux-powerline/config.sh .config/tmux-powerline/config.sh +assert_link tmux-powerline/themes/theme.sh .config/tmux-powerline/themes/theme.sh +assert_link claude/statusline.sh .claude/statusline.sh + +if [ "$(uname -s)" = Darwin ]; then + assert_link bashrc .bashrc + assert_link hammerspoon .hammerspoon + assert_link vscode/code_settings.json .vscode/settings.json + assert_link zed/settings.json .config/zed/settings.json + assert_link zed/keymap.json .config/zed/keymap.json + assert_link ghostty/config 'Library/Application Support/com.mitchellh.ghostty/config' +else + test ! -L "$HOME/.bashrc" + grep -Fq 'dotfiles/bashrc' "$HOME/.bashrc" +fi + +test "$(HOME="$HOME" bash -c 'source "$HOME/.bashrc" >/dev/null 2>&1; printf %s "$UV_MALWARE_CHECK"')" = 1 +test "$(HOME="$HOME" zsh -c 'source "$HOME/.zshrc" >/dev/null 2>&1; printf %s "$UV_MALWARE_CHECK"')" = 1 +test "$("$HOME/.local/bin/delta" --version)" = "delta 0.19.2" +test -x "$HOME/.cargo/bin/rustc" +test -d "$HOME/.oh-my-zsh/.git" +test -d "$HOME/.tmux/plugins/tpm/.git" +test -d "$HOME/.tmux/plugins/tmux-powerline/.git" + +git -C "$DOTFILES" diff --exit-code +git -C "$DOTFILES" diff --cached --exit-code From fbbd2412c9c1b499d8ff47e6c31481e75764cffd Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:48:50 -0400 Subject: [PATCH 02/14] ci: fix hosted runner prerequisites --- .github/dependabot.yml | 2 ++ .github/workflows/ci.yml | 19 ++++++++++++++----- assimilate.sh | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ca79ca5..9cbb9b7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,5 @@ updates: directory: / schedule: interval: weekly + cooldown: + default-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b20010a..0b7de08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,9 +25,13 @@ jobs: contents: read steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - name: Install Zsh + run: | + sudo apt-get update + sudo apt-get install --yes zsh - name: Check shell syntax run: | bash -n assimilate.sh bashrc bash_profile claude/statusline.sh scripts/ci/verify-assimilate.sh @@ -48,7 +52,7 @@ jobs: contents: read steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install pre-commit @@ -64,7 +68,7 @@ jobs: contents: read steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Validate workflows with actionlint @@ -80,7 +84,7 @@ jobs: contents: read steps: - name: Check out full history - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false @@ -105,9 +109,14 @@ jobs: contents: read steps: - name: Check out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - name: Install Linux test prerequisites + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --yes zsh - name: Install dotfiles twice in an isolated home env: HOMEBREW_NO_ANALYTICS: "1" diff --git a/assimilate.sh b/assimilate.sh index 4134951..52c5211 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -87,6 +87,8 @@ if [ "$OS" = "Darwin" ]; then sym zed/keymap.json .config/zed/keymap.json sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/config + # Homebrew 6 requires explicit trust before installing formulae from a tap. + brew trust hashicorp/tap brew bundle install fi From 640413a8a903e08900eabfa4ee409e58053afbbe Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:05:56 -0400 Subject: [PATCH 03/14] ci: preserve hosted Homebrew trust --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b7de08..275c204 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -125,6 +125,13 @@ jobs: mkdir -p "$test_home" ln -s "$GITHUB_WORKSPACE" "$test_home/dotfiles" + # GitHub's macOS image has trusted taps for its preinstalled tools. + # Preserve only that public tap metadata when isolating HOME. + if [ -f "$HOME/.homebrew/trust.json" ]; then + mkdir -p "$test_home/.homebrew" + cp "$HOME/.homebrew/trust.json" "$test_home/.homebrew/trust.json" + fi + HOME="$test_home" ./assimilate.sh HOME="$test_home" DOTFILES="$GITHUB_WORKSPACE" scripts/ci/verify-assimilate.sh From 2d5da1a32ca66e1edb0e7784b6848066cd45c657 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:15:42 -0400 Subject: [PATCH 04/14] ci: trust hosted runner AWS tap --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 275c204..969bebe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,9 @@ jobs: mkdir -p "$test_home/.homebrew" cp "$HOME/.homebrew/trust.json" "$test_home/.homebrew/trust.json" fi + if [ "$(uname -s)" = Darwin ]; then + HOME="$test_home" brew trust aws/tap + fi HOME="$test_home" ./assimilate.sh HOME="$test_home" DOTFILES="$GITHUB_WORKSPACE" scripts/ci/verify-assimilate.sh From 05e7d7c71c8e19cc7151bcd07d9850d161c8b5a4 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:18:17 -0400 Subject: [PATCH 05/14] ci: use hosted runner home for integration --- .github/workflows/ci.yml | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 969bebe..9f79854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,28 +117,21 @@ jobs: run: | sudo apt-get update sudo apt-get install --yes zsh - - name: Install dotfiles twice in an isolated home + - name: Install dotfiles twice on hosted runner env: HOMEBREW_NO_ANALYTICS: "1" run: | - test_home="${RUNNER_TEMP}/dotfiles-home" - mkdir -p "$test_home" - ln -s "$GITHUB_WORKSPACE" "$test_home/dotfiles" + # The hosted VM and its home directory are discarded after this job. + # Keep the runner's package-manager state while honoring assimilate's + # documented requirement that the repository live at $HOME/dotfiles. + test ! -e "$HOME/dotfiles" + ln -s "$GITHUB_WORKSPACE" "$HOME/dotfiles" - # GitHub's macOS image has trusted taps for its preinstalled tools. - # Preserve only that public tap metadata when isolating HOME. - if [ -f "$HOME/.homebrew/trust.json" ]; then - mkdir -p "$test_home/.homebrew" - cp "$HOME/.homebrew/trust.json" "$test_home/.homebrew/trust.json" - fi - if [ "$(uname -s)" = Darwin ]; then - HOME="$test_home" brew trust aws/tap - fi + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + backup_count="$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" - HOME="$test_home" ./assimilate.sh - HOME="$test_home" DOTFILES="$GITHUB_WORKSPACE" scripts/ci/verify-assimilate.sh + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh - HOME="$test_home" ./assimilate.sh - HOME="$test_home" DOTFILES="$GITHUB_WORKSPACE" scripts/ci/verify-assimilate.sh - - test -z "$(find "$test_home/backups" -mindepth 1 -maxdepth 1 -print -quit)" + test "$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" = "$backup_count" From bc95df8b8da6d61999af887a06f7415920c00a7b Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:36:20 -0400 Subject: [PATCH 06/14] ci: test clean VPS installs --- .github/workflows/ci.yml | 26 +++++++++- scripts/ci/run-vps-container.sh | 82 ++++++++++++++++++++++++++++++++ scripts/ci/test-vps-container.sh | 14 ++++++ scripts/ci/verify-assimilate.sh | 14 ++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100755 scripts/ci/run-vps-container.sh create mode 100755 scripts/ci/test-vps-container.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f79854..8b19b89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,10 @@ jobs: sudo apt-get install --yes zsh - name: Check shell syntax run: | - bash -n assimilate.sh bashrc bash_profile claude/statusline.sh scripts/ci/verify-assimilate.sh + bash -n assimilate.sh bashrc bash_profile claude/statusline.sh scripts/ci/*.sh zsh -n zshrc + - name: Test VPS container harness + run: scripts/ci/test-vps-container.sh - name: Check Git configuration run: git config --file gitconfig --list >/dev/null - name: Check strict JSON @@ -135,3 +137,25 @@ jobs: DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh test "$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" = "$backup_count" + + vps-install: + name: VPS install (${{ matrix.target }}) + strategy: + fail-fast: false + matrix: + target: + - ubuntu + - amazon-linux + runs-on: ubuntu-24.04 + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Install dotfiles twice in a minimal VPS image + env: + VPS_TARGET: ${{ matrix.target }} + run: scripts/ci/run-vps-container.sh run "$VPS_TARGET" diff --git a/scripts/ci/run-vps-container.sh b/scripts/ci/run-vps-container.sh new file mode 100755 index 0000000..e034f71 --- /dev/null +++ b/scripts/ci/run-vps-container.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + echo "usage: $0 image|run ubuntu|amazon-linux" >&2 + exit 2 +} + +image_for() { + case "$1" in + ubuntu) + echo "ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea" + ;; + amazon-linux) + echo "amazonlinux:2023@sha256:694092ae18877ed4e3cb9b643759ba95df1f12af12528fefa18f60f79d4c1568" + ;; + *) + echo "unsupported VPS target: $1" >&2 + return 2 + ;; + esac +} + +command="${1:-}" +target="${2:-}" +[ "$#" -eq 2 ] || usage + +image="$(image_for "$target")" + +if [ "$command" = image ]; then + echo "$image" + exit 0 +fi + +[ "$command" = run ] || usage + +repo_root="$(git rev-parse --show-toplevel)" + +docker run --rm \ + --platform linux/amd64 \ + --env VPS_TARGET="$target" \ + --volume "$repo_root:/workspace:ro" \ + "$image" \ + bash -s <<'CONTAINER' +set -euxo pipefail + +case "$VPS_TARGET" in + ubuntu) + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install --yes ca-certificates curl findutils git gzip passwd tar zsh + ;; + amazon-linux) + dnf install --assumeyes ca-certificates curl findutils git gzip shadow-utils tar util-linux zsh + ;; + *) + echo "unsupported VPS target: $VPS_TARGET" >&2 + exit 2 + ;; +esac + +useradd --create-home --shell /bin/bash dotfiles +cp -a /workspace /home/dotfiles/dotfiles +chown -R dotfiles:dotfiles /home/dotfiles/dotfiles + +runuser --user dotfiles -- env \ + HOME=/home/dotfiles \ + PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ + bash -c ' + set -euo pipefail + cd "$HOME/dotfiles" + + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + backup_count="$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d " ")" + + ./assimilate.sh + DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + test "$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d " ")" = "$backup_count" + ' +CONTAINER diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh new file mode 100755 index 0000000..d2f0c14 --- /dev/null +++ b/scripts/ci/test-vps-container.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -euo pipefail + +runner="scripts/ci/run-vps-container.sh" + +test -x "$runner" +test "$("$runner" image ubuntu)" = "ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea" +test "$("$runner" image amazon-linux)" = "amazonlinux:2023@sha256:694092ae18877ed4e3cb9b643759ba95df1f12af12528fefa18f60f79d4c1568" + +if "$runner" image alpine >/dev/null 2>&1; then + echo "unsupported VPS target unexpectedly succeeded" >&2 + exit 1 +fi diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh index efc6cc7..540cc84 100755 --- a/scripts/ci/verify-assimilate.sh +++ b/scripts/ci/verify-assimilate.sh @@ -40,6 +40,20 @@ test -x "$HOME/.cargo/bin/rustc" test -d "$HOME/.oh-my-zsh/.git" test -d "$HOME/.tmux/plugins/tpm/.git" test -d "$HOME/.tmux/plugins/tmux-powerline/.git" +test -d "$HOME/.local/share/nvim/site/pack/packer/start/packer.nvim/.git" + +nvim_log="$(mktemp)" +if ! HOME="$HOME" nvim --headless -c 'quitall' >"$nvim_log" 2>&1; then + cat "$nvim_log" >&2 + rm -f "$nvim_log" + exit 1 +fi +if grep -Fq 'Error detected while processing' "$nvim_log"; then + cat "$nvim_log" >&2 + rm -f "$nvim_log" + exit 1 +fi +rm -f "$nvim_log" git -C "$DOTFILES" diff --exit-code git -C "$DOTFILES" diff --cached --exit-code From e9faa06eab2c6cee9e42c65251a2a5a4a5cf0a45 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:38:51 -0400 Subject: [PATCH 07/14] ci: attach VPS container input --- scripts/ci/run-vps-container.sh | 2 +- scripts/ci/test-vps-container.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/run-vps-container.sh b/scripts/ci/run-vps-container.sh index e034f71..e7c67c6 100755 --- a/scripts/ci/run-vps-container.sh +++ b/scripts/ci/run-vps-container.sh @@ -37,7 +37,7 @@ fi repo_root="$(git rev-parse --show-toplevel)" -docker run --rm \ +docker run --rm --interactive \ --platform linux/amd64 \ --env VPS_TARGET="$target" \ --volume "$repo_root:/workspace:ro" \ diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh index d2f0c14..94003c9 100755 --- a/scripts/ci/test-vps-container.sh +++ b/scripts/ci/test-vps-container.sh @@ -7,6 +7,7 @@ runner="scripts/ci/run-vps-container.sh" test -x "$runner" test "$("$runner" image ubuntu)" = "ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea" test "$("$runner" image amazon-linux)" = "amazonlinux:2023@sha256:694092ae18877ed4e3cb9b643759ba95df1f12af12528fefa18f60f79d4c1568" +grep -Fq -- '--interactive' "$runner" if "$runner" image alpine >/dev/null 2>&1; then echo "unsupported VPS target unexpectedly succeeded" >&2 From 0810212b60d954cda621d0f4425a8de6199ca1ad Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:42:52 -0400 Subject: [PATCH 08/14] fix: support clean Linux bootstrap --- assimilate.sh | 20 ++++++++++++-------- scripts/ci/run-vps-container.sh | 2 +- scripts/ci/test-vps-container.sh | 5 +++++ scripts/ci/verify-assimilate.sh | 1 + 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/assimilate.sh b/assimilate.sh index 52c5211..43b5a88 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -ex +set -euxo pipefail OS="$(uname -s)" ARCH="$(uname -m)" @@ -118,7 +118,7 @@ clone_pinned https://github.com/ohmyzsh/ohmyzsh "$HOME/.oh-my-zsh" e7aa0c56e6834 # Install rust via rustup-init.sh pinned to a specific GitHub commit (immutable), # with rustc toolchain version locked if [ ! -d "$HOME/.cargo" ]; then - RUSTUP_SHA=e10ffbdbb807c47fdd208119de99e7baae3e0dfe # rustup 1.29.0 + RUSTUP_SHA=28d1352dbcb436d3111c3594b9e1588e94950464 # rustup 1.29.0 tag's commit curl --proto '=https' --tlsv1.2 -sSf \ "https://raw.githubusercontent.com/rust-lang/rustup/$RUSTUP_SHA/rustup-init.sh" \ | sh -s -- -y --no-modify-path --default-toolchain 1.95.0 @@ -169,11 +169,12 @@ clone_pinned https://github.com/erikw/tmux-powerline "$HOME/.tmux/plugins/tmux-p # Install neovim on Linux from a pinned upstream tarball (macOS gets it via Brewfile). # AL2023 doesn't ship neovim in its default dnf repos. Lands in $HOME/.local so no # root needed, and runs before the PackerSync block below so the plugin sync works. -NVIM_VERSION=0.9.5 -NVIM_SHA256=44ee395d9b5f8a14be8ec00d3b8ead34e18fe6461e40c9c8c50e6956d643b6ca -if [ "$OS" = "Linux" ] && [ "$ARCH" = "x86_64" ] && ! command -v nvim >/dev/null; then +NVIM_VERSION=0.12.4 +NVIM_SHA256=012bf3fcac5ade43914df3f174668bf64d05e049a4f032a388c027b1ebd78628 +NVIM_BIN="$HOME/.local/bin/nvim" +if [ "$OS" = "Linux" ] && [ "$ARCH" = "x86_64" ] && { [ ! -x "$NVIM_BIN" ] || [ "$("$NVIM_BIN" --version | head -n 1)" != "NVIM v${NVIM_VERSION}" ]; }; then tmp=$(mktemp -d) - curl -fsSL "https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-linux64.tar.gz" -o "$tmp/nvim.tar.gz" + curl -fsSL "https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-linux-x86_64.tar.gz" -o "$tmp/nvim.tar.gz" if command -v sha256sum >/dev/null; then echo "${NVIM_SHA256} $tmp/nvim.tar.gz" | sha256sum -c - else @@ -181,9 +182,12 @@ if [ "$OS" = "Linux" ] && [ "$ARCH" = "x86_64" ] && ! command -v nvim >/dev/null fi mkdir -p "$HOME/.local/share" "$HOME/.local/bin" tar -xzf "$tmp/nvim.tar.gz" -C "$HOME/.local/share" - ln -sf "$HOME/.local/share/nvim-linux64/bin/nvim" "$HOME/.local/bin/nvim" + ln -sf "$HOME/.local/share/nvim-linux-x86_64/bin/nvim" "$NVIM_BIN" rm -rf "$tmp" - # Make nvim visible to the rest of this script (PackerSync below) +fi + +if [ "$OS" = "Linux" ] && [ "$ARCH" = "x86_64" ] && [ -x "$NVIM_BIN" ]; then + # Prefer the pinned nvim for PackerSync, even when the host has another version. export PATH="$HOME/.local/bin:$PATH" fi diff --git a/scripts/ci/run-vps-container.sh b/scripts/ci/run-vps-container.sh index e7c67c6..484b884 100755 --- a/scripts/ci/run-vps-container.sh +++ b/scripts/ci/run-vps-container.sh @@ -52,7 +52,7 @@ case "$VPS_TARGET" in apt-get install --yes ca-certificates curl findutils git gzip passwd tar zsh ;; amazon-linux) - dnf install --assumeyes ca-certificates curl findutils git gzip shadow-utils tar util-linux zsh + dnf install --assumeyes ca-certificates curl-minimal findutils git gzip shadow-utils tar util-linux zsh ;; *) echo "unsupported VPS target: $VPS_TARGET" >&2 diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh index 94003c9..da94c26 100755 --- a/scripts/ci/test-vps-container.sh +++ b/scripts/ci/test-vps-container.sh @@ -8,6 +8,11 @@ test -x "$runner" test "$("$runner" image ubuntu)" = "ubuntu:24.04@sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea" test "$("$runner" image amazon-linux)" = "amazonlinux:2023@sha256:694092ae18877ed4e3cb9b643759ba95df1f12af12528fefa18f60f79d4c1568" grep -Fq -- '--interactive' "$runner" +grep -Fq 'curl-minimal' "$runner" + +grep -Fq 'set -euxo pipefail' assimilate.sh +grep -Fq '28d1352dbcb436d3111c3594b9e1588e94950464' assimilate.sh +grep -Fq 'NVIM_VERSION=0.12.4' assimilate.sh if "$runner" image alpine >/dev/null 2>&1; then echo "unsupported VPS target unexpectedly succeeded" >&2 diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh index 540cc84..642bcc8 100755 --- a/scripts/ci/verify-assimilate.sh +++ b/scripts/ci/verify-assimilate.sh @@ -3,6 +3,7 @@ set -euo pipefail : "${DOTFILES:?DOTFILES must point to the checked-out repository}" +export PATH="$HOME/.local/bin:$PATH" assert_link() { local source="$DOTFILES/$1" From c0007043a3f085b86b348e454427eef42b1e1c26 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:44:57 -0400 Subject: [PATCH 09/14] test: verify interactive VPS shell --- scripts/ci/test-vps-container.sh | 2 ++ scripts/ci/verify-assimilate.sh | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh index da94c26..2229bd8 100755 --- a/scripts/ci/test-vps-container.sh +++ b/scripts/ci/test-vps-container.sh @@ -13,6 +13,8 @@ grep -Fq 'curl-minimal' "$runner" grep -Fq 'set -euxo pipefail' assimilate.sh grep -Fq '28d1352dbcb436d3111c3594b9e1588e94950464' assimilate.sh grep -Fq 'NVIM_VERSION=0.12.4' assimilate.sh +grep -Fq 'set -euxo pipefail' scripts/ci/verify-assimilate.sh +grep -Fq 'bash --noprofile --norc -ic' scripts/ci/verify-assimilate.sh if "$runner" image alpine >/dev/null 2>&1; then echo "unsupported VPS target unexpectedly succeeded" >&2 diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh index 642bcc8..b3a61b6 100755 --- a/scripts/ci/verify-assimilate.sh +++ b/scripts/ci/verify-assimilate.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -euo pipefail +set -euxo pipefail : "${DOTFILES:?DOTFILES must point to the checked-out repository}" export PATH="$HOME/.local/bin:$PATH" @@ -34,7 +34,7 @@ else grep -Fq 'dotfiles/bashrc' "$HOME/.bashrc" fi -test "$(HOME="$HOME" bash -c 'source "$HOME/.bashrc" >/dev/null 2>&1; printf %s "$UV_MALWARE_CHECK"')" = 1 +test "$(HOME="$HOME" bash --noprofile --norc -ic 'printf %s "$UV_MALWARE_CHECK"' 2>/dev/null)" = 1 test "$(HOME="$HOME" zsh -c 'source "$HOME/.zshrc" >/dev/null 2>&1; printf %s "$UV_MALWARE_CHECK"')" = 1 test "$("$HOME/.local/bin/delta" --version)" = "delta 0.19.2" test -x "$HOME/.cargo/bin/rustc" From 67737f26bbf8a27f64ea068c067c9e098fbc39a2 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:46:59 -0400 Subject: [PATCH 10/14] test: load VPS bashrc --- scripts/ci/test-vps-container.sh | 6 +++++- scripts/ci/verify-assimilate.sh | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh index 2229bd8..ba97ca3 100755 --- a/scripts/ci/test-vps-container.sh +++ b/scripts/ci/test-vps-container.sh @@ -14,7 +14,11 @@ grep -Fq 'set -euxo pipefail' assimilate.sh grep -Fq '28d1352dbcb436d3111c3594b9e1588e94950464' assimilate.sh grep -Fq 'NVIM_VERSION=0.12.4' assimilate.sh grep -Fq 'set -euxo pipefail' scripts/ci/verify-assimilate.sh -grep -Fq 'bash --noprofile --norc -ic' scripts/ci/verify-assimilate.sh +grep -Fq 'bash --noprofile -ic' scripts/ci/verify-assimilate.sh +if grep -Fq -- '--norc' scripts/ci/verify-assimilate.sh; then + echo "VPS shell verification must not disable .bashrc" >&2 + exit 1 +fi if "$runner" image alpine >/dev/null 2>&1; then echo "unsupported VPS target unexpectedly succeeded" >&2 diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh index b3a61b6..28fcd32 100755 --- a/scripts/ci/verify-assimilate.sh +++ b/scripts/ci/verify-assimilate.sh @@ -34,7 +34,7 @@ else grep -Fq 'dotfiles/bashrc' "$HOME/.bashrc" fi -test "$(HOME="$HOME" bash --noprofile --norc -ic 'printf %s "$UV_MALWARE_CHECK"' 2>/dev/null)" = 1 +test "$(HOME="$HOME" bash --noprofile -ic 'printf %s "$UV_MALWARE_CHECK"' 2>/dev/null)" = 1 test "$(HOME="$HOME" zsh -c 'source "$HOME/.zshrc" >/dev/null 2>&1; printf %s "$UV_MALWARE_CHECK"')" = 1 test "$("$HOME/.local/bin/delta" --version)" = "delta 0.19.2" test -x "$HOME/.cargo/bin/rustc" From 0d3ea0b5151a0204adea4c0292b606f56093ea7b Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:48:56 -0400 Subject: [PATCH 11/14] test: verify interactive VPS zsh --- scripts/ci/test-vps-container.sh | 1 + scripts/ci/verify-assimilate.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh index ba97ca3..37ea4a8 100755 --- a/scripts/ci/test-vps-container.sh +++ b/scripts/ci/test-vps-container.sh @@ -15,6 +15,7 @@ grep -Fq '28d1352dbcb436d3111c3594b9e1588e94950464' assimilate.sh grep -Fq 'NVIM_VERSION=0.12.4' assimilate.sh grep -Fq 'set -euxo pipefail' scripts/ci/verify-assimilate.sh grep -Fq 'bash --noprofile -ic' scripts/ci/verify-assimilate.sh +grep -Fq "zsh -ic 'printf" scripts/ci/verify-assimilate.sh if grep -Fq -- '--norc' scripts/ci/verify-assimilate.sh; then echo "VPS shell verification must not disable .bashrc" >&2 exit 1 diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh index 28fcd32..04006bd 100755 --- a/scripts/ci/verify-assimilate.sh +++ b/scripts/ci/verify-assimilate.sh @@ -35,7 +35,7 @@ else fi test "$(HOME="$HOME" bash --noprofile -ic 'printf %s "$UV_MALWARE_CHECK"' 2>/dev/null)" = 1 -test "$(HOME="$HOME" zsh -c 'source "$HOME/.zshrc" >/dev/null 2>&1; printf %s "$UV_MALWARE_CHECK"')" = 1 +test "$(HOME="$HOME" zsh -ic 'printf %s "$UV_MALWARE_CHECK"' 2>/dev/null)" = 1 test "$("$HOME/.local/bin/delta" --version)" = "delta 0.19.2" test -x "$HOME/.cargo/bin/rustc" test -d "$HOME/.oh-my-zsh/.git" From 51edac61c9720e24fbac97e46ac60b1c39cf95ca Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:11:45 -0400 Subject: [PATCH 12/14] test: ignore shell startup output --- scripts/ci/test-vps-container.sh | 4 ++-- scripts/ci/verify-assimilate.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh index 37ea4a8..8ed261a 100755 --- a/scripts/ci/test-vps-container.sh +++ b/scripts/ci/test-vps-container.sh @@ -14,8 +14,8 @@ grep -Fq 'set -euxo pipefail' assimilate.sh grep -Fq '28d1352dbcb436d3111c3594b9e1588e94950464' assimilate.sh grep -Fq 'NVIM_VERSION=0.12.4' assimilate.sh grep -Fq 'set -euxo pipefail' scripts/ci/verify-assimilate.sh -grep -Fq 'bash --noprofile -ic' scripts/ci/verify-assimilate.sh -grep -Fq "zsh -ic 'printf" scripts/ci/verify-assimilate.sh +grep -Fq "bash --noprofile -ic 'test \"\$UV_MALWARE_CHECK\" = 1'" scripts/ci/verify-assimilate.sh +grep -Fq "zsh -ic 'test \"\$UV_MALWARE_CHECK\" = 1'" scripts/ci/verify-assimilate.sh if grep -Fq -- '--norc' scripts/ci/verify-assimilate.sh; then echo "VPS shell verification must not disable .bashrc" >&2 exit 1 diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh index 04006bd..610d9e1 100755 --- a/scripts/ci/verify-assimilate.sh +++ b/scripts/ci/verify-assimilate.sh @@ -34,8 +34,8 @@ else grep -Fq 'dotfiles/bashrc' "$HOME/.bashrc" fi -test "$(HOME="$HOME" bash --noprofile -ic 'printf %s "$UV_MALWARE_CHECK"' 2>/dev/null)" = 1 -test "$(HOME="$HOME" zsh -ic 'printf %s "$UV_MALWARE_CHECK"' 2>/dev/null)" = 1 +HOME="$HOME" bash --noprofile -ic 'test "$UV_MALWARE_CHECK" = 1' >/dev/null 2>&1 +HOME="$HOME" zsh -ic 'test "$UV_MALWARE_CHECK" = 1' >/dev/null 2>&1 test "$("$HOME/.local/bin/delta" --version)" = "delta 0.19.2" test -x "$HOME/.cargo/bin/rustc" test -d "$HOME/.oh-my-zsh/.git" From 89975f28e7b46644987671c8f225dc7ce8943f2d Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:41:13 -0400 Subject: [PATCH 13/14] Fix CI checks for resolved paths and optional JSON files --- .github/workflows/ci.yml | 5 ++--- scripts/ci/verify-assimilate.sh | 8 ++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b19b89..d327a9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,8 @@ jobs: run: git config --file gitconfig --list >/dev/null - name: Check strict JSON run: | - jq empty nvim/coc-settings.json - jq empty vscode/code_settings.json - jq empty zed/keymap.json + find nvim -maxdepth 1 -type f -name '*.json' -exec jq empty {} + + jq empty vscode/code_settings.json zed/keymap.json pre-commit: name: Pre-commit diff --git a/scripts/ci/verify-assimilate.sh b/scripts/ci/verify-assimilate.sh index 610d9e1..af64441 100755 --- a/scripts/ci/verify-assimilate.sh +++ b/scripts/ci/verify-assimilate.sh @@ -10,7 +10,7 @@ assert_link() { local target="$HOME/$2" test -L "$target" - test "$(readlink "$target")" = "$source" + test "$(realpath "$target")" = "$(realpath "$source")" } assert_link bash_profile .bash_profile @@ -41,7 +41,11 @@ test -x "$HOME/.cargo/bin/rustc" test -d "$HOME/.oh-my-zsh/.git" test -d "$HOME/.tmux/plugins/tpm/.git" test -d "$HOME/.tmux/plugins/tmux-powerline/.git" -test -d "$HOME/.local/share/nvim/site/pack/packer/start/packer.nvim/.git" +if [ -f "$DOTFILES/nvim/lazy-lock.json" ]; then + test -d "$HOME/.local/share/nvim/lazy/lazy.nvim/.git" +else + test -d "$HOME/.local/share/nvim/site/pack/packer/start/packer.nvim/.git" +fi nvim_log="$(mktemp)" if ! HOME="$HOME" nvim --headless -c 'quitall' >"$nvim_log" 2>&1; then From 0c51c44e50abcd3c0ad4bbef908fd63f7fbe3c8f Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Sun, 23 Aug 2026 09:56:21 -0400 Subject: [PATCH 14/14] Speed up repeated CI installs --- .github/workflows/ci.yml | 21 +++++++++++++--- assimilate.sh | 27 +++++++++++++++++--- scripts/ci/run-vps-container.sh | 8 +++++- scripts/ci/test-vps-container.sh | 5 ++++ tests/clone-pinned.sh | 42 ++++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 tests/clone-pinned.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d327a9f..853e943 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,10 +34,12 @@ jobs: sudo apt-get install --yes zsh - name: Check shell syntax run: | - bash -n assimilate.sh bashrc bash_profile claude/statusline.sh scripts/ci/*.sh + bash -n assimilate.sh bashrc bash_profile claude/statusline.sh scripts/ci/*.sh tests/*.sh zsh -n zshrc - name: Test VPS container harness - run: scripts/ci/test-vps-container.sh + run: | + scripts/ci/test-vps-container.sh + bash tests/clone-pinned.sh - name: Check Git configuration run: git config --file gitconfig --list >/dev/null - name: Check strict JSON @@ -128,15 +130,28 @@ jobs: test ! -e "$HOME/dotfiles" ln -s "$GITHUB_WORKSPACE" "$HOME/dotfiles" + first_started=$SECONDS ./assimilate.sh DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + first_seconds=$((SECONDS - first_started)) backup_count="$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" - ./assimilate.sh + second_started=$SECONDS + ASSIMILATE_SKIP_PLUGIN_SYNC=1 ./assimilate.sh DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + second_seconds=$((SECONDS - second_started)) test "$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')" = "$backup_count" + { + echo "### Install timing (${{ matrix.os }})" + echo + echo "| Pass | Seconds |" + echo "| --- | ---: |" + echo "| Clean install | $first_seconds |" + echo "| Idempotency | $second_seconds |" + } >> "$GITHUB_STEP_SUMMARY" + vps-install: name: VPS install (${{ matrix.target }}) strategy: diff --git a/assimilate.sh b/assimilate.sh index 43b5a88..daecd3c 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -36,6 +36,17 @@ function sym () { function clone_pinned () { url="$1"; dir="$2"; sha="$3" + current_sha="" + if [ -d "$dir/.git" ]; then + current_sha="$(git -C "$dir" rev-parse --verify HEAD 2>/dev/null || true)" + fi + + # The requested content is already installed. In particular, this avoids + # repeating four network fetches during CI's second idempotency pass. + if [ "$current_sha" = "$sha" ]; then + return + fi + # Fetch only the pinned commit, no full history. Avoids `git clone --revision` # (git >= 2.49) so this works on older git too, e.g. AL2023's 2.40 on stale AMIs. # Fetch-by-SHA relies on the server allowing reachable-SHA1-in-want (GitHub does). @@ -88,8 +99,12 @@ if [ "$OS" = "Darwin" ]; then sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/config # Homebrew 6 requires explicit trust before installing formulae from a tap. - brew trust hashicorp/tap - brew bundle install + # Skip bundle resolution when everything is already present, which makes the + # second CI install substantially cheaper without caching runner state. + if ! brew bundle check --quiet --no-upgrade --file="$DOTFILES/Brewfile"; then + brew trust hashicorp/tap + brew bundle install --no-upgrade --file="$DOTFILES/Brewfile" + fi fi # Sync portable agent settings after Homebrew has supplied jq, yq, and Codex on @@ -191,11 +206,15 @@ if [ "$OS" = "Linux" ] && [ "$ARCH" = "x86_64" ] && [ -x "$NVIM_BIN" ]; then export PATH="$HOME/.local/bin:$PATH" fi -# Install Packer (nvim plugin manager) and run PackerSync — only if nvim is available +# Install Packer (nvim plugin manager) and run PackerSync — only if nvim is available. +# CI skips only this update operation on its second pass; the verification that +# follows still starts Neovim and checks the installed plugin manager. if command -v nvim >/dev/null; then clone_pinned https://github.com/wbthomason/packer.nvim "$HOME/.local/share/nvim/site/pack/packer/start/packer.nvim" ea0cc3c59f67c440c5ff0bbe4fb9420f4350b9a3 # 2023-08-24, matches plugins.lua pin - nvim --headless -c 'autocmd User PackerComplete quitall' -c 'PackerSync' || true + if [ "${ASSIMILATE_SKIP_PLUGIN_SYNC:-0}" != 1 ]; then + nvim --headless -c 'autocmd User PackerComplete quitall' -c 'PackerSync' || true + fi fi echo "> Assimilation successful!" diff --git a/scripts/ci/run-vps-container.sh b/scripts/ci/run-vps-container.sh index 484b884..0c8d8de 100755 --- a/scripts/ci/run-vps-container.sh +++ b/scripts/ci/run-vps-container.sh @@ -71,12 +71,18 @@ runuser --user dotfiles -- env \ set -euo pipefail cd "$HOME/dotfiles" + first_started=$SECONDS ./assimilate.sh DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + first_seconds=$((SECONDS - first_started)) backup_count="$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d " ")" - ./assimilate.sh + second_started=$SECONDS + ASSIMILATE_SKIP_PLUGIN_SYNC=1 ./assimilate.sh DOTFILES="$HOME/dotfiles" scripts/ci/verify-assimilate.sh + second_seconds=$((SECONDS - second_started)) test "$(find "$HOME/backups" -mindepth 1 -maxdepth 1 | wc -l | tr -d " ")" = "$backup_count" + + echo "> Install timing: clean=${first_seconds}s idempotency=${second_seconds}s" ' CONTAINER diff --git a/scripts/ci/test-vps-container.sh b/scripts/ci/test-vps-container.sh index 8ed261a..a4c506d 100755 --- a/scripts/ci/test-vps-container.sh +++ b/scripts/ci/test-vps-container.sh @@ -13,6 +13,11 @@ grep -Fq 'curl-minimal' "$runner" grep -Fq 'set -euxo pipefail' assimilate.sh grep -Fq '28d1352dbcb436d3111c3594b9e1588e94950464' assimilate.sh grep -Fq 'NVIM_VERSION=0.12.4' assimilate.sh +grep -Fq 'current_sha="$(git -C "$dir" rev-parse --verify HEAD 2>/dev/null || true)"' assimilate.sh +grep -Fq 'brew bundle check --quiet --no-upgrade --file="$DOTFILES/Brewfile"' assimilate.sh +grep -Fq 'ASSIMILATE_SKIP_PLUGIN_SYNC' assimilate.sh +grep -Fq 'ASSIMILATE_SKIP_PLUGIN_SYNC=1 ./assimilate.sh' "$runner" +grep -Fq 'ASSIMILATE_SKIP_PLUGIN_SYNC=1 ./assimilate.sh' .github/workflows/ci.yml grep -Fq 'set -euxo pipefail' scripts/ci/verify-assimilate.sh grep -Fq "bash --noprofile -ic 'test \"\$UV_MALWARE_CHECK\" = 1'" scripts/ci/verify-assimilate.sh grep -Fq "zsh -ic 'test \"\$UV_MALWARE_CHECK\" = 1'" scripts/ci/verify-assimilate.sh diff --git a/tests/clone-pinned.sh b/tests/clone-pinned.sh new file mode 100644 index 0000000..3219d19 --- /dev/null +++ b/tests/clone-pinned.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +test_root="$(mktemp -d "${TMPDIR:-/tmp}/clone-pinned.XXXXXX")" + +cleanup() { + rm -rf "$test_root" +} +trap cleanup EXIT HUP INT TERM + +# Load only the helper under test without executing the installer. +eval "$(sed -n '/^function clone_pinned () {/,/^}/p' "$repo_root/assimilate.sh")" + +source_repo="$test_root/source" +remote_repo="$test_root/remote.git" +install_dir="$test_root/install" + +git init --quiet "$source_repo" +git -C "$source_repo" config user.email ci@example.invalid +git -C "$source_repo" config user.name CI +git -C "$source_repo" config commit.gpgsign false +printf 'pinned content\n' > "$source_repo/content" +git -C "$source_repo" add content +git -C "$source_repo" commit --quiet -m fixture +sha="$(git -C "$source_repo" rev-parse HEAD)" + +git init --quiet --bare "$remote_repo" +git -C "$source_repo" remote add origin "$remote_repo" +git -C "$source_repo" push --quiet origin HEAD:main + +clone_pinned "$remote_repo" "$install_dir" "$sha" +test "$(git -C "$install_dir" rev-parse HEAD)" = "$sha" + +# If the second call attempts a fetch, moving the remote makes it fail. A +# successful call therefore proves that an already-pinned checkout is offline. +mv "$remote_repo" "$test_root/remote-offline.git" +clone_pinned "$remote_repo" "$install_dir" "$sha" +test "$(git -C "$install_dir" rev-parse HEAD)" = "$sha" + +echo "clone_pinned skips network access for installed commits"