The distribution is named `mesh-term`, because `meshterm` is on PyPI's prohibited list and a request to free it is 319 deep in a queue. The name after `pip install` is the only thing that changes: the import package is still `meshterm`, the console script is still `meshterm`, and nothing in the code reads the distribution name, so this is one line rather than a rename. Proven rather than assumed — the wheel was built, installed into a clean interpreter, and the command run from outside the source tree. The rest is what a stranger sees on a package page and what the app never shows them: project URLs, trove classifiers for the console, the three operating systems and the four Python versions, and the licence moved to its PEP 639 expression form. No `License ::` classifier, which that form forbids, and no `Typing :: Typed` either — there is no `py.typed` marker, and a classifier that claims something untrue is worse than an absent one. `release.yml` publishes to PyPI and is built to be hard to fire. It has no `push: tags` trigger, and that absence is the design: a workflow that published on a `v*` tag would make tagging v0.9.0 on launch day claim a name on a public index, permanently, as a side effect of an unrelated action — and PyPI has no rename and no real delete. So a human runs it from the Actions tab and types the version, the job compares that against the version the tree actually built and refuses on a mismatch, and a `pypi` environment can hold it for a reviewer. Trusted Publishing means there is no API token in the repository to leak. It publishes whatever version is committed; there is no version argument, so it cannot publish something other than what is there. CI gains a packaging job that does the same build and clean-install check on every push, publishing nothing. Packaging breaks quietly, and the alternative is finding out on the one day that is worst for finding out. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
116 lines
4.0 KiB
YAML
116 lines
4.0 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
|
|
echo "the wheel installs and the console script runs"
|