Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
2126c3284This 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