Skip to content
Merged
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
138 changes: 138 additions & 0 deletions .github/workflows/test-install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: Test Installation

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:

jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run ShellCheck
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
chmod +x scripts/test-shellcheck.sh
./scripts/test-shellcheck.sh

test-macos:
name: Test on macOS
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run installation (dry-run)
run: |
chmod +x install.sh
./install.sh --dry-run --non-interactive

- name: Display system info
run: |
echo "=== System Information ==="
sw_vers
echo "=== Homebrew ==="
which brew || echo "Homebrew not found"
echo "=== Shell ==="
echo $SHELL

test-ubuntu:
name: Test on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y curl git

- name: Run installation (dry-run)
run: |
chmod +x install.sh
./install.sh --dry-run --non-interactive

- name: Display system info
run: |
echo "=== System Information ==="
lsb_release -a
echo "=== Shell ==="
echo $SHELL

test-ubuntu-full:
name: Test Full Installation on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y curl git stow

- name: Run full installation
run: |
chmod +x install.sh
# Skip components that require user interaction or are macOS-only
./install.sh --non-interactive --skip homebrew,vscode,fonts,macos-defaults

- name: Run integration tests
continue-on-error: true # Don't fail the job on integration test failures for now

Copilot AI Feb 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With continue-on-error: true, CI will report success even when integration tests fail, which undercuts the purpose of adding the test suite in Phase 9. If the tests are expected to be flaky/partial, consider tightening the suite or marking specific checks as skippable, but keep the step failing the job (or split into required vs. informational test steps).

Suggested change
continue-on-error: true # Don't fail the job on integration test failures for now

Copilot uses AI. Check for mistakes.
run: |
chmod +x scripts/test-integration.sh
./scripts/test-integration.sh

Comment on lines +89 to +93

Copilot AI Feb 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With continue-on-error: true, integration test failures won’t fail the workflow, which reduces the value of adding the test suite (regressions can merge unnoticed). Consider removing continue-on-error, or alternatively keep it but add a follow-up step that parses the result/report and fails the job for main/develop while still allowing PRs to be non-blocking if that’s the intent.

Suggested change
continue-on-error: true # Don't fail the job on integration test failures for now
run: |
chmod +x scripts/test-integration.sh
./scripts/test-integration.sh
id: integration-tests
continue-on-error: true # Allow integration tests to be non-blocking on PRs
run: |
chmod +x scripts/test-integration.sh
./scripts/test-integration.sh
- name: Fail on integration test failure for main/develop pushes
if: >
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') &&
steps.integration-tests.outcome == 'failure'
run: |
echo "Integration tests failed on push to ${GITHUB_REF}. Failing the job."
exit 1

Copilot uses AI. Check for mistakes.
- name: Display installed tools
run: |
echo "=== Installed Tools ==="
which git || echo "git not found"
which zsh || echo "zsh not found"
which stow || echo "stow not found"
which fzf || echo "fzf not found"
which nvim || echo "nvim not found"
which tmux || echo "tmux not found"

validate-packages:
name: Validate Package Files
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Run package validator
run: |
chmod +x scripts/validate-packages.sh
./scripts/validate-packages.sh

test-scripts:
name: Test Scripts
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Test help flags
run: |
chmod +x install.sh uninstall.sh update.sh
./install.sh --help
./uninstall.sh --help
./update.sh --help

- name: Test version flags
run: |
./install.sh --version
./uninstall.sh --version
./update.sh --version

- name: Test list components
run: |
./install.sh --list-components
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ files/.config/uv
files/.config/zed
files/.tmux/plugins
.install.log

# Test artifacts
shellcheck-report.txt
.install.log

# cagent files
files/.config/cagent/
11 changes: 11 additions & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ShellCheck configuration for dotfiles project
# See: https://www.shellcheck.net/wiki/

# Disable multiple checks - use comma-separated list for compatibility with older versions
# SC2155 - Declare and assign separately (verbose without significant benefit)
# SC1091 - Not following sourced files (verified at runtime)
# SC2034 - Variables appear unused (library definitions for external use)
# SC2086 - Double quote to prevent globbing (intentional in safe contexts)
# SC2059 - Don't use variables in printf (intentional color variables)
# SC2129 - Consider using { cmd1; cmd2; } >> file (style preference)
disable=SC2155,SC1091,SC2034,SC2086,SC2059,SC2129
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
# Exit on error
set -e

