name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main, develop] jobs: # TypeScript/Node.js tests typescript-tests: name: TypeScript Build & Test runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-24.04, ubuntu-22.04, windows-latest, macos-latest] node-version: [20.x, 22.x] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: "npm" - name: Install dependencies run: npm ci - name: Run TypeScript compiler run: npm run build - name: Run linter shell: bash run: npm run lint || echo "Linter not configured yet" - name: Run TypeScript tests run: npm run test:ts # Python tests python-tests: name: Python Tests runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-24.04, ubuntu-22.04] python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: "pip" - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install pytest pytest-cov black mypy pylint if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - name: Run Black formatter check run: black --check python/ || echo "Black not configured yet" - name: Run MyPy type checker run: mypy python/ || echo "MyPy not configured yet" - name: Run Pylint run: pylint python/ || echo "Pylint not configured yet" - name: Run pytest run: pytest python/ --cov=python --cov-report=xml || echo "Tests not configured yet" - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: python name: python-${{ matrix.python-version }} if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-24.04' # Integration tests (requires KiCAD) integration-tests: name: Integration Tests (Linux + KiCAD) runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: kicad: ["8.0", "9.0", "10.0"] steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.12" cache: "pip" - name: Add KiCAD PPA and Install KiCAD ${{ matrix.kicad }} run: | sudo add-apt-repository --yes ppa:kicad/kicad-${{ matrix.kicad }}-releases sudo apt-get update sudo apt-get install -y kicad kicad-libraries mapfile -t KICAD_PYTHON_PACKAGES < <( { apt-cache search kicad apt-cache search pcbnew } | awk ' BEGIN { IGNORECASE = 1 } { name = $1 line = tolower($0) if (name !~ /nightly/ && (line ~ /kicad/ || line ~ /pcbnew/) && line ~ /python/) print name } ' | sort -u ) if [ "${#KICAD_PYTHON_PACKAGES[@]}" -gt 0 ]; then echo "Installing KiCad Python packages: ${KICAD_PYTHON_PACKAGES[*]}" sudo apt-get install -y "${KICAD_PYTHON_PACKAGES[@]}" fi KICAD_PYTHONPATH="$(python3 - <<'PY' import glob import os import subprocess import sys from pathlib import Path candidates = [] seen = set() def add_candidate(root: Path) -> None: if not root.exists(): return root_str = str(root) if root_str not in seen: seen.add(root_str) candidates.append(root_str) def add_candidate_for_file(file_path: Path) -> None: if file_path.name == "__init__.py" and file_path.parent.name == "pcbnew": add_candidate(file_path.parent.parent) return if file_path.parent.name == "pcbnew": add_candidate(file_path.parent.parent) return add_candidate(file_path.parent) def looks_like_pcbnew(file_path: Path) -> bool: name = file_path.name if name == "pcbnew.py": return True if name == "__init__.py" and file_path.parent.name == "pcbnew": return True return name.endswith(".so") and ( name.startswith("pcbnew") or name.startswith("_pcbnew") ) def collect_from_package(package: str) -> None: try: result = subprocess.run( ["dpkg", "-L", package], check=True, capture_output=True, text=True, ) except subprocess.CalledProcessError: return for raw_path in result.stdout.splitlines(): path = Path(raw_path) if path.is_file() and looks_like_pcbnew(path): add_candidate_for_file(path) package_result = subprocess.run( ["dpkg-query", "-W", "-f=${Package}\n"], check=True, capture_output=True, text=True, ) installed_packages = [ package for package in package_result.stdout.splitlines() if "kicad" in package or "pcbnew" in package ] for package in installed_packages: collect_from_package(package) file_patterns = [ "/usr/lib/python3/dist-packages/pcbnew.py", "/usr/lib/python3/dist-packages/pcbnew*.so", "/usr/lib/python3/dist-packages/_pcbnew*.so", "/usr/lib/python3/dist-packages/pcbnew/__init__.py", "/usr/lib/python3/dist-packages/pcbnew/_pcbnew*.so", "/usr/lib/kicad*/lib/python3/dist-packages/pcbnew.py", "/usr/lib/kicad*/lib/python3/dist-packages/pcbnew*.so", "/usr/lib/kicad*/lib/python3/dist-packages/_pcbnew*.so", "/usr/lib/kicad*/lib/python3/dist-packages/pcbnew/__init__.py", "/usr/lib/kicad*/lib/python3/dist-packages/pcbnew/_pcbnew*.so", "/usr/lib/*/dist-packages/pcbnew.py", "/usr/lib/*/dist-packages/pcbnew*.so", "/usr/lib/*/dist-packages/_pcbnew*.so", "/usr/lib/*/dist-packages/pcbnew/__init__.py", "/usr/lib/*/dist-packages/pcbnew/_pcbnew*.so", "/usr/local/lib/python*/dist-packages/pcbnew.py", "/usr/local/lib/python*/dist-packages/pcbnew*.so", "/usr/local/lib/python*/dist-packages/_pcbnew*.so", "/usr/local/lib/python*/dist-packages/pcbnew/__init__.py", "/usr/local/lib/python*/dist-packages/pcbnew/_pcbnew*.so", "/usr/share/kicad*/scripting/pcbnew.py", "/usr/share/kicad*/scripting/pcbnew*.so", "/usr/share/kicad*/scripting/_pcbnew*.so", "/usr/share/kicad*/scripting/pcbnew/__init__.py", "/usr/share/kicad*/scripting/pcbnew/_pcbnew*.so", ] for pattern in file_patterns: for raw_path in glob.glob(pattern): path = Path(raw_path) if path.is_file() and looks_like_pcbnew(path): add_candidate_for_file(path) if not candidates: for file_path in Path("/usr").rglob("*"): if file_path.is_file() and looks_like_pcbnew(file_path): add_candidate_for_file(file_path) def import_succeeds(candidate: str) -> bool: env = os.environ.copy() existing = env.get("PYTHONPATH", "") env["PYTHONPATH"] = ( f"{candidate}:{existing}" if existing else candidate ) probe = subprocess.run( [ sys.executable, "-c", ( "import pcbnew; " "print(getattr(pcbnew, '__file__', '')); " "print(pcbnew.GetBuildVersion())" ), ], capture_output=True, text=True, env=env, ) if probe.returncode == 0: print( f"Using pcbnew from {probe.stdout.splitlines()[0]}", file=sys.stderr, ) return True return False working_candidates = [candidate for candidate in candidates if import_succeeds(candidate)] print(":".join(working_candidates)) PY )" if [ -z "$KICAD_PYTHONPATH" ]; then echo "Unable to locate pcbnew Python bindings after installing KiCad ${{ matrix.kicad }}" >&2 { apt-cache search kicad apt-cache search pcbnew } | awk 'BEGIN { IGNORECASE = 1 } tolower($0) ~ /python/ { print }' || true dpkg-query -W -f='${Package}\n' | grep -Ei 'kicad|pcbnew' || true find /usr -path '*dist-packages*' \( -name 'pcbnew.py' -o -name 'pcbnew*.so' -o -name '_pcbnew*.so' \) -print || true exit 1 fi echo "KICAD_PYTHONPATH=$KICAD_PYTHONPATH" >> $GITHUB_ENV - name: Verify KiCAD installation run: | export KICAD_CLI="kicad-cli" export PYTHONPATH="$KICAD_PYTHONPATH:$PYTHONPATH" "$KICAD_CLI" version python3 -c "import pcbnew; print(f'pcbnew version: {pcbnew.GetBuildVersion()}')" - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi - name: Run real pcbnew smoke tests env: KICAD_USE_REAL_PCBNEW: "1" run: | export PYTHONPATH="$KICAD_PYTHONPATH:$PYTHONPATH" pytest tests/test_real_pcbnew_matrix.py -m "integration and real_pcbnew" # Docker build test docker-build: name: Docker Build Test 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 Docker image run: | echo "Docker build not yet configured" # docker build -t kicad-mcp-server:test . # Code quality checks code-quality: name: Code Quality runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: "20.x" - name: Install dependencies run: npm ci - name: Run ESLint run: npx eslint src/ || echo "ESLint not configured yet" - name: Run Prettier check run: npx prettier --check "src/**/*.ts" || echo "Prettier not configured yet" - name: Check for security vulnerabilities run: npm audit --audit-level=moderate || echo "No critical vulnerabilities" # Documentation check docs-check: name: Documentation Check runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Check README exists run: test -f README.md - name: Check for broken links in docs run: | sudo apt-get install -y linkchecker || true # linkchecker docs/ || echo "Link checker not configured" - name: Validate JSON files run: | find . -name "*.json" -not -path "./node_modules/*" -not -path "./dist/*" | xargs -I {} sh -c 'python3 -m json.tool {} > /dev/null && echo "✓ {}" || echo "✗ {}"'