This document describes how a new version of KiCad Routing Tools is built, published on GitHub, and submitted to the official KiCad Plugin and Content Manager (PCM) repository.
The release pipeline is mostly automated by .github/workflows/release.yml. A maintainer's job is:
- Bump the version.
- Tag and push.
- Wait for CI to publish the GitHub Release.
- Submit / update the PCM metadata via a merge request to
gitlab.com/kicad/addons/metadata.
| File | Purpose |
|---|---|
VERSION |
Source of truth for the project version (e.g. 0.15.5). |
metadata.json |
KiCad PCM package manifest at repo root. CI patches download_* fields after building. |
package_pcm.py |
Builds one PCM-compatible zip per version, bundling all 4 prebuilt Rust binaries. |
update_metadata.py |
Patches metadata.json with sha256 / sizes from package_pcm.py's sidecar. |
.github/workflows/release.yml |
CI: builds Rust binaries, runs package_pcm.py, publishes GitHub Release. |
__init__.py (root) |
_resolve_rust_binary() picks the right platform binary at plugin startup. |
kicad_routing_plugin/deps_check.py |
First-launch wx dialog that pip-installs missing Python deps (read from requirements.txt) into KiCad's Python. |
VERSION: 2- or 3-part dotted decimal, e.g.0.15.5. The KiCad PCM regex^\d{1,4}(\.\d{1,4}(\.\d{1,6})?)?$rejects 4-part versions.rust_router/Cargo.toml: bumped only when the Rust crate itself changes (see the comment at the top of that file). Python-only changes keep the Rust version constant and only bumpVERSION.- Git tag must be
v<VERSION>, e.g.v0.15.5.
requirements.txt at the repo root is the single source of truth for the plugin's Python dependencies. It's consumed by three things:
install_plugin.py— pip-installs everything into KiCad's Python during manual install.kicad_routing_plugin/deps_check.py— parses it at plugin startup, checks each package is importable, and offers a wx pip-install dialog for anything missing (the PCM install path skips pip otherwise).- CLI scripts (
startup_checks.py) — fails loudly if a required package isn't importable when running from the terminal.
To add a dependency:
- Append it to
requirements.txt(with a version specifier, e.g.mypkg>=1.2). - (Optional) Add a more thorough import test to the
IMPORT_TESTSdict inkicad_routing_plugin/deps_check.py— by default a plainimport <pkgname>is used, which is fine for most packages but doesn't catch broken installs of compiled extensions. The existing entries (scipy,shapely) import specific submodules the plugin actually uses. - (Optional) Add it to
startup_checks.check_python_dependencies()if you also want the CLI scripts to fail with a clearer message.
You do not need to update metadata.json — Python deps live entirely outside the PCM manifest.
The CLI scripts and the kicad_routing_plugin/ GUI call the same routing
engine, but a new engine parameter, results-data key, writer argument, or
changed default has to be threaded through both the argparse layer and
every GUI call site (and file-text parse_kicad_pcb fixes must be mirrored in
build_pcb_data_from_board). A release must not ship an engine fix the GUI
silently drops. Audit every commit since the last-confirmed SHA:
LAST=$(awk '{print $1}' .gui-parity-checked)
git log --oneline $LAST..HEAD
git diff $LAST..HEAD -- py_router/route.py py_router/route_diff.py py_router/route_planes.py \
py_router/repair_planes.py py_router/bga_fanout.py py_router/place_fanout_clearance.py \
py_router/placement/ py_router/single_ended_routing.py py_router/layer_swap_optimization.py \
py_router/stub_layer_switching.py py_router/obstacle_map.py py_router/kicad_parser.pyFor each new engine parameter / results key / writer arg / changed default,
confirm the matching kicad_routing_plugin/ call site was updated (see the
parity rules in CLAUDE.md). Fix any gap and commit it, then update
.gui-parity-checked to the new HEAD (SHA + date + outcome) and commit that.
check_release_version.py --tag vX.Y.Z (step 2 and the first CI gate) fails
the release unless .gui-parity-checked references the current HEAD, so this
audit is mandatory. For a release that provably touches no engine/GUI code
(docs-only), pass --skip-parity-check to bypass the gate.
# Edit:
# VERSION -> new x.y.z
# metadata.json -> bump versions[].version to x.y.z
# update download_url path (sha/sizes are CI-patched)
# rust_router/Cargo.toml (only if Rust changed)
# rust_router/README.md (only if Rust changed, version-history table)Then verify the bump is consistent before tagging — this is the same check
CI runs first, so catch a half-bumped VERSION/metadata.json here instead of
~10 minutes into the release build:
python3 check_release_version.py --tag v0.15.5It must print OK:. (A VERSION-only bump that forgets metadata.json is the
exact mistake that failed the v0.17.0 release.)
git add -u
git commit -m "v0.15.5: <short summary>"
git tag -a v0.15.5 -m "v0.15.5 - <short summary>"
git push origin main
git push origin v0.15.5The tag push triggers .github/workflows/release.yml.
gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId')The workflow has four jobs:
validate: runscheck_release_version.pyto gate the release on VERSION/metadata/tag consistency and GUI parity before anything is built.build(matrix, ~2–3 min each): builds the Rust binary forlinux-x86_64,macos-arm64,macos-x86_64, andwindows-x86_64. The macOS x86_64 build is cross-compiled from the macOS arm64 runner (cheaper / available capacity thanmacos-13).package(~1 min): runspackage_pcm.pyto produce oneKiCadRoutingTools-<ver>.zipbundling all 4 binaries under platform-suffix filenames, then runsupdate_metadata.pyto patch the real sha256 / size intometadata.json.release: creates / updates the GitHub Release atv<VERSION>with all 4 raw binaries, the PCM zip, and the patchedmetadata.jsonattached.
When release completes successfully, the public Release URL is:
https://github.com/drandyhaas/KiCadRoutingTools/releases/tag/v<VERSION>
The KiCad PCM repository at gitlab.com/kicad/addons/repository is read-only; it is generated from gitlab.com/kicad/addons/metadata. Submissions and updates go to the metadata repo as merge requests.
-
A GitLab account verified for shared runners (free tier requires identity verification before CI minutes work).
-
glabCLI installed and authenticated:brew install glab glab auth login --hostname gitlab.com # pick Web auth
-
Download the patched
metadata.jsonfrom the GitHub Release:curl -fsSL -o /tmp/metadata.json \ https://github.com/drandyhaas/KiCadRoutingTools/releases/download/v0.15.5/metadata.json
-
Fork
kicad/addons/metadata(one time):glab api -X POST 'projects/kicad%2Faddons%2Fmetadata/fork' -
Clone the fork and create a feature branch (do not commit to
main; the validator pipeline rejects pushes tomain):git clone https://gitlab.com/<your-user>/metadata.git /tmp/kicad-addons-fork cd /tmp/kicad-addons-fork git checkout -b add-com.github.drandyhaas.kicadroutingtools
-
Drop in the package files at
packages/<identifier>/:mkdir -p packages/com.github.drandyhaas.kicadroutingtools cp /tmp/metadata.json packages/com.github.drandyhaas.kicadroutingtools/metadata.json cp ~/path/to/icon_64.png packages/com.github.drandyhaas.kicadroutingtools/icon.png -
Validate locally (catches the same errors the upstream CI catches):
git remote add target https://gitlab.com/kicad/addons/metadata.git git fetch -n target pip install jsonschema pillow munch requests tqdm export MERGE_BASE_SHA=$(git merge-base target/main HEAD) export DIFF_FILES=$(git diff --name-status "${MERGE_BASE_SHA}..HEAD") ./ci/validate.sh
Both files should report
Validation passed. If not, fix locally and re-run before pushing. -
Commit and push to your fork:
git add packages/com.github.drandyhaas.kicadroutingtools/ git commit -m "Add com.github.drandyhaas.kicadroutingtools v0.15.5" git push -u origin add-com.github.drandyhaas.kicadroutingtools -
Open the merge request against the upstream:
glab mr create \ --repo kicad/addons/metadata \ --target-branch main \ --source-branch add-com.github.drandyhaas.kicadroutingtools \ --remove-source-branch \ --title "Add com.github.drandyhaas.kicadroutingtools v0.15.5" \ --description "..."
-
Smoke-test the package end-to-end before maintainers review. The successful
buildCI job on the MR exposes a temporary PCM repository URL. Find it with:PIPE_ID=<the head_pipeline id from the MR> BUILD_JOB=$(glab api projects/<your-fork-id>/pipelines/$PIPE_ID/jobs \ | python3 -c "import sys,json; print([j['id'] for j in json.load(sys.stdin) if j['name']=='build'][0])") glab api projects/<your-fork-id>/jobs/$BUILD_JOB/trace | grep "Repository should be available"
The URL looks like
https://gitlab.com/<you>/metadata/-/jobs/<id>/artifacts/raw/artifacts/repository.json. Add it to KiCad: Plugin Manager → Manage… → +, paste the URL, install the package, and verify the plugin loads and works on Pcbnew.
A KiCad maintainer will then review. After it merges, expect up to one day for the package to appear in the public PCM catalogue (the public kicad/addons/repository is regenerated on a schedule).
After the initial MR is merged, the metadata file packages/com.github.drandyhaas.kicadroutingtools/metadata.json already exists upstream. To add a new version:
-
Build and publish the new GitHub Release as above (steps 1–3 of Cutting a release).
-
In your
kicad-addons-forkclone, pull upstream and create a new branch:git fetch target git checkout target/main git checkout -b update-kicadroutingtools-v0.15.6
-
Append the new version object to
versions[]inpackages/com.github.drandyhaas.kicadroutingtools/metadata.json(keep older entries — PCM history shows them). -
Validate locally (
./ci/validate.sh), commit, push, open MR — same as before.
-
Duplicate
versionstrings inversions[]— PCM rejects two entries with the same version string regardless of theirplatformsarrays. Ship one zip per version that supports multiple platforms via runtime resolver, not one zip per platform. -
Inner
metadata.jsonwithdownload_*fields — themetadata.jsoninside the PCM zip must have exactly one version entry and nodownload_*fields.package_pcm.pystrips these automatically; don't bypass it. -
4-part
VERSION(e.g.0.15.3.1) — PCM regex rejects this. Use 3-part versions. -
GitLab pipeline fails before any job runs (status
failed, no jobs, nostarted_at) — the GitLab account isn't identity-verified for shared compute yet. Try https://gitlab.com/-/identity_verification. After verifying, retrigger with a force-push (see next item). -
MR widget keeps showing the old failed pipeline — GitLab's MR
head_pipelineonly updates when a newmerge_request_event-source pipeline runs. Manually creating a pipeline viaPOST /projects/<id>/pipeline?ref=...produces apush-source pipeline that does not update the MR widget. To get a fresh head_pipeline, force-push the same commit with a new timestamp:git commit --amend --no-edit --date=now git push --force-with-lease origin <branch>
-
schema-v2.jsonshowing up in your commits — the local validator overwrites this when running. Alwaysgit checkout target/main -- schema-v2.json(or stash it) before committing. -
macOS x86_64 runners stuck queued —
macos-13runners are scarce. The workflow cross-compiles x86_64 frommacos-14(arm64) instead.
- KiCad addon submission guide: https://dev-docs.kicad.org/en/addons/
- Upstream metadata repo: https://gitlab.com/kicad/addons/metadata
- PCM schema v1: https://go.kicad.org/pcm/schemas/v1