Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# ---------------------------------------------------------------------------
# Fast ruff lint gate — rule selection lives in [tool.ruff] in pyproject.toml.
# Also runs the mypy beachhead (scope + rationale in [tool.mypy] there);
# the covered packages are listed in [tool.mypy].files. Widen `files`
# in pyproject.toml as further packages are brought up to the same bar.
# ---------------------------------------------------------------------------
lint:
name: ruff lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install ruff
run: python -m pip install ruff
# tools/ and hooks/ ship with the plugin and are inside the text-I/O
# portability AST gate, so they are held to the same lint bar as src/.
- name: ruff check
run: ruff check src tests tools hooks
# The op vocabulary lives in src/akcli/opspec.py; schemas/*.json and the
# packaged mirror are generated views of it. Drift = a stale contract.
- name: schemas match opspec (generated + packaged mirror)
run: python3 tools/schemas.py check
# examples/circuits/*/ops.json is the canonical corpus; the packaged
# akcli/examples mirror is what `project new --example` ships in a wheel.
- name: packaged example mirror matches examples/circuits
run: python3 tools/examples_mirror.py check
- name: Install mypy (dev extra)
run: python -m pip install -e ".[dev]"
- name: mypy (beachhead — scope in pyproject [tool.mypy].files)
run: mypy
# ---------------------------------------------------------------------------
# Unit / fixture matrix — zero runtime deps; tests use the [dev] extra.
# OS {ubuntu, macos, windows} x Python {3.11, 3.12, 3.13, 3.14}
# ---------------------------------------------------------------------------
test:
name: test (${{ matrix.os }} / py${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install (dev extra)
run: python -m pip install -e ".[dev]"
- name: Run tests
run: python -m pytest -q
# ---------------------------------------------------------------------------
# akcli --help smoke from a clean checkout + bin wrapper executable bit
# + hooks.json validity (jq). Unix only (bash wrapper).
# ---------------------------------------------------------------------------
cli-smoke:
name: cli smoke + hooks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: jq . hooks/hooks.json
run: jq . hooks/hooks.json
- name: bin/akcli is executable
run: test -x bin/akcli
- name: akcli --help (clean checkout, no install)
run: ./bin/akcli --help
- name: akcli --version
run: ./bin/akcli --version
# ---------------------------------------------------------------------------
# Packaging: build + twine check; version-sync drift check.
# ---------------------------------------------------------------------------
packaging:
name: build + twine check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install build tooling
run: python -m pip install build twine
- name: Build sdist + wheel
run: python -m build
- name: twine check
run: twine check dist/*
- name: Version sync drift check
run: python tools/sync_version.py --check
# ---------------------------------------------------------------------------
# Wheel install smoke — proves the built wheel is actually usable: fresh venv,
# pip install dist/*.whl, exercise entry point + package data (ops schema).
# ---------------------------------------------------------------------------
wheel-smoke:
name: wheel install smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Build wheel
run: |
python -m pip install build
python -m build --wheel
- name: Install wheel into a fresh venv and smoke akcli
run: |
set -euo pipefail
python -m venv "$RUNNER_TEMP/smoke"
"$RUNNER_TEMP/smoke/bin/pip" install dist/*.whl
"$RUNNER_TEMP/smoke/bin/akcli" --version
"$RUNNER_TEMP/smoke/bin/akcli" ops list
"$RUNNER_TEMP/smoke/bin/akcli" ops template add_wire
# ---------------------------------------------------------------------------
# Claude Code plugin + marketplace validation.
# ---------------------------------------------------------------------------
plugin-validate:
name: plugin validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: "20"
- name: Install Claude Code CLI
run: npm install -g @anthropic-ai/claude-code
- name: Validate marketplace (strict)
run: claude plugin validate . --strict
- name: Validate plugin component (tree minus marketplace.json)
run: |
set -euo pipefail
tmp="$(mktemp -d)"
cp -R . "$tmp/plugin"
rm -f "$tmp/plugin/.claude-plugin/marketplace.json"
claude plugin validate "$tmp/plugin"
# ---------------------------------------------------------------------------
# Browser UI regression for `akcli view` — drives the runner's Chrome via
# puppeteer-core (no browser download). macOS runners ship Google Chrome.
# ---------------------------------------------------------------------------
browser-ui:
name: browser ui (akcli view)
runs-on: macos-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- uses: actions/setup-node@v6
with:
node-version: "20"
- name: Install (dev extra)
run: python -m pip install -e ".[dev]"
- name: Install puppeteer-core
run: npm install --no-fund --no-audit
working-directory: tools/ui-test
- name: Run the browser regression
run: python -m pytest tests/test_webui_browser.py -v
# ---------------------------------------------------------------------------
# KiCad 10.x present — optional secondary kicad-cli verify path. Ubuntu only.
# This job also exercises the sim engine (sim/engine.py) live: it installs
# libngspice, confirms discovery actually finds a loadable library (failing
# the job otherwise rather than silently falling back to skipped tests), and
# runs the sim engine + sim CLI test modules so their live (non-skipif'd)
# cases execute in CI instead of only on developer machines with a KiCad app
# bundle.
# ---------------------------------------------------------------------------
kicad-cli:
name: kicad-cli (ubuntu + KiCad 10)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install pinned KiCad 10.x
run: |
set -euo pipefail
sudo add-apt-repository --yes ppa:kicad/kicad-10.0-releases
sudo apt-get update
sudo apt-get install --yes --no-install-recommends kicad
- name: kicad-cli version
run: kicad-cli version
- name: Install libngspice (sim engine live tests)
run: |
set -euo pipefail
# The KiCad apt bundle usually pulls in libngspice0-kicad as a dep,
# but install explicitly (with a soname-package fallback) so the
# sim engine has a library to discover even if that changes.
sudo apt-get install -y libngspice0 || sudo apt-get install -y libngspice0-kicad
- name: Install (dev extra)
run: python -m pip install -e ".[dev]"
- name: Discover libngspice for AKCLI_NGSPICE (only if auto-discovery misses)
run: |
set -euo pipefail
found="$(python -c 'from akcli.sim import engine; print(engine.available() or "")')"
if [ -n "$found" ]; then
echo "sim engine auto-discovered libngspice at: $found"
else
echo "auto-discovery found nothing; searching ldconfig for a fallback path"
path="$(ldconfig -p 2>/dev/null | grep -m1 -i libngspice | awk '{print $NF}')"
if [ -z "$path" ]; then
echo "::error::libngspice not found via discovery or ldconfig after install" >&2
exit 1
fi
echo "AKCLI_NGSPICE=$path" >> "$GITHUB_ENV"
echo "auto-discovery missed; exporting AKCLI_NGSPICE=$path"
fi
- name: Assert sim engine is live (not None) before running its tests
run: |
set -euo pipefail
python -c "
from akcli.sim import engine
lib = engine.available()
print('libngspice:', lib)
assert lib is not None, 'sim engine unavailable in CI: engine.available() returned None'
"
- name: Run tests with kicad-cli available
run: python -m pytest -q
- name: Run KiCad ground-truth tests live (fail if any case is skipped)
run: |
set -euo pipefail
# Fixture-first is only a real gate if these actually RUN: all four
# files skip themselves when kicad-cli is missing, so a broken install
# (or a broken availability probe) would otherwise turn the whole
# ground-truth suite into a silent green. test_kicad_parity.py is the
# biggest of the four (the eeschema netlist parity corpus) and was
# the one this assertion used to miss entirely; test_e2e_project.py is
# the only place the .kicad_pro/.kicad_pcb emitters are proven against
# a real KiCad at all.
python -m pytest -q -rs \
tests/test_e2e_draw.py tests/test_kicad_cli.py tests/test_kicad_parity.py \
tests/test_e2e_project.py \
| tee /tmp/kicad-tests.log
# Matches the "kicad-cli not installed" skip reason in all four files;
# capability-fallback skips (e.g. KiCad < 8 emits no JSON ERC report)
# stay allowed.
if grep -qE "^SKIPPED .*kicad-cli not installed" /tmp/kicad-tests.log; then
echo "::error::KiCad ground-truth tests were skipped despite kicad-cli being installed" >&2
grep -E "^SKIPPED" /tmp/kicad-tests.log >&2
exit 1
fi
# The parity corpus has no capability-fallback skips at all — ANY
# skipped case there means the ground truth did not run.
if grep -qE "^SKIPPED .*test_kicad_parity" /tmp/kicad-tests.log; then
echo "::error::tests/test_kicad_parity.py must not skip in the KiCad job" >&2
grep -E "^SKIPPED .*test_kicad_parity" /tmp/kicad-tests.log >&2
exit 1
fi
- name: Run sim engine + sim CLI tests live (fail if any live case is skipped)
run: |
set -euo pipefail
# Every module with an ngspice-gated case, not just the two obvious
# ones: fullloop / hardening / sweep / wave each carry live cases that
# a broken libngspice probe would silently turn green.
python -m pytest -q -rs \
tests/test_sim_engine.py tests/test_sim_cli.py \
tests/test_sim_fullloop.py tests/test_sim_hardening.py \
tests/test_sim_sweep.py tests/test_sim_wave.py \
| tee /tmp/sim-tests.log
if grep -qE "^SKIPPED .*(libngspice|ngspice)" /tmp/sim-tests.log; then
echo "::error::sim live tests were skipped despite libngspice being available" >&2
grep -E "^SKIPPED" /tmp/sim-tests.log >&2
exit 1
fi
# ---------------------------------------------------------------------------
# Graceful degradation when kicad-cli is absent (primary path is pure Python).
# ---------------------------------------------------------------------------
no-kicad-cli:
name: no kicad-cli degradation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Confirm kicad-cli absent
run: |
if command -v kicad-cli >/dev/null 2>&1; then
echo "kicad-cli unexpectedly present" >&2; exit 1
fi
echo "kicad-cli absent — exercising pure-Python degradation path"
- name: Install (dev extra)
run: python -m pip install -e ".[dev]"
- name: Run tests without kicad-cli
run: python -m pytest -q
2126c3284This file is pinned to the selected release. Download the original to open it in your design tool.