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) <noreply@anthropic.com>
This commit is contained in:
Clément SAILLANT
2026-03-27 02:12:51 +01:00
parent 781b278b0d
commit c48c7da34e
+308
View File
@@ -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:-<none>}"
# 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<<EOFSCH"
echo "$SCHEMATICS"
echo "EOFSCH"
echo "pcbs<<EOFPCB"
echo "$PCBS"
echo "EOFPCB"
} >> "$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."