Files
MeshTerm/.github/workflows/ci.yml
T
Jean-Pierre MartineauandClaude Opus 5 117b958edf Everything PyPI needs, and no way to reach it by accident
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>
2026-09-07 04:00:56 -04:00

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"