Files
MeshTerm/.github/workflows/release.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

100 lines
3.3 KiB
YAML

# 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