Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
175 changes: 175 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
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@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/*.sh tests/*.sh
zsh -n zshrc
- name: Test VPS container harness
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
run: |
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
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
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@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 on hosted runner
env:
HOMEBREW_NO_ANALYTICS: "1"
run: |
# 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"

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 ' ')"

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:
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"
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
1 change: 1 addition & 0 deletions Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ brew "neovim"
brew "mosh"
brew "node"
brew "postgresql@17"
brew "pre-commit"
brew "tmux"
brew "tree"
brew "wget"
Expand Down
62 changes: 50 additions & 12 deletions assimilate.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash

set -ex
set -euxo pipefail

OS="$(uname -s)"
ARCH="$(uname -m)"
Expand All @@ -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
Expand All @@ -29,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).
Expand Down Expand Up @@ -80,7 +98,13 @@ if [ "$OS" = "Darwin" ]; then
sym zed/keymap.json .config/zed/keymap.json
sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/config

brew bundle install
# Homebrew 6 requires explicit trust before installing formulae from a tap.
# 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
Expand All @@ -96,14 +120,20 @@ 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

# 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
Expand Down Expand Up @@ -154,29 +184,37 @@ 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
echo "${NVIM_SHA256} $tmp/nvim.tar.gz" | shasum -a 256 -c -
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

# 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!"
3 changes: 3 additions & 0 deletions bashrc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading