DEV Community

Abhinav
Abhinav

Posted on

Building Robust CI/CD Pipeline

In this post, I'll share my experience implementing a comprehensive CI/CD pipeline using GitHub Actions, writing tests for an existing codebase, and the valuable lessons learned along the way.

Setting Up GitHub Actions: From Zero to Production-Ready

The Challenge:

When I first approached this project, I was working with repo-contextr, a Python CLI tool that analyzes git repositories and packages their content for sharing with Large Language Models (LLMs). The project already had a solid foundation but lacked automated testing and continuous integration.

Designing the CI Workflow

In this, I made sure that I opt a robust approach to maintain the following:

  • Multi-platform compatibility (Linux, Windows, macOS)
  • Code quality through linting and formatting
  • Type safety with static analysis
  • Comprehensive test coverage
  • Successful package building

Here's the GitHub Actions workflow I implemented:

name: CI on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ['3.12'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' - name: Install dependencies run: | python -m pip install --upgrade pip pip install "ruff>=0.8.0" "mypy>=1.17.1" "pytest>=8.4.2" "pytest-cov>=6.0.0" pip install --editable . - name: Verify installation run: | python -c "import contextr; print('Package imported successfully')" python -c "from contextr.cli import app; print('CLI imported successfully')" - name: Run ruff linting run: | ruff check src tests - name: Run ruff formatting check run: | ruff format --check src tests - name: Run mypy type checking run: | mypy src continue-on-error: true - name: Run tests with coverage run: | pytest --cov=src --cov-report=xml --cov-report=term build: runs-on: ubuntu-latest needs: test steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install build dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: dist-packages path: dist/ 
Enter fullscreen mode Exit fullscreen mode

Writing Tests for Someone Else's Code: A Different Perspective

Understanding the perspective:

The Repo-Contextor already had a modular architecture as displayed below:

src/contextr/ ├── commands/ # CLI command implementations ├── config/ # Configuration management ├── discovery/ # File discovery logic ├── git/ # Git operations ├── processing/ # File reading and processing └── statistics/ # Token counting and stats 
Enter fullscreen mode Exit fullscreen mode

My Approach:

  1. File Discovery Module: The file discovery system needed comprehensive testing for various scenarios:
def test_discover_files_with_pattern(self, temp_dir): """Test discovering files with include pattern.""" (temp_dir / "file1.py").write_text("# python") (temp_dir / "file2.js").write_text("// javascript") (temp_dir / "file3.py").write_text("# python") result = discover_files([temp_dir], include_pattern="*.py") assert len(result) == 2 assert all(f.suffix == ".py" for f in result) 
Enter fullscreen mode Exit fullscreen mode
  1. Git Operations: Git integration required careful mocking to test various scenarios:
def test_get_git_info_valid_repo(self, sample_git_repo): """Test getting git info from valid repository.""" result = get_git_info(sample_git_repo) assert result is not None assert isinstance(result, dict) assert "commit" in result assert "branch" in result assert "author" in result assert "date" in result 
Enter fullscreen mode Exit fullscreen mode
  1. CLI Interface: Testing the CLI required understanding the Typer framework and proper mocking:
def test_basic_execution(self, mock_config, mock_package): """Test basic CLI execution without errors.""" # Mock configuration mock_config_obj = Mock() mock_config_obj.paths = ["."] mock_config_obj.include = None mock_config_obj.recent = False mock_config_obj.output = None 
Enter fullscreen mode Exit fullscreen mode

Outcome:

  • Test count: Increased from 108 to 160 tests (+48% increase)
  • Code coverage: Improved from 35.72% to 78.03% (+42.31% improvement)
  • Module coverage: Several modules went from 0% to 95%+ coverage.

Conclusion:

Implementing CI/CD isn't just about automation—it's about adopting a mindset of continuous improvement and quality assurance. The process taught me several valuable lessons: Quality Gates Matter, Fast Feedback Loops, Documentation Through Code, Confidence in Changes.

Top comments (0)