375 lines
12 KiB
YAML
375 lines
12 KiB
YAML
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 "✗ {}"'
|