# Get the directory where this script is located
DOTFILES="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Get the directory where this script is located and export it
export DOTFILES="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Execute the new modular installation script
exec "$DOTFILES/scripts/install.sh" "$@"
41 changes: 40 additions & 1 deletion scripts/components/neovim.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,49 @@ setup_neovim() {

if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would install pynvim via pip"
echo "[DRY RUN] Would check Neovim version and upgrade if needed"
return 0
fi

if [ "$(command -v python)" ]; then
# Check if we need to upgrade Neovim on Linux (LazyVim requires >= 0.11.2)
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
if command -v nvim &> /dev/null; then
local nvim_version
nvim_version=$(nvim --version | head -n1 | grep -oP 'v\K[0-9]+\.[0-9]+\.[0-9]+' || echo "0.0.0")
local major minor patch
major=$(echo "$nvim_version" | cut -d. -f1)
minor=$(echo "$nvim_version" | cut -d. -f2)
patch=$(echo "$nvim_version" | cut -d. -f3)

# Check if version is less than 0.11.2
local needs_upgrade=false
if [ "$major" -eq 0 ]; then
if [ "$minor" -lt 11 ]; then
needs_upgrade=true
elif [ "$minor" -eq 11 ] && [ "$patch" -lt 2 ]; then
needs_upgrade=true
fi
fi

if [ "$needs_upgrade" = true ]; then
log_warning "Neovim $nvim_version is too old for LazyVim (requires >= 0.11.2)"
log_info "Installing latest Neovim from unstable PPA..."

# Add Neovim unstable PPA and upgrade
if command -v add-apt-repository &> /dev/null; then
sudo add-apt-repository -y ppa:neovim-ppa/unstable
sudo apt-get update
sudo apt-get install -y --only-upgrade neovim
log_success "Neovim upgraded to $(nvim --version | head -n1)"
else
log_warning "Cannot upgrade Neovim automatically - add-apt-repository not found"
fi
fi
Comment on lines +23 to +56

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic doesn’t match the requirement in the comment: it only upgrades when < 0.11, but LazyVim requirement is >= 0.11.2. This means 0.11.0 and 0.11.1 would incorrectly be treated as “new enough” and won’t be upgraded. Parse and compare the patch version too (or compare the full semver string) so versions below 0.11.2 trigger the upgrade.

Copilot uses AI. Check for mistakes.
fi
fi

# Install Python dependencies
if command -v python >/dev/null 2>&1; then
python -m pip install --upgrade pynvim
log_success "Neovim dependencies installed"
else
Expand Down
10 changes: 8 additions & 2 deletions scripts/components/shell.sh
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,15 @@ setup_shell() {
echo "$zsh_path" | sudo tee -a /etc/shells
fi

# Skip changing default shell in non-interactive mode (CI/automation)
# chsh requires password authentication which fails in CI
if [[ "$SHELL" != "$zsh_path" ]]; then
chsh -s "$zsh_path"
log_success "default shell changed to $zsh_path"
if [ "$NON_INTERACTIVE" = true ]; then
log_info "Non-interactive mode: skipping default shell change (use 'chsh -s $zsh_path' manually)"
else
chsh -s "$zsh_path"
log_success "default shell changed to $zsh_path"
fi
fi

# Install zap plugin manager if not already installed
Expand Down
22 changes: 20 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ set -euo pipefail
# GLOBAL VARIABLES
###########################################

DOTFILES="${DOTFILES:-$HOME/.dotfiles}"
# Calculate DOTFILES based on script location (scripts/ is subdirectory of repo root)
# If DOTFILES is already set (e.g., from root install.sh), use that value
if [ -z "${DOTFILES:-}" ]; then
DOTFILES="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VERSION="2.0.0"
OS=""
Expand Down Expand Up @@ -198,8 +202,22 @@ initialQuestions() {

if [ "$NON_INTERACTIVE" = true ]; then
log_info "Non-interactive mode: using defaults"
OS="mac"
# Auto-detect OS instead of assuming macOS
local detected_os=$(detect_os)
case "$detected_os" in
macos)
OS="mac"
;;
linux)
OS="linux"
;;
*)
log_error "Unsupported OS: $detected_os"
Comment on lines +206 to +215

Copilot AI Feb 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other scripts in this PR, detect_os is used for its side effects (populating variables like OS/DISTRO) rather than as a function that prints a value. Capturing it via detected_os=$(detect_os) is likely to set detected_os to empty and incorrectly hit the “Unsupported OS” branch. Make this consistent with the rest of the repo: call detect_os without command substitution and branch on the variable it sets (or adjust detect_os to reliably echo the OS string if that’s the intended contract).

