The Windows build launched, printed help, drew the specimen card, and showed a
menu containing one item: Quit. Not a permissions problem and nothing to do with
Bluetooth — the tools were simply not in the bundle.
`meshterm/tools/__init__.py` finds every tool with `pkgutil.iter_modules` and
imports it so its `@register` decorator fires. Nothing static ever names a tool
module, so PyInstaller had no reason to include one, and the registry came up
empty: no menu entries, no CLI subcommands, an app that starts and does nothing.
`collect_submodules("meshterm.tools")` fixes it, which is the same treatment
`bleak.backends` already had for the same reason — the spec knew about other
people's dynamic imports and not about ours.
The worse half is that CI said this was fine. The smoke test ran `--help` and
`specimen`, and a build with no tools at all passes both. It now runs a
tool-provided subcommand, so an empty registry fails the pipeline instead of
reaching a release page. Green tests that cannot see the defect are worse than no
tests, because they are believed.
`MESHTERM_HOME` comes out of the same session. Every copy of MeshTerm reads and
writes `~/.meshterm`, so trying a downloaded build meant pointing it at the same
34MB history database as the checkout it was built from, with no way to keep them
apart. The variable moves the whole directory. An empty or whitespace value falls
back rather than resolving to the current directory, because a shell exporting it
blank is common and scattering someone's history into their downloads folder is a
poor way to find that out.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
120 lines
4.3 KiB
YAML
120 lines
4.3 KiB
YAML
# The three commands CONTRIBUTING.md asks a contributor to run, run for them.
|
|
#
|
|
# Split in two on purpose. Style is a property of the source, not of the machine, so
|
|
# `lint` runs once and its failure is unambiguous. Behaviour is a property of the
|
|
# machine, so `test` runs everywhere we claim to work — the description promises
|
|
# Windows, macOS and Linux, and until this file existed that promise had only ever been
|
|
# checked on one of them.
|
|
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
# Nothing here writes to the repository; say so rather than inherit the default.
|
|
permissions:
|
|
contents: read
|
|
|
|
# A second push while the first is still running makes the first answer obsolete.
|
|
# Cancel it — on a private repo those are metered minutes spent on a stale commit.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
lint:
|
|
name: lint
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.10"
|
|
cache: pip
|
|
cache-dependency-path: pyproject.toml
|
|
|
|
- run: pip install -e ".[dev]"
|
|
|
|
# Both gates, and the second runs even when the first fails: they report
|
|
# different things and seeing only the earlier one wastes a round trip.
|
|
- run: ruff check .
|
|
- if: ${{ !cancelled() }}
|
|
run: ruff format --check .
|
|
|
|
test:
|
|
name: test ${{ matrix.os }} py${{ matrix.python }}
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 15
|
|
strategy:
|
|
# Let every leg finish. The point of a run is the whole picture of what broke,
|
|
# and one red leg cancelling its siblings hides the rest until the next push.
|
|
fail-fast: false
|
|
matrix:
|
|
# Shaped for a *private* repo, where minutes are metered and billed by a
|
|
# multiplier: Linux 1x, Windows 2x, macOS 10x. So Linux carries the full
|
|
# Python range and the costly runners cover the ends of it, which is where a
|
|
# version-dependent break actually shows up.
|
|
#
|
|
# Once the repo is public, standard runners are unmetered: widen this to every
|
|
# OS times every version and stop thinking about it.
|
|
include:
|
|
- { os: ubuntu-latest, python: "3.10" }
|
|
- { os: ubuntu-latest, python: "3.11" }
|
|
- { os: ubuntu-latest, python: "3.12" }
|
|
- { os: ubuntu-latest, python: "3.13" }
|
|
- { os: windows-latest, python: "3.10" }
|
|
- { os: windows-latest, python: "3.13" }
|
|
- { os: macos-latest, python: "3.13" }
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: ${{ matrix.python }}
|
|
cache: pip
|
|
cache-dependency-path: pyproject.toml
|
|
|
|
- run: pip install -e ".[dev]"
|
|
|
|
# Includes tests/test_gallery.py, the dual-platform readability gate: every
|
|
# screen has to stay readable at 72 columns regular and 53 on the PicoCalc, and
|
|
# every PicoCalc case is a hard failure rather than a warning.
|
|
- run: python -m pytest -q
|
|
|
|
# Builds the distributions and checks them the way PyPI would, publishing nothing.
|
|
# Worth its minute: packaging breaks silently and is otherwise only discovered on
|
|
# the one day you least want to discover it. The wheel is installed into a clean
|
|
# environment and the command run from outside the source tree, because "it built"
|
|
# and "it works when installed" are different claims.
|
|
build:
|
|
name: build
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- run: pip install build twine
|
|
|
|
- run: python -m build
|
|
|
|
- run: twine check dist/*
|
|
|
|
- name: Install the wheel clean and run the command
|
|
run: |
|
|
python -m venv /tmp/fresh
|
|
/tmp/fresh/bin/pip install --quiet dist/*.whl
|
|
cd /tmp
|
|
/tmp/fresh/bin/meshterm --help > /dev/null
|
|
# A tool-provided subcommand, not just --help: the tools are discovered at
|
|
# runtime, so a broken package still answers --help perfectly well.
|
|
/tmp/fresh/bin/meshterm contacts --help > /dev/null
|
|
echo "the wheel installs, the console script runs, and its tools registered"
|