# UnitForge CI/CD Pipeline # Fast and comprehensive testing using uv package manager name: CI/CD on: push: branches: [main, develop] pull_request: branches: [main] release: types: [published] env: PYTHON_VERSION: "3.11" UV_CACHE_DIR: /tmp/.uv-cache jobs: # Code quality checks quality: name: Code Quality runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install dependencies run: uv pip install -e ".[dev]" - name: Format check (Black) run: uv run black --check backend/ tests/ - name: Import sorting check (isort) run: uv run isort --check-only backend/ tests/ - name: Lint (flake8) run: uv run flake8 backend/ tests/ - name: Type check (mypy) run: uv run mypy backend/ - name: Security check (bandit) run: uv run bandit -r backend/ -x tests/ # Test matrix across Python versions test: name: Test Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv --python python${{ matrix.python-version }} - name: Install dependencies run: uv pip install -e ".[dev]" - name: Run tests run: uv run pytest tests/ -v --tb=short - name: Test CLI functionality run: | ./unitforge-cli --help ./unitforge-cli template list ./unitforge-cli create --type service --name test --exec-start "/bin/true" --output test.service ./unitforge-cli validate test.service # Test with coverage coverage: name: Coverage Report runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install dependencies run: uv pip install -e ".[dev]" - name: Run tests with coverage run: uv run pytest tests/ --cov=backend --cov-report=xml --cov-report=html - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./coverage.xml fail_ci_if_error: false - name: Archive coverage report uses: actions/upload-artifact@v3 with: name: coverage-report path: htmlcov/ # Integration tests with real services integration: name: Integration Tests runs-on: ubuntu-latest services: systemd: image: jrei/systemd-ubuntu:20.04 options: --privileged --cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install dependencies run: uv pip install -e ".[dev,web]" - name: Start web server run: | ./start-server.sh --host 0.0.0.0 --port 8000 & sleep 10 - name: Test web interface run: | curl -f http://localhost:8000/health curl -f http://localhost:8000/api/templates curl -X POST -H "Content-Type: application/json" \ -d '{"content":"[Unit]\nDescription=Test\n[Service]\nType=simple\nExecStart=/bin/true\n[Install]\nWantedBy=multi-user.target"}' \ http://localhost:8000/api/validate - name: Test template generation run: | curl -X POST -H "Content-Type: application/json" \ -d '{"template_name":"webapp","parameters":{"name":"testapp","description":"Test App","exec_start":"/bin/true","user":"test","group":"test","working_directory":"/tmp","restart_policy":"on-failure","private_tmp":true,"protect_system":"strict"}}' \ http://localhost:8000/api/generate # Docker build and test docker: name: Docker Build runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build development image uses: docker/build-push-action@v5 with: context: . target: development cache-from: type=gha cache-to: type=gha,mode=max tags: unitforge:dev - name: Build production image uses: docker/build-push-action@v5 with: context: . target: production cache-from: type=gha cache-to: type=gha,mode=max tags: unitforge:prod - name: Build CLI image uses: docker/build-push-action@v5 with: context: . target: cli-only cache-from: type=gha cache-to: type=gha,mode=max tags: unitforge:cli - name: Test Docker images run: | # Test CLI image docker run --rm unitforge:cli --help # Test development image docker run --rm -d -p 8001:8000 --name unitforge-test unitforge:dev sleep 10 curl -f http://localhost:8001/health || exit 1 docker stop unitforge-test # Security scanning security: name: Security Scan runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install dependencies run: uv pip install -e ".[dev]" - name: Run security checks run: | uv run bandit -r backend/ -f json -o bandit-report.json || true uv run pip-audit --format=json --output=safety-report.json || true - name: Upload security reports uses: actions/upload-artifact@v3 with: name: security-reports path: | bandit-report.json safety-report.json # Dependency vulnerability check vulnerability: name: Vulnerability Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install dependencies run: uv pip install -e ".[dev]" - name: Check for vulnerabilities run: uv run pip-audit --desc --format=json --output=vulnerability-report.json || true - name: Upload vulnerability report uses: actions/upload-artifact@v3 with: name: vulnerability-report path: vulnerability-report.json # Performance benchmarks performance: name: Performance Tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install dependencies run: uv pip install -e ".[dev,web]" - name: Benchmark CLI operations run: | echo "Benchmarking CLI performance..." time ./unitforge-cli template list time ./unitforge-cli create --type service --name bench --exec-start "/bin/true" --output bench.service time ./unitforge-cli validate bench.service - name: Benchmark template generation run: | echo "Benchmarking template generation..." time ./unitforge-cli template generate webapp \ --param name=benchapp \ --param description="Benchmark App" \ --param exec_start="/bin/true" \ --param user=bench \ --param group=bench \ --param working_directory=/tmp \ --param restart_policy=on-failure \ --param private_tmp=true \ --param protect_system=strict \ --output bench-webapp.service # Build and package build: name: Build Package runs-on: ubuntu-latest needs: [quality, test, coverage] steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install build dependencies run: uv pip install build twine - name: Build package run: uv run python -m build - name: Check package run: uv run twine check dist/* - name: Upload package artifacts uses: actions/upload-artifact@v3 with: name: python-package path: dist/ # Publish to PyPI (only on release) publish: name: Publish to PyPI runs-on: ubuntu-latest needs: [build, docker, integration] if: github.event_name == 'release' && github.event.action == 'published' environment: pypi steps: - name: Download package artifacts uses: actions/download-artifact@v3 with: name: python-package path: dist/ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{ secrets.PYPI_API_TOKEN }} # Deploy documentation (on main branch) docs: name: Deploy Documentation runs-on: ubuntu-latest needs: [quality, test] if: github.ref == 'refs/heads/main' steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v2 with: enable-cache: true cache-dependency-glob: "pyproject.toml" - name: Set up virtual environment run: uv venv - name: Install dependencies run: uv pip install -e ".[dev]" - name: Generate API documentation run: | mkdir -p docs/api python -c " import json import sys sys.path.insert(0, 'backend') from app.main import app from fastapi.openapi.utils import get_openapi openapi_schema = get_openapi( title=app.title, version=app.version, description=app.description, routes=app.routes ) with open('docs/api/openapi.json', 'w') as f: json.dump(openapi_schema, f, indent=2) " - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs # Notification on failure notify: name: Notify on Failure runs-on: ubuntu-latest needs: [quality, test, coverage, integration, docker, security] if: failure() steps: - name: Notify failure run: | echo "CI/CD pipeline failed. Check the logs for details." echo "Failed jobs: ${{ join(needs.*.result, ', ') }}"