diff --git a/.claude/tasks/2025-07-06-setup-improvements.md b/.claude/tasks/2025-07-06-setup-improvements.md index 850918fa..5a2635f0 100644 --- a/.claude/tasks/2025-07-06-setup-improvements.md +++ b/.claude/tasks/2025-07-06-setup-improvements.md @@ -4,8 +4,9 @@ **Task**: Improve reliability and testability of dotfiles setup process through TDD approach **Total PRs Planned**: 10 PRs + 2 Infrastructure PRs -**Completed**: 8 PRs (including SSH Installation Testing) -**Current**: Working on Infrastructure Improvements (PR 8.1 & 8.2) +**Completed**: 8 PRs (including SSH Installation Testing + Infrastructure PRs) +**Current**: Bash migration experiment (see bash-migration task file) +**Critical Issue Discovered**: setup.zsh references $DOTFILES files before repo is cloned - needs urgent fix ## Completed PRs @@ -56,16 +57,16 @@ - **Functions**: `detect_ssh_keys()`, `ssh_key_pair_found()`, centralized path configuration - **Improvements**: Post-implementation refactoring, dead code elimination, enhanced CLAUDE.md guidelines -## Infrastructure Improvements (In Progress) +## Infrastructure Improvements (Completed) -### ๐Ÿ”„ PR 8.1: Pull Request Template (In Progress) +### โœ… PR 8.1: Pull Request Template (Merged) - **Goal**: Standardize PR descriptions using established patterns from successful PRs - **Implementation**: `.github/PULL_REQUEST_TEMPLATE.md` with structured sections - **Features**: Clean What/Why/Usage/Validation/Links structure with clear guidance comments - **Benefits**: Consistent PR format, reduced cognitive load, cross-project template reference -- **Status**: Enhanced with emoji headings, refined comments, and CLAUDE.md guidelines +- **Status**: Enhanced with emoji headings, refined comments, and CLAUDE.md guidelines - merged -### โœ… PR 8.2: GitHub Actions CI (Completed) +### โœ… PR 8.2: GitHub Actions CI (Merged) - **Goal**: Automatic test execution on PRs with GitHub UI status indicators - **Implementation**: `.github/workflows/test-dotfiles.yml` using existing test infrastructure - **Features**: macOS runner, zsh shell, smart path triggering, existing test/run-tests.zsh integration diff --git a/.claude/tasks/2025-07-07-bash-migration.md b/.claude/tasks/2025-07-07-bash-migration.md new file mode 100644 index 00000000..02e1c1cb --- /dev/null +++ b/.claude/tasks/2025-07-07-bash-migration.md @@ -0,0 +1,163 @@ +# Bash Migration Epic + +## Current Status (July 7, 2025) + +**Task**: Migrate dotfiles installation scripts from custom zsh test framework to industry-standard bash + shellcheck + bats +**Approach**: Complete 3-phase migration for better tooling, maintainability, and reliability +**Current**: Phase 1 - PR #13 (GitHub Installation Testing) in draft review + +## Migration Strategy + +### Phase 1: Demonstrate Complete Migration Pattern (In Progress) +**Goal**: Create one complete bash + bats example to establish the migration pattern + +#### ๐Ÿ”„ PR #13: GitHub Installation Testing (Draft) +- **Files Created**: + - `lib/github-utils.bash` - GitHub SSH utilities (shellcheck clean) + - `bin/install/github.bash` - Bash replacement for github.zsh + - `test/install/test-github-utils.bats` - 14 utility function tests + - `test/install/test-github-installation.bats` - 6 integration tests + - `experiment/bash-testing/` - Comparison docs and examples +- **Test Results**: All 20 tests passing, shellcheck compliance verified +- **Benefits Demonstrated**: Better error catching, industry-standard tooling, cleaner syntax +- **Status**: Ready for review, includes CLAUDE.md improvements for PR workflow + +### Phase 2: Systematic Script Migration (Pending) +**Goal**: Migrate remaining installation scripts one-by-one using established pattern + +**Priority Order**: +1. `bin/install/ssh.bash` - Critical dependency for GitHub operations +2. `bin/install/homebrew.bash` - Foundation for all other tools +3. `bin/install/node.bash` - Programming language setup +4. Remaining scripts based on dependency analysis + +**Pattern per Script**: +- Create `lib/{script}-utils.bash` with shellcheck compliance +- Create comprehensive bats test suite +- Migrate main installation script to bash +- Verify feature parity with original zsh version +- Update integration points + +### Phase 3: Framework Cleanup (Pending) +**Goal**: Remove zsh test framework and update CI + +**Tasks**: +- Update `.github/workflows/test-dotfiles.yml` to use bats instead of custom runner +- Remove `test/run-tests.zsh` and custom zsh test utilities +- Update documentation to reflect bash-first approach +- Archive zsh test framework with migration notes + +## Key Decisions Made + +### Bash + Shellcheck + Bats Stack +- **Bash**: Universal, better CI support, consistent behavior +- **Shellcheck**: Industry-standard linting, catches common errors +- **Bats**: Mature testing framework, wide adoption, good GitHub Actions support + +### Migration Approach: Complete Examples First +- **Rejected**: Hybrid approach (keep scripts in zsh, tests in bash) +- **Chosen**: Full migration with complete working examples +- **Reasoning**: Cleaner long-term solution, better tooling ecosystem + +### Test Coverage Standards +- **Utilities**: Comprehensive unit tests for all functions +- **Integration**: End-to-end workflow testing with mocking +- **Compliance**: All bash scripts must pass shellcheck with zero warnings +- **Behavioral Focus**: Test what code does, not implementation details + +## Technical Patterns Established + +### File Structure Pattern +``` +lib/{component}-utils.bash # Utility functions +bin/install/{component}.bash # Installation script +test/install/test-{component}-utils.bats # Unit tests +test/install/test-{component}-installation.bats # Integration tests +``` + +### Bash Script Standards +- `#!/usr/bin/env bash` shebang +- `set -euo pipefail` strict mode +- Shellcheck compliance (zero warnings) +- Function documentation with usage examples +- Proper error handling and user feedback + +### Test Standards +- Comprehensive test coverage (both success and failure paths) +- Environment isolation with setup/teardown +- Mocking of external dependencies +- Clear test descriptions and assertions +- Integration tests that verify complete workflows + +## Integration Points + +### CI/CD Pipeline +- GitHub Actions currently uses custom zsh test runner +- **Future**: Migrate to bats with better reporting and parallelization +- **Benefit**: Standard test output format, better integration with GitHub UI + +### Existing Zsh Infrastructure +- **Preserve**: Current zsh configuration and shell setup (user-facing) +- **Migrate**: Only installation and testing scripts (development tooling) +- **Benefit**: Users keep familiar shell while development gets better tools + +## Discovered Issues + +### Critical Setup.zsh Bug +**Issue**: `setup.zsh` references `$DOTFILES` files via `source` commands before the dotfiles repository is cloned +**Impact**: Setup process fails on fresh installations +**Priority**: High - affects core functionality +**Next Steps**: Fix in separate PR after current bash migration PR is reviewed + +### Dead Code Prevention +**Pattern**: Every function/utility must have demonstrated usage in the same PR +**Enforcement**: CLAUDE.md updated with strict guidelines +**Benefit**: Prevents speculative code that becomes maintenance burden + +## Success Metrics + +### Phase 1 (Current) +- โœ… 20 comprehensive tests passing +- โœ… Zero shellcheck warnings +- โœ… Feature parity with original zsh scripts +- โœ… Complete behavior bundle (utilities + tests + integration + usage) +- ๐Ÿ”„ User review and approval + +### Phase 2 (Future) +- All installation scripts migrated to bash +- Comprehensive bats test coverage +- CI pipeline updated to use bats +- Documentation updated + +### Phase 3 (Future) +- Custom zsh test framework removed +- Clean codebase with single testing approach +- Improved developer experience with standard tooling + +## Development Workflow + +### Current Process +1. Create utilities with shellcheck compliance +2. Write comprehensive bats tests (unit + integration) +3. Implement installation script in bash +4. Verify feature parity and test coverage +5. Create draft PR for review iteration +6. Update task documentation with progress + +### Quality Gates +- All bats tests must pass +- Zero shellcheck warnings allowed +- Feature parity verification required +- Complete behavior bundle (no dead code) + +## Next Steps After PR #13 Review + +1. **Address Review Feedback**: Incorporate any requested changes +2. **Fix Critical Setup Bug**: Resolve $DOTFILES dependency issue +3. **Continue Phase 1**: SSH installation script migration +4. **Plan Phase 2**: Define systematic migration order +5. **Update CI**: Begin migration to bats-based testing + +--- + +*This epic represents a significant infrastructure improvement that will provide better development tools, more reliable testing, and easier maintenance for the dotfiles project.* \ No newline at end of file diff --git a/bin/install/github.bash b/bin/install/github.bash new file mode 100644 index 00000000..0623222c --- /dev/null +++ b/bin/install/github.bash @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +# GitHub SSH setup and repository configuration +# Handles adding SSH keys to GitHub and converting HTTPS remotes to SSH + +set -euo pipefail + +# Configuration +DOTFILES="${DOTFILES:-$HOME/Repos/ooloth/dotfiles}" +PRIVATE_KEY="$HOME/.ssh/id_rsa" +PUBLIC_KEY="$PRIVATE_KEY.pub" + +# Load utilities +# shellcheck source=../../lib/github-utils.bash +source "$DOTFILES/lib/github-utils.bash" + +# All needed utilities are now available in github-utils.bash + +main() { + echo "๐Ÿ”‘ Adding SSH key pair to GitHub" + + # Check if GitHub SSH connection already works + if github_ssh_connection_works; then + echo "โœ… You can already connect to GitHub via SSH." + return 0 + fi + + # Verify SSH keys exist + if ! ssh_keys_exist; then + echo "โŒ SSH keys not found. Please run the SSH installation script first." + echo "Run: source $DOTFILES/bin/install/ssh.zsh" + return 1 + fi + + # Verify SSH keys are loaded in agent + if ! ssh_keys_in_agent; then + echo "โŒ SSH keys not loaded in ssh-agent. Please add them first." + echo "Run: ssh-add $PRIVATE_KEY" + return 1 + fi + + # Display public key for user to add to GitHub + echo "" + echo "Your turn!" + echo "Please visit https://github.com/settings/ssh/new now and add the following SSH key to your GitHub account:" + echo "" + cat "$PUBLIC_KEY" + echo "" + echo "Actually go do this! This step is required before you'll be able to clone repos via SSH." + echo "" + + # Wait for user confirmation (interactive step) + echo -n "All set? (y/N): " + read -r github_key_added + + if [[ "$github_key_added" != "y" && "$github_key_added" != "Y" ]]; then + echo "You have chosen...poorly." + return 0 + else + echo "Excellent!" + fi + + # Verify GitHub SSH connection now works + echo "" + echo "๐Ÿงช Verifying you can now connect to GitHub via SSH..." + + if github_ssh_connection_works; then + echo "โœ… SSH key was added to GitHub successfully." + else + echo "โŒ Failed to connect to GitHub via SSH. Please verify the key was added correctly." + echo "Visit: https://github.com/settings/keys" + return 1 + fi + + # Convert dotfiles remote from HTTPS to SSH if needed + convert_dotfiles_remote_to_ssh + + echo "" + echo "๐Ÿš€ Done adding your SSH key pair to GitHub." +} + +convert_dotfiles_remote_to_ssh() { + local dotfiles_dir="$DOTFILES" + + # Check if dotfiles directory exists + if [[ ! -d "$dotfiles_dir" ]]; then + echo "โš ๏ธ Dotfiles directory not found at $dotfiles_dir" + return 0 + fi + + # Get current remote URL + local current_url + if ! current_url=$(get_git_remote_url "$dotfiles_dir"); then + echo "โš ๏ธ Could not get git remote URL for dotfiles" + return 0 + fi + + # Check if remote uses HTTPS + if is_https_remote "$current_url"; then + echo "" + echo "๐Ÿ”— Converting the dotfiles remote URL from HTTPS to SSH" + + local ssh_url + ssh_url=$(convert_https_to_ssh "$current_url") + + if set_git_remote_url "$dotfiles_dir" "$ssh_url"; then + echo "โœ… Dotfiles remote URL has been updated to use SSH" + else + echo "โŒ Failed to update dotfiles remote URL" + return 1 + fi + else + echo "โœ… Dotfiles remote URL already uses SSH" + fi +} + +# Run main function if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/experiment/bash-testing/COMPARISON.md b/experiment/bash-testing/COMPARISON.md new file mode 100644 index 00000000..5380b7bb --- /dev/null +++ b/experiment/bash-testing/COMPARISON.md @@ -0,0 +1,90 @@ +# Bash + Shellcheck + Bats vs Current Zsh Approach + +## Experiment Results + +### โœ… What Worked Well (Bash + Shellcheck + Bats) + +**Shellcheck Benefits:** +- **Zero warnings** on well-written bash script +- **Automatic linting** catches common bash pitfalls +- **IDE integration** available for real-time feedback +- **Best practices enforcement** (set -euo pipefail, quoting, etc.) + +**Bats Testing Benefits:** +- **Clean, readable syntax** with `@test` decorators +- **Built-in assertions** with simple `[ ]` syntax and `[[ ]]` patterns +- **Automatic test discovery** and execution +- **Great output formatting** with TAP (Test Anything Protocol) format +- **Setup/teardown hooks** for test isolation +- **Easy mocking** with environment variable overrides +- **Mature ecosystem** with extensive documentation + +**Development Experience:** +- **5/5 tests passed** immediately +- **Clear output format** showing test status +- **Easy environment isolation** with temp directories +- **Simple mocking** by overriding HOME directory +- **Excellent error messages** when tests fail + +### ๐Ÿค” Current Zsh Approach Comparison + +**Zsh Custom Framework:** +- **Works well** for our current needs +- **Tailored** to our specific requirements +- **8 test files running** successfully +- **Comprehensive mocking** framework we built + +**Potential Limitations:** +- **No automatic linting** for shell script quality +- **Custom test framework** requires maintenance +- **Less tooling ecosystem** compared to bash +- **Fewer developers familiar** with advanced zsh scripting + +## Strategic Recommendation + +### Option 1: Gradual Migration to Bash (Recommended) +**Approach:** +1. **Keep existing zsh install scripts** (they work and match target environment) +2. **Migrate test infrastructure to bash + bats** for better tooling +3. **New utilities in bash** when they don't require zsh-specific features +4. **Use shellcheck** for all bash scripts + +**Benefits:** +- **Best of both worlds** - zsh for environment match, bash for testing +- **Better tooling** without breaking existing working code +- **Gradual transition** reduces risk +- **Improved code quality** with shellcheck + +### Option 2: Full Migration to Bash +**Approach:** +- Rewrite installation scripts in bash +- Use bash + bats for all testing +- Ensure compatibility with target macOS bash version + +**Trade-offs:** +- **More work** to migrate existing scripts +- **Potential compatibility issues** with macOS default bash 3.x +- **Benefits may not justify migration effort** for working code + +### Option 3: Hybrid Experiment First +**Approach:** +1. **Try bash + bats** for the next PR (GitHub installation testing) +2. **Compare side-by-side** with existing zsh approach +3. **Make decision** based on real-world development experience + +## Tooling Quality Assessment + +| Tool | Bash + Bats | Current Zsh | +|------|-------------|-------------| +| Linting | โœ… shellcheck | โŒ None | +| Test Framework | โœ… Mature bats | โœ… Custom (works) | +| IDE Support | โœ… Excellent | โš ๏ธ Limited | +| Documentation | โœ… Extensive | โš ๏ธ Custom | +| Community | โœ… Large | โš ๏ธ Smaller | +| Ecosystem | โœ… Rich | โš ๏ธ Limited | + +## Conclusion + +The bash + shellcheck + bats approach offers **significant tooling advantages** that could improve code quality and development velocity. The experiment demonstrates that migration is **technically feasible** and would provide **immediate benefits**. + +**Recommendation**: Start with **Option 3** (hybrid experiment) for the next PR to get real-world comparison data before making a strategic decision. \ No newline at end of file diff --git a/experiment/bash-testing/bad-example.bash b/experiment/bash-testing/bad-example.bash new file mode 100644 index 00000000..c41d80a3 --- /dev/null +++ b/experiment/bash-testing/bad-example.bash @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Example with common bash mistakes to show shellcheck value + +check_file() { + if [ -f $1 ]; then # Missing quotes - shellcheck will catch this + echo "File exists: $1" + fi + + # Unused variable - shellcheck will catch this + unused_var="this is never used" + + # Variable used before set - shellcheck will catch this + echo $undefined_var +} \ No newline at end of file diff --git a/experiment/bash-testing/example-utils.bash b/experiment/bash-testing/example-utils.bash new file mode 100644 index 00000000..d6710620 --- /dev/null +++ b/experiment/bash-testing/example-utils.bash @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Example utilities for testing bash + shellcheck + bats approach +# This mirrors some functionality from our existing SSH utilities + +set -euo pipefail + +# Check if a command exists +command_exists() { + local command_name="$1" + command -v "$command_name" >/dev/null 2>&1 +} + +# Detect if SSH keys exist (similar to our SSH utils) +detect_ssh_keys() { + local ssh_dir="${HOME}/.ssh" + local private_key="${ssh_dir}/id_rsa" + local public_key="${ssh_dir}/id_rsa.pub" + + if [[ -s "$private_key" && -s "$public_key" ]]; then + echo "โœ… SSH key pair found." + return 0 + else + echo "๐Ÿ‘Ž No SSH key pair found." + return 1 + fi +} + +# Validate homebrew installation (similar to our Homebrew utils) +validate_homebrew() { + if ! command_exists "brew"; then + echo "โŒ Homebrew not found" + return 1 + fi + + echo "โœ… Homebrew found" + return 0 +} \ No newline at end of file diff --git a/experiment/bash-testing/test-example-utils.bats b/experiment/bash-testing/test-example-utils.bats new file mode 100644 index 00000000..ea4a4ea1 --- /dev/null +++ b/experiment/bash-testing/test-example-utils.bats @@ -0,0 +1,67 @@ +#!/usr/bin/env bats + +# Load the functions we want to test +load "example-utils.bash" + +setup() { + # Create a temporary directory for each test + export TEST_TEMP_DIR + TEST_TEMP_DIR="$(mktemp -d)" +} + +teardown() { + # Clean up temporary directory + if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +@test "command_exists returns 0 for existing commands" { + run command_exists "ls" + [ "$status" -eq 0 ] +} + +@test "command_exists returns 1 for non-existing commands" { + run command_exists "nonexistent-command-12345" + [ "$status" -eq 1 ] +} + +@test "detect_ssh_keys finds existing SSH keys" { + # Mock SSH directory with keys + local fake_home="$TEST_TEMP_DIR/fake_home" + mkdir -p "$fake_home/.ssh" + echo "fake private key" > "$fake_home/.ssh/id_rsa" + echo "fake public key" > "$fake_home/.ssh/id_rsa.pub" + + # Override HOME for this test + export HOME="$fake_home" + + run detect_ssh_keys + [ "$status" -eq 0 ] + [[ "$output" == *"SSH key pair found"* ]] +} + +@test "detect_ssh_keys reports missing SSH keys" { + # Use empty fake home directory + local fake_home="$TEST_TEMP_DIR/fake_home" + mkdir -p "$fake_home" + + # Override HOME for this test + export HOME="$fake_home" + + run detect_ssh_keys + [ "$status" -eq 1 ] + [[ "$output" == *"No SSH key pair found"* ]] +} + +@test "validate_homebrew works when brew is available" { + # This test will pass/fail based on actual system state + # In a real scenario, we'd mock the command + if command -v brew >/dev/null 2>&1; then + run validate_homebrew + [ "$status" -eq 0 ] + [[ "$output" == *"Homebrew found"* ]] + else + skip "Homebrew not installed on this system" + fi +} \ No newline at end of file diff --git a/home/.claude/CLAUDE.md b/home/.claude/CLAUDE.md index cf33946e..9fe782b0 100644 --- a/home/.claude/CLAUDE.md +++ b/home/.claude/CLAUDE.md @@ -238,6 +238,24 @@ assert_not_equals(0, exit_code, "setup should exit when prerequisites fail") - Split large changes into multiple PRs when possible - Include context about why changes were made, not just what changed +### PR Creation Requirements + +**CRITICAL: Always create PRs in draft mode for review workflows:** + +1. **Default to draft mode** - Use `gh pr create --draft` when creating PRs +2. **Draft allows iteration** - User can review and request changes before marking ready +3. **Prevents premature merge** - Ensures proper review process is followed +4. **Only use non-draft PRs** when explicitly instructed or for trivial changes +5. **Mark ready when told** - User will explicitly say when to convert from draft to ready + +**Example PR creation:** +```bash +gh pr create --draft --title "Feature Title" --body "$(cat <<'EOF' +[PR description using template] +EOF +)" +``` + ### PR Template Usage **Always use the high-quality PR template when repositories lack good templates:** @@ -337,6 +355,8 @@ assert_not_equals(0, exit_code, "setup should exit when prerequisites fail") - **Stay focused on current PR** until user explicitly says to move on or confirms merge - **PR work includes** creation, iteration, addressing feedback, and final merge - **Wait for user direction** before considering PR work finished +- **Never mark PR tasks as completed in todo lists** until the PR is actually merged +- **Don't update project roadmaps or task trackers** to show PR completion until merge is confirmed **CRITICAL: Always update PR description after pushing commits with new functionality:** @@ -384,17 +404,32 @@ EOF ### Multi-PR Task Management +**CRITICAL: Always maintain task roadmap files throughout development** + For tasks involving multiple PRs, create and maintain a roadmap file: 1. **Create task roadmap file** in `.claude/tasks/YYYY-MM-DD-task-name.md` 2. **Update throughout development** with progress, learnings, and context -3. **Include essential information** for future Claudes taking over: +3. **Update after EVERY significant change**: + - After creating/merging PRs + - After discovering new issues or requirements + - After making important technical decisions + - After user feedback or direction changes + - **Never let task files become stale** - they are critical handoff documentation +4. **Include essential information** for future Claudes taking over: - Completed PRs with key outcomes - Current PR status and next steps - Important decisions made and why - Technical patterns established - Any gotchas or lessons learned -4. **When moving files**: If git doesn't detect as a move (due to content changes), explicitly stage both the new file creation AND the old file deletion in the same commit + - Critical issues discovered during development +5. **When moving files**: If git doesn't detect as a move (due to content changes), explicitly stage both the new file creation AND the old file deletion in the same commit + +**Task file maintenance is NOT optional** - these files are essential for: +- Continuity when conversations end mid-task +- Context for future development sessions +- Preventing repeated mistakes and decisions +- Maintaining project momentum across multiple sessions Example task file structure: ```markdown diff --git a/home/.claude/settings.json b/home/.claude/settings.json index 19e51066..09098eea 100644 --- a/home/.claude/settings.json +++ b/home/.claude/settings.json @@ -25,6 +25,7 @@ "Bash(ls:*)", "Bash(mkdir:*)", "Bash(mv:*)", + "Bash(shellcheck:*)", "Bash(timeout:*)", "Edit(*)", "MultiEdit(*)", diff --git a/lib/github-utils.bash b/lib/github-utils.bash new file mode 100644 index 00000000..01b1ca9d --- /dev/null +++ b/lib/github-utils.bash @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# GitHub utility functions for installation scripts +# Provides reusable functionality for GitHub SSH setup and verification + +set -euo pipefail + +# Test SSH connection to GitHub +github_ssh_connection_works() { + ssh -T git@github.com >/dev/null 2>&1 +} + +# Check if SSH keys exist and are non-empty +ssh_keys_exist() { + local ssh_dir="${HOME}/.ssh" + local private_key="${ssh_dir}/id_rsa" + local public_key="${ssh_dir}/id_rsa.pub" + + [[ -s "$private_key" && -s "$public_key" ]] +} + +# Check if SSH keys are loaded in ssh-agent +ssh_keys_in_agent() { + ssh-add -l >/dev/null 2>&1 +} + +# Get the current git remote URL for a repository +get_git_remote_url() { + local repo_dir="$1" + + if [[ ! -d "$repo_dir" ]]; then + return 1 + fi + + ( + cd "$repo_dir" + git config --get remote.origin.url 2>/dev/null + ) +} + +# Check if a git remote URL uses HTTPS +is_https_remote() { + local remote_url="$1" + [[ "$remote_url" == https://github.com/* ]] +} + +# Convert HTTPS GitHub URL to SSH format +convert_https_to_ssh() { + local https_url="$1" + + if [[ "$https_url" == https://github.com/* ]]; then + echo "git@github.com:${https_url#https://github.com/}" + else + echo "$https_url" + fi +} + +# Set git remote URL for a repository +set_git_remote_url() { + local repo_dir="$1" + local new_url="$2" + + if [[ ! -d "$repo_dir" ]]; then + return 1 + fi + + ( + cd "$repo_dir" + git remote set-url origin "$new_url" + ) +} \ No newline at end of file diff --git a/macos/Brewfile b/macos/Brewfile index 24377ca2..c258609e 100644 --- a/macos/Brewfile +++ b/macos/Brewfile @@ -34,6 +34,7 @@ end brew "basedpyright" # for python in neovim brew "bash" brew "bat" # replaces cat +brew "bats-core" # for bash testing brew "btop" # top alternative brew "bufbuild/buf/buf" # for buf_lint in neovim brew "coreutils" diff --git a/test/install/test-github-installation.bats b/test/install/test-github-installation.bats new file mode 100644 index 00000000..fcf80b71 --- /dev/null +++ b/test/install/test-github-installation.bats @@ -0,0 +1,110 @@ +#!/usr/bin/env bats + +# Integration tests for GitHub installation script +# Tests the complete workflow including error handling and user interactions + +# Load the GitHub installation script (for testing main function logic) +load "../../bin/install/github.bash" + +setup() { + # Create temporary directory for each test + export TEST_TEMP_DIR + TEST_TEMP_DIR="$(mktemp -d)" + + # Save original environment + export ORIGINAL_HOME="$HOME" + export ORIGINAL_DOTFILES="${DOTFILES:-}" +} + +teardown() { + # Restore original environment + export HOME="$ORIGINAL_HOME" + if [[ -n "${ORIGINAL_DOTFILES}" ]]; then + export DOTFILES="$ORIGINAL_DOTFILES" + else + unset DOTFILES + fi + + # Clean up temporary directory + if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +@test "convert_dotfiles_remote_to_ssh handles missing dotfiles directory gracefully" { + # Set up fake environment + export DOTFILES="$TEST_TEMP_DIR/nonexistent_dotfiles" + + run convert_dotfiles_remote_to_ssh + [ "$status" -eq 0 ] + [[ "$output" == *"Dotfiles directory not found"* ]] +} + +@test "convert_dotfiles_remote_to_ssh handles non-git directory gracefully" { + # Create fake dotfiles directory (not a git repo) + export DOTFILES="$TEST_TEMP_DIR/fake_dotfiles" + mkdir -p "$DOTFILES" + + run convert_dotfiles_remote_to_ssh + [ "$status" -eq 0 ] + [[ "$output" == *"Could not get git remote URL"* ]] +} + +@test "convert_dotfiles_remote_to_ssh converts HTTPS remote to SSH" { + # Create fake dotfiles git repository with HTTPS remote + export DOTFILES="$TEST_TEMP_DIR/fake_dotfiles" + mkdir -p "$DOTFILES" + cd "$DOTFILES" + git init + git remote add origin "https://github.com/user/dotfiles.git" + + run convert_dotfiles_remote_to_ssh + [ "$status" -eq 0 ] + [[ "$output" == *"Converting the dotfiles remote URL from HTTPS to SSH"* ]] + [[ "$output" == *"Dotfiles remote URL has been updated to use SSH"* ]] + + # Verify the remote was actually updated + local new_url + new_url=$(git config --get remote.origin.url) + [ "$new_url" = "git@github.com:user/dotfiles.git" ] +} + +@test "convert_dotfiles_remote_to_ssh leaves SSH remote unchanged" { + # Create fake dotfiles git repository with SSH remote + export DOTFILES="$TEST_TEMP_DIR/fake_dotfiles" + mkdir -p "$DOTFILES" + cd "$DOTFILES" + git init + git remote add origin "git@github.com:user/dotfiles.git" + + run convert_dotfiles_remote_to_ssh + [ "$status" -eq 0 ] + [[ "$output" == *"Dotfiles remote URL already uses SSH"* ]] + + # Verify the remote wasn't changed + local url + url=$(git config --get remote.origin.url) + [ "$url" = "git@github.com:user/dotfiles.git" ] +} + +@test "script can be loaded without errors" { + # Simple test to verify the script loads properly + run bash -c "source bin/install/github.bash && echo 'Script loaded successfully'" + [ "$status" -eq 0 ] + [[ "$output" == *"Script loaded successfully"* ]] +} + +@test "main function detects missing SSH keys" { + # Create fake environment with no SSH keys + export HOME="$TEST_TEMP_DIR/fake_home" + mkdir -p "$HOME/.ssh" + + # Mock github_ssh_connection_works to return failure + github_ssh_connection_works() { + return 1 + } + + run bash -c "source bin/install/github.bash; main" + [ "$status" -eq 1 ] + [[ "$output" == *"SSH keys not found"* ]] +} \ No newline at end of file diff --git a/test/install/test-github-utils.bats b/test/install/test-github-utils.bats new file mode 100644 index 00000000..6e079c65 --- /dev/null +++ b/test/install/test-github-utils.bats @@ -0,0 +1,154 @@ +#!/usr/bin/env bats + +# Test GitHub utility functions +# Tests both utility functions and integration with installation workflow + +# Load the GitHub utilities +load "../../lib/github-utils.bash" + +setup() { + # Create temporary directory for each test + export TEST_TEMP_DIR + TEST_TEMP_DIR="$(mktemp -d)" + + # Save original HOME for restoration + export ORIGINAL_HOME="$HOME" +} + +teardown() { + # Restore original HOME + export HOME="$ORIGINAL_HOME" + + # Clean up temporary directory + if [[ -n "${TEST_TEMP_DIR:-}" && -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +@test "ssh_keys_exist returns 0 when both keys exist and are non-empty" { + # Create fake home with SSH keys + local fake_home="$TEST_TEMP_DIR/fake_home" + mkdir -p "$fake_home/.ssh" + echo "fake private key content" > "$fake_home/.ssh/id_rsa" + echo "fake public key content" > "$fake_home/.ssh/id_rsa.pub" + + # Override HOME for this test + export HOME="$fake_home" + + run ssh_keys_exist + [ "$status" -eq 0 ] +} + +@test "ssh_keys_exist returns 1 when private key is missing" { + # Create fake home with only public key + local fake_home="$TEST_TEMP_DIR/fake_home" + mkdir -p "$fake_home/.ssh" + echo "fake public key content" > "$fake_home/.ssh/id_rsa.pub" + + export HOME="$fake_home" + + run ssh_keys_exist + [ "$status" -eq 1 ] +} + +@test "ssh_keys_exist returns 1 when public key is missing" { + # Create fake home with only private key + local fake_home="$TEST_TEMP_DIR/fake_home" + mkdir -p "$fake_home/.ssh" + echo "fake private key content" > "$fake_home/.ssh/id_rsa" + + export HOME="$fake_home" + + run ssh_keys_exist + [ "$status" -eq 1 ] +} + +@test "ssh_keys_exist returns 1 when keys are empty" { + # Create fake home with empty key files + local fake_home="$TEST_TEMP_DIR/fake_home" + mkdir -p "$fake_home/.ssh" + touch "$fake_home/.ssh/id_rsa" + touch "$fake_home/.ssh/id_rsa.pub" + + export HOME="$fake_home" + + run ssh_keys_exist + [ "$status" -eq 1 ] +} + +@test "get_git_remote_url returns remote URL for valid git repository" { + # Create a fake git repository + local fake_repo="$TEST_TEMP_DIR/fake_repo" + mkdir -p "$fake_repo" + cd "$fake_repo" + git init + git remote add origin "https://github.com/user/repo.git" + + run get_git_remote_url "$fake_repo" + [ "$status" -eq 0 ] + [ "$output" = "https://github.com/user/repo.git" ] +} + +@test "get_git_remote_url returns 1 for non-existent directory" { + run get_git_remote_url "/nonexistent/directory" + [ "$status" -eq 1 ] +} + +@test "get_git_remote_url returns 1 for directory without git repository" { + local non_git_dir="$TEST_TEMP_DIR/not_git" + mkdir -p "$non_git_dir" + + run get_git_remote_url "$non_git_dir" + [ "$status" -eq 1 ] +} + +@test "is_https_remote returns 0 for GitHub HTTPS URLs" { + run is_https_remote "https://github.com/user/repo.git" + [ "$status" -eq 0 ] +} + +@test "is_https_remote returns 1 for SSH URLs" { + run is_https_remote "git@github.com:user/repo.git" + [ "$status" -eq 1 ] +} + +@test "is_https_remote returns 1 for non-GitHub HTTPS URLs" { + run is_https_remote "https://gitlab.com/user/repo.git" + [ "$status" -eq 1 ] +} + +@test "convert_https_to_ssh converts GitHub HTTPS URL to SSH format" { + run convert_https_to_ssh "https://github.com/user/repo.git" + [ "$status" -eq 0 ] + [ "$output" = "git@github.com:user/repo.git" ] +} + +@test "convert_https_to_ssh returns unchanged URL for non-HTTPS URLs" { + run convert_https_to_ssh "git@github.com:user/repo.git" + [ "$status" -eq 0 ] + [ "$output" = "git@github.com:user/repo.git" ] +} + +@test "set_git_remote_url updates remote URL in git repository" { + # Create a fake git repository + local fake_repo="$TEST_TEMP_DIR/fake_repo" + mkdir -p "$fake_repo" + cd "$fake_repo" + git init + git remote add origin "https://github.com/user/repo.git" + + # Update the remote URL + run set_git_remote_url "$fake_repo" "git@github.com:user/repo.git" + [ "$status" -eq 0 ] + + # Verify the URL was updated + cd "$fake_repo" + local new_url + new_url=$(git config --get remote.origin.url) + [ "$new_url" = "git@github.com:user/repo.git" ] +} + +@test "set_git_remote_url returns 1 for non-existent directory" { + run set_git_remote_url "/nonexistent/directory" "git@github.com:user/repo.git" + [ "$status" -eq 1 ] +} \ No newline at end of file