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>
This commit is contained in:
1 parent
365df236b9
commit
117b958edf
4 files changed
+172
-2
No files matched your search
@@ -84,3 +84,32 @@ jobs:
|
||||
# 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"
|
||||
@@ -0,0 +1,99 @@
|
||||
# Publishing to PyPI. Deliberately hard to fire.
|
||||
#
|
||||
# There is no `push: tags` trigger here, and that absence is the point. A release
|
||||
# workflow that publishes on a `v*` tag would mean tagging v0.9.0 on launch day
|
||||
# silently claims a distribution name on a public index, permanently, as a side
|
||||
# effect of an unrelated action. PyPI has no rename and no true delete, so that
|
||||
# side effect is not one you can take back.
|
||||
#
|
||||
# So: a human runs this, from the Actions tab, on purpose, and types the version
|
||||
# to prove they meant this version. Three gates before anything leaves the runner:
|
||||
#
|
||||
# 1. no automatic trigger at all
|
||||
# 2. the typed version must equal the version actually built
|
||||
# 3. the `pypi` environment, which can require a reviewer in repo settings
|
||||
#
|
||||
# It publishes whatever version is in the tree. There is no version argument, so
|
||||
# this cannot publish something other than what is committed.
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm_version:
|
||||
description: "Version to publish, exactly (e.g. 0.9.0). A mismatch aborts."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build and check
|
||||
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
|
||||
|
||||
# Renders the README the way PyPI will, and fails on metadata PyPI would reject.
|
||||
- run: twine check dist/*
|
||||
|
||||
# The version the build actually produced, read off the artifact rather than
|
||||
# trusted from anywhere else.
|
||||
- id: built
|
||||
shell: bash
|
||||
run: |
|
||||
version=$(ls dist/*.tar.gz | sed -E 's/.*-([0-9][^-]*)\.tar\.gz/\1/')
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "built version: $version"
|
||||
|
||||
- name: Confirm the typed version matches what was built
|
||||
shell: bash
|
||||
env:
|
||||
TYPED: ${{ inputs.confirm_version }}
|
||||
BUILT: ${{ steps.built.outputs.version }}
|
||||
run: |
|
||||
if [ "$TYPED" != "$BUILT" ]; then
|
||||
echo "::error::You typed '$TYPED' but the tree builds '$BUILT'. Nothing published."
|
||||
exit 1
|
||||
fi
|
||||
echo "confirmed: $BUILT"
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
publish:
|
||||
name: publish to PyPI
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
# Configure this environment in repo settings with a required reviewer, and it
|
||||
# becomes a fourth gate: the job waits for an approval before it runs. The name
|
||||
# must also match the environment set on the PyPI trusted publisher, or the OIDC
|
||||
# exchange fails with an unhelpful error. If the publisher has no environment
|
||||
# configured, delete this line.
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/mesh-term
|
||||
permissions:
|
||||
# Trusted Publishing mints a short-lived OIDC token instead of storing an API
|
||||
# token in the repository. This is the only thing that needs write here.
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1
|
||||
@@ -9,6 +9,22 @@ allowed to change behaviour, not just add to it.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
|
||||
- The distribution is named `mesh-term`. PyPI prohibits `meshterm` (pypi/support#12162),
|
||||
and the name you install is the only thing that changes: the import package and the
|
||||
command are both still `meshterm`.
|
||||
- `pyproject.toml` carries the metadata a stranger sees on a package page — project URLs,
|
||||
trove classifiers, and the licence as a PEP 639 expression.
|
||||
|
||||
### Added
|
||||
|
||||
- Continuous integration. Lint once, tests across seven OS and Python combinations, and a
|
||||
packaging job that installs the built wheel into a clean environment and runs the command
|
||||
from outside the source tree.
|
||||
- A release workflow with no automatic trigger, which publishes nothing until a human runs
|
||||
it on purpose and types the version.
|
||||
|
||||
## [0.2.0] — 2026-09-07
|
||||
|
||||
The first version to carry a changelog. Everything before it is in the commit log and is
|
||||
|
||||
+28
-2
@@ -3,14 +3,34 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "meshterm"
|
||||
# The distribution name only. The import package and the console script both stay
|
||||
# `meshterm` — PyPI prohibits that name (see pypi/support#12162), and nothing about the
|
||||
# app changes because of it. `[tool.hatch.build.targets.wheel]` below is what keeps the
|
||||
# wheel shipping the `meshterm/` package under this different name.
|
||||
name = "mesh-term"
|
||||
dynamic = ["version"]
|
||||
description = "MeshTerm is a full-featured TUI-based MeshCore client for your terminal. Supports USB, Bluetooth and TCP companion connection. For Windows, macOS and Linux."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
authors = [{ name = "Jean-Pierre Martineau" }]
|
||||
license = { text = "Apache-2.0" }
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE", "NOTICE"]
|
||||
keywords = ["meshcore", "lora", "mesh", "radio", "cli"]
|
||||
# No `License ::` entry: PEP 639 forbids one alongside the SPDX expression above.
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Communications :: Ham Radio",
|
||||
"Topic :: System :: Networking",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"meshcore>=2.3", # companion-device protocol (serial/BLE/TCP)
|
||||
@@ -34,6 +54,12 @@ dev = [
|
||||
"ruff>=0.4",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/jpmartineau/MeshTerm"
|
||||
Repository = "https://github.com/jpmartineau/MeshTerm"
|
||||
Issues = "https://github.com/jpmartineau/MeshTerm/issues"
|
||||
Changelog = "https://github.com/jpmartineau/MeshTerm/blob/main/CHANGELOG.md"
|
||||
|
||||
[project.scripts]
|
||||
meshterm = "meshterm.cli:main"
|
||||
|
||||
|
||||
Reference in new issue
Block a user