From c48c7da34eb5f4211a52cb24021831ceb1b348e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20SAILLANT?= <108685187+electron-rare@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:12:51 +0100 Subject: [PATCH] ci: add KiCad exports pipeline (ERC/DRC/PDF/BOM) Auto-discovers .kicad_sch and .kicad_pcb files under hardware/ and tools/. Runs ERC, DRC, exports PDF/SVG schematics, BOM and netlists. Fails on ERC/DRC errors. Uploads artifacts. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/kicad-exports.yml | 308 ++++++++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 .github/workflows/kicad-exports.yml diff --git a/.github/workflows/kicad-exports.yml b/.github/workflows/kicad-exports.yml new file mode 100644 index 0000000..d0bf44b --- /dev/null +++ b/.github/workflows/kicad-exports.yml @@ -0,0 +1,308 @@ +name: KiCad Exports + +on: + push: + branches: + - main + paths: + - "hardware/**" + - "tools/hw/**" + - ".github/workflows/kicad-exports.yml" + pull_request: + paths: + - "hardware/**" + - "tools/hw/**" + - ".github/workflows/kicad-exports.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + ARTIFACTS_DIR: artifacts/hw_exports + +jobs: + kicad-checks: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install KiCad 10 + run: | + sudo add-apt-repository -y ppa:kicad/kicad-10.0-releases + sudo apt-get update + sudo apt-get install -y --no-install-recommends kicad + kicad-cli version + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Prepare output directories + run: | + mkdir -p "$ARTIFACTS_DIR/erc" + mkdir -p "$ARTIFACTS_DIR/drc" + mkdir -p "$ARTIFACTS_DIR/pdf" + mkdir -p "$ARTIFACTS_DIR/svg" + mkdir -p "$ARTIFACTS_DIR/bom" + mkdir -p "$ARTIFACTS_DIR/netlist" + + # ── Discover all KiCad projects ────────────────────────────── + - name: Discover schematics and PCBs + id: discover + run: | + # Find all .kicad_pro files (each is a project root) + SCHEMATICS=$(find hardware -name "*.kicad_sch" \ + -not -path "*/.kicad_blocks/*" \ + -not -path "*/kicad_blocks/*" \ + -not -path "*backup*" | sort) + PCBS=$(find hardware -name "*.kicad_pcb" | sort) + + echo "Found schematics:" + echo "$SCHEMATICS" + echo "" + echo "Found PCBs:" + echo "${PCBS:-}" + + # Also check tools/cad proof fixtures + PROOF_PCBS=$(find tools/cad -name "*.kicad_pcb" 2>/dev/null | sort || true) + if [[ -n "$PROOF_PCBS" ]]; then + echo "" + echo "Found proof-fixture PCBs:" + echo "$PROOF_PCBS" + PCBS=$(printf "%s\n%s" "$PCBS" "$PROOF_PCBS" | sed '/^$/d') + fi + + # Export for later steps + { + echo "schematics<> "$GITHUB_OUTPUT" + + # ── ERC (Electrical Rules Check) ───────────────────────────── + - name: Run ERC on all schematics + id: erc + run: | + EXIT_CODE=0 + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "::group::ERC $SCH" + if kicad-cli sch erc "$SCH" \ + --format json \ + --output "$ARTIFACTS_DIR/erc/${BASENAME}_erc.json" 2>&1; then + # Parse error count + ERRORS=$(python3 -c " + import json, sys + try: + d = json.load(open('$ARTIFACTS_DIR/erc/${BASENAME}_erc.json')) + sheets = d.get('sheets', []) + errs = sum(1 for s in sheets for v in s.get('violations', []) if v.get('severity') == 'error') + print(errs) + except Exception as e: + print(f'parse-error: {e}', file=sys.stderr) + print(0) + ") + if [[ "$ERRORS" -gt 0 ]]; then + echo "::error file=$SCH::ERC found $ERRORS error(s)" + EXIT_CODE=1 + else + echo "ERC passed: $SCH" + fi + else + echo "::warning file=$SCH::ERC command failed (kicad-cli error)" + EXIT_CODE=1 + fi + echo "::endgroup::" + done <<< "${{ steps.discover.outputs.schematics }}" + echo "erc_exit=$EXIT_CODE" >> "$GITHUB_OUTPUT" + + # ── DRC (Design Rules Check) ───────────────────────────────── + - name: Run DRC on all PCBs + id: drc + run: | + EXIT_CODE=0 + while IFS= read -r PCB; do + [[ -z "$PCB" ]] && continue + BASENAME="$(basename "$PCB" .kicad_pcb)" + echo "::group::DRC $PCB" + if kicad-cli pcb drc "$PCB" \ + --format json \ + --output "$ARTIFACTS_DIR/drc/${BASENAME}_drc.json" 2>&1; then + ERRORS=$(python3 -c " + import json, sys + try: + d = json.load(open('$ARTIFACTS_DIR/drc/${BASENAME}_drc.json')) + errs = sum(1 for v in d.get('violations', []) if v.get('severity') == 'error') + print(errs) + except Exception as e: + print(f'parse-error: {e}', file=sys.stderr) + print(0) + ") + if [[ "$ERRORS" -gt 0 ]]; then + echo "::error file=$PCB::DRC found $ERRORS error(s)" + EXIT_CODE=1 + else + echo "DRC passed: $PCB" + fi + else + echo "::warning file=$PCB::DRC command failed (kicad-cli error)" + EXIT_CODE=1 + fi + echo "::endgroup::" + done <<< "${{ steps.discover.outputs.pcbs }}" + echo "drc_exit=$EXIT_CODE" >> "$GITHUB_OUTPUT" + + # ── PDF Schematic Export ───────────────────────────────────── + - name: Export schematic PDFs + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "Exporting PDF: $SCH" + kicad-cli sch export pdf "$SCH" \ + --output "$ARTIFACTS_DIR/pdf/${BASENAME}.pdf" 2>&1 \ + || echo "::warning file=$SCH::PDF export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── SVG Schematic Export ───────────────────────────────────── + - name: Export schematic SVGs + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + SVGDIR="$ARTIFACTS_DIR/svg/${BASENAME}" + mkdir -p "$SVGDIR" + echo "Exporting SVG: $SCH" + kicad-cli sch export svg "$SCH" \ + --output "$SVGDIR/" 2>&1 \ + || echo "::warning file=$SCH::SVG export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── BOM Export ─────────────────────────────────────────────── + - name: Export BOMs + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "Exporting BOM: $SCH" + kicad-cli sch export bom "$SCH" \ + --output "$ARTIFACTS_DIR/bom/${BASENAME}_bom.csv" \ + --fields 'Reference,Value,Footprint,Datasheet,Manufacturer,MPN,${QUANTITY},${DNP}' \ + --group-by "Value" \ + --sort-field "Reference" 2>&1 \ + || echo "::warning file=$SCH::BOM export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── Netlist Export ─────────────────────────────────────────── + - name: Export netlists + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "Exporting netlist: $SCH" + kicad-cli sch export netlist "$SCH" \ + --output "$ARTIFACTS_DIR/netlist/${BASENAME}_netlist.xml" 2>&1 \ + || echo "::warning file=$SCH::Netlist export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── Summary ────────────────────────────────────────────────── + - name: Generate summary + if: always() + run: | + echo "## KiCad Export Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + # ERC results + echo "### ERC Reports" >> "$GITHUB_STEP_SUMMARY" + for f in "$ARTIFACTS_DIR"/erc/*.json; do + [[ -f "$f" ]] || continue + NAME="$(basename "$f" _erc.json)" + ERRORS=$(python3 -c " + import json + d = json.load(open('$f')) + sheets = d.get('sheets', []) + errs = sum(1 for s in sheets for v in s.get('violations', []) if v.get('severity') == 'error') + warns = sum(1 for s in sheets for v in s.get('violations', []) if v.get('severity') == 'warning') + print(f'{errs} errors, {warns} warnings') + " 2>/dev/null || echo "parse error") + echo "- **$NAME**: $ERRORS" >> "$GITHUB_STEP_SUMMARY" + done + + # DRC results + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### DRC Reports" >> "$GITHUB_STEP_SUMMARY" + for f in "$ARTIFACTS_DIR"/drc/*.json; do + [[ -f "$f" ]] || continue + NAME="$(basename "$f" _drc.json)" + ERRORS=$(python3 -c " + import json + d = json.load(open('$f')) + errs = sum(1 for v in d.get('violations', []) if v.get('severity') == 'error') + warns = sum(1 for v in d.get('violations', []) if v.get('severity') == 'warning') + print(f'{errs} errors, {warns} warnings') + " 2>/dev/null || echo "parse error") + echo "- **$NAME**: $ERRORS" >> "$GITHUB_STEP_SUMMARY" + done + + # Exports + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### Exported Artifacts" >> "$GITHUB_STEP_SUMMARY" + echo "- PDFs: $(find "$ARTIFACTS_DIR/pdf" -name "*.pdf" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + echo "- SVGs: $(find "$ARTIFACTS_DIR/svg" -name "*.svg" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + echo "- BOMs: $(find "$ARTIFACTS_DIR/bom" -name "*.csv" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + echo "- Netlists: $(find "$ARTIFACTS_DIR/netlist" -name "*.xml" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + + # ── Upload Artifacts ───────────────────────────────────────── + - name: Upload ERC/DRC reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: kicad-reports + if-no-files-found: warn + path: | + artifacts/hw_exports/erc/ + artifacts/hw_exports/drc/ + + - name: Upload schematic exports + if: always() + uses: actions/upload-artifact@v4 + with: + name: kicad-schematics + if-no-files-found: warn + path: | + artifacts/hw_exports/pdf/ + artifacts/hw_exports/svg/ + + - name: Upload BOM and netlists + if: always() + uses: actions/upload-artifact@v4 + with: + name: kicad-bom-netlist + if-no-files-found: warn + path: | + artifacts/hw_exports/bom/ + artifacts/hw_exports/netlist/ + + # ── Fail if ERC or DRC had errors ──────────────────────────── + - name: Check ERC/DRC results + if: always() + run: | + ERC_EXIT="${{ steps.erc.outputs.erc_exit }}" + DRC_EXIT="${{ steps.drc.outputs.drc_exit }}" + if [[ "${ERC_EXIT:-0}" -ne 0 ]] || [[ "${DRC_EXIT:-0}" -ne 0 ]]; then + echo "::error::ERC or DRC checks failed. Review the uploaded reports." + exit 1 + fi + echo "All ERC/DRC checks passed."