Suggested change
local detected_os=$(detect_os)
case "$detected_os" in
macos)
OS="mac"
;;
linux)
OS="linux"
;;
*)
log_error "Unsupported OS: $detected_os"
detect_os
case "$OS" in
mac|linux)
;;
*)
log_error "Unsupported OS: $OS"

Copilot uses AI. Check for mistakes.
exit 1
;;
esac
Comment on lines +205 to +218

Copilot AI Feb 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

local detected_os=$(detect_os) runs detect_os in a command-substitution subshell; if detect_os primarily sets globals (as implied by other usage like detect_os + $DETECTED_OS), this will not work reliably and may yield an empty detected_os. Call detect_os normally and then branch on the exported/global value it sets (e.g., DETECTED_OS) rather than capturing output.

Copilot uses AI. Check for mistakes.
USE_DESKTOP_ENV=FALSE
log_info "Detected OS: $OS"
return
fi

Expand Down
45 changes: 38 additions & 7 deletions scripts/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ BACKUP_DIR="${BACKUP_DIR:-$HOME/.dotfiles.backup.$(date +%Y%m%d_%H%M%S)}"
CURRENT_STEP="${CURRENT_STEP:-0}"
TOTAL_STEPS="${TOTAL_STEPS:-10}"

# Ensure log file directory exists
if [ -n "${LOG_FILE:-}" ]; then
LOG_DIR="$(dirname "$LOG_FILE")"
mkdir -p "$LOG_DIR" 2>/dev/null || true
fi

###########################################
# ERROR HANDLING
###########################################
Expand All @@ -32,7 +38,7 @@ cleanup_on_error() {
echo "Performing cleanup..."

# Restore from backup if it exists
if [ -d "$BACKUP_DIR" ] && [ "$(ls -A $BACKUP_DIR)" ]; then
if [ -d "$BACKUP_DIR" ] && [ "$(ls -A "$BACKUP_DIR")" ]; then
log_warning "Restoring backup from $BACKUP_DIR"
cp -r "$BACKUP_DIR"/. "$HOME/"
log_info "Backup restored"
Expand Down Expand Up @@ -67,39 +73,59 @@ log() {
local message="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] $message" | tee -a "$LOG_FILE"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$timestamp] $message" | tee -a "$LOG_FILE" 2>/dev/null || echo "[$timestamp] $message"
else
echo "[$timestamp] $message"
fi
}

# Log success message
log_success() {
local message="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ✅ $message" | tee -a "$LOG_FILE"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$timestamp] ✅ $message" | tee -a "$LOG_FILE" 2>/dev/null || echo "[$timestamp] ✅ $message"
else
echo "[$timestamp] ✅ $message"
fi
}

# Log error message
log_error() {
local message="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ❌ $message" | tee -a "$LOG_FILE" >&2
if [ -n "${LOG_FILE:-}" ]; then
echo "[$timestamp] ❌ $message" | tee -a "$LOG_FILE" 2>/dev/null >&2 || echo "[$timestamp] ❌ $message" >&2
else
echo "[$timestamp] ❌ $message" >&2
fi
}

# Log warning message
log_warning() {
local message="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ⚠️ $message" | tee -a "$LOG_FILE"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$timestamp] ⚠️ $message" | tee -a "$LOG_FILE" 2>/dev/null || echo "[$timestamp] ⚠️ $message"
else
echo "[$timestamp] ⚠️ $message"
fi
}

# Log info message
log_info() {
local message="$1"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] ℹ️ $message" | tee -a "$LOG_FILE"
if [ -n "${LOG_FILE:-}" ]; then
echo "[$timestamp] ℹ️ $message" | tee -a "$LOG_FILE" 2>/dev/null || echo "[$timestamp] ℹ️ $message"
else
echo "[$timestamp] ℹ️ $message"
fi
}

# Show step progress
Expand All @@ -110,7 +136,12 @@ show_step() {
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " Step $CURRENT_STEP/$TOTAL_STEPS: $step_name"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
log_info "Step $CURRENT_STEP/$TOTAL_STEPS: $step_name"
# Log to file if possible, otherwise just echo
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
if [ -n "${LOG_FILE:-}" ]; then
echo "[$timestamp] ℹ️ Step $CURRENT_STEP/$TOTAL_STEPS: $step_name" >> "$LOG_FILE" 2>/dev/null || true
fi
}

###########################################
Expand Down
Loading
Loading