installers.yml gains a cardputer-zero job: build-deb.sh in a Debian 13
container on an arm64 runner, the same system as the Cardputer. The
release attaches the .deb with the other downloads and lists it in
SHA256SUMS, and its notes say where Cardputer users get MeshTerm.
github-release.yml gains a store job after the release. It runs
scripts/cardputer-zero/store-publish.py, which calls czdev publish with
two corrections: the manifest's email is johnputer@meshterm.net, not
the GitHub profile's, and the branch name stays the same between
czdev's two calls (for a fork, the second call named a branch that was
never pushed, and GitHub refused the PR). It also sends czdev's git
over HTTPS with the token, and it publishes nothing when the store has
the version or a PR for it is open, so a rerun opens no second PR.
AppBuilder is pinned to the commit whose functions it corrects.
The job needs the CARDPUTER_STORE_TOKEN secret (public_repo scope). The
release skill now checks the store PR and counts fifteen assets.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README, CONTRIBUTING, SECURITY, the docs index, the development
documents, the script READMEs, the comments of config.example.toml and
pyproject.toml, and the issue and pull request forms. The legal sections
of the README (attribution and licence) stay exactly as they were, between
ste: off and ste: on markers. The SignPath attribution line, the one-line
description of MeshTerm, every code block, and every URL do not change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
SignPath Foundation's terms make a person approve every signing request,
so the plan's automatic approval doesn't exist. The action gave up after
ten minutes, which would fail a release whenever the maintainer stepped
away; it now waits an hour, and the Windows job's timeout grows to make
room. Only a tag build or a run started by hand asks to be signed: a push
to main that touched the packaging builds unsigned instead of sending an
approval request nobody wanted. Still inert until the SignPath variable
exists.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Patreon becomes GitHub's built-in `patreon:` entry, so the sidebar shows
patreon.com/MeshTerm with its icon like Buy Me a Coffee, and the Support
page carries the same short URL (a smaller QR code too). PayPal stays the
hosted donate button, only without its scheme in FUNDING.yml: a paypal.me
link would let a donor pay as goods and services and claim a refund for an
item that never arrived, which the donate button's donation label avoids.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three pull requests against toy issues arrived from agents that farm open
issues for contribution stats. None had said a word to the project, and
none disclosed that AI wrote them.
CONTRIBUTING.md now says, in plain words, how MeshTerm was made and who
it wants: people who use it, AI-assisted or not. Before writing code, a
contributor says hello in the new #contributing channel on Discord and
waits for a yes; the pull request carries their Discord username and a
link to that yes. AI use has to be disclosed in the commits and the pull
request; undisclosed or unattended agent output is closed without review.
The PR template, the README's two notes on pull requests, and the docs
index all said small fixes could skip the conversation, and now match.
The release skill's commit carries the AI trailer its seven release
commits went out without.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every list of three or more takes the serial comma: the one-liner in
pyproject.toml, meshterm/__init__.py, and --help ("USB, Bluetooth, or TCP",
"Windows, macOS, and Linux"), the README, the changelog, the manuals, the
issue and PR templates, and six strings the app shows (the region-name
rule, a repeater setting's help, the diagnostics tool's description, and
the archive confirms). The Windows-console notice is rewrapped so its
comma keeps it inside 72 columns.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One line after the changelog text, with the reason: GitHub ranks projects
by stars in its search and topic pages, so a star helps other MeshCore users
find MeshTerm. It lives in the workflow's template rather than the changelog,
so every release carries it and the changelog stays a record of changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It only recognised trailers written with a colon, so "Fixes #12", which
GitHub closes on just the same, went unchecked, as did fix, fixed, close,
closed, resolve and resolved. The colon is now optional and every closing
keyword counts.
It also treated any failed lookup as a missing issue, so a rate limit or an
outage would have left a public comment on a commit saying a real issue did
not exist. Now only a 404 does that; anything else is a warning in the log.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Python 3.10 reaches end of life in October 2026, and the newer image that
ubuntu-latest moves to may not offer it. The leg would then fail on setup,
and the badge on the public README would turn red with nothing broken.
Pinning it keeps 3.10 tested for as long as MeshTerm says it supports it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JP's call: rebuild 0.9.0 rather than wait for 0.9.1. It has never been public, so the
release is recut from main: Python 3.14 inside the downloads, the corrected written
pages inside the app, companion and room server as the only names for those types, and
the legend that fits a PicoCalc.
[Unreleased] folds into the 0.9.0 entry. On a first release there is nothing for those
changes to have changed from, so they are not listed as changes; the entry gains the one
fact a reader needs, that pipx works on Python 3.10 to 3.14.
The release workflow writes the install half of the notes from its own template, which
still had the two lines fixed by hand on the first 0.9.0 page: "every Mac since 2020",
and "every file below" for a SHA256SUMS that lists only the binaries. Both are fixed at
the source. The changelog extraction also stops carrying the link definitions at the
foot of CHANGELOG.md into the notes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JP builds with 3.14 now, so the workflows do too. The lint job moves from 3.10, the
packaging build job and the release's site rebuild and PyPI build from 3.12, and the
installers from 3.10. The test matrix is untouched: it still runs every version from
3.10 up, because 3.10 is still the floor.
The installers' comment said the oldest Python was chosen on purpose, for reach. The
reach that matters on Linux comes from glibc, and that comes from the runner, so the
Linux legs stay on ubuntu-22.04 and the comment now says that. The binaries carry 3.14
from the next release; 0.9.0's were built on 3.10.
The changelog gains the 3.14 entry JP wrote in another session, plus a line for the
downloads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JP moved to 3.14. Every dependency installs on it -- the Windows Bluetooth packages
included -- and the suite passes: 2705 passed, 1 skipped, on 3.14.7 as on 3.10.
- pyproject gains the 3.14 classifier. requires-python stays >=3.10: the uConsole on
bookworm runs 3.11, and a pip or pipx install on an older system should keep working.
- CI keeps its shape, which is Linux over the whole range and the other two runners at
its ends. Linux gains a 3.14 leg, and 3.14 replaces 3.13 as the top end on Windows and
macOS.
- Three tests reached a loop through asyncio.get_event_loop_policy(), which 3.14
deprecates and 3.16 removes, and never closed the loop they made. The two countdown
tests now make one with new_event_loop() and close it; the sweep test uses
asyncio.run(), which does both. On 3.14 the suite is back to no warnings.
The standalone installers still bundle 3.10; moving their build to 3.14 is for 0.9.1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things on the new-issue page would have gone wrong for the first stranger to use it.
The second contact link was named `No GitHub account? Report it in the #bugs forum
instead`, unquoted. YAML reads ` #` as the start of a comment, so GitHub would have shown
"No GitHub account? Report it in the" and nothing after it. The name is quoted now.
The diagnostics and log fields said to attach a file, and both had `render:` set. GitHub
turns attachments off on any textarea that renders, so the one required field asked for
something it would not accept. Neither field renders now; the diagnostics block is
markdown and reads fine as is, and the log field says to fence pasted lines.
Also: the "not recognised? you are on 0.3.7" paragraph goes, since nobody outside ever ran
0.3.x; the Diagnostics page's save key is named for the PicoCalc too; Log detail is
spelled the way the Preferences page shows it and says it needs a restart; the Cardputer
leaves the placeholder, since nothing else mentions one. The pull-request template now
states the same rule as CONTRIBUTING, and FUNDING points at Patreon's canonical /cw/ URL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine of the bug form's fourteen fields were transcription: version, host, OS,
terminal, size, platform, device model, firmware, transport. `meshterm
diagnostics --out` writes every one of them, correctly, from the machine that
has the problem, and the form went on asking a tired person to copy them out by
hand.
A field that demands precision from someone who does not have it does not
produce precision. It produces an abandoned form, or an invented answer -- a
reporter guessing at their radio's chip name because the form would not let them
past without one. A guess is worse than a blank, because it reads as a fact.
So the required set drops from nine fields to five, and not one of the five asks
for something the reporter has to look up: what happened, how often, how to
repeat it, whether `--mock` changes it, and the diagnostics block itself.
Two of the cut fields came back as optional, because the block genuinely cannot
know them. It reads the model and firmware off the radio, so a report about a
radio that will not connect -- the most common first-run report there is -- has
nothing to read them from. And `platform` only separates PicoCalc from regular,
so a uConsole and a laptop look alike to it. Both fields now say exactly when
they apply, which is rarely.
The command itself lands in 0.9.0 and the released build is 0.3.7, so both the
form and the forum say what to do when it is not recognised: name your version
and send what you can. That line comes out once nobody is running an older one.
The trailer workflow inverts the obvious design. It does not nag about commits
with no issue trailer, because most commits here legitimately close nothing and
a workflow that complains about all of them gets muted within a week. It checks
the trailers that are there: `Fixes: #42` against an issue that does not exist
is not an error in GitHub's eyes, it is silence -- the issue stays open, the
reporter is never told, and the commit looks like it did the job. That is the
failure worth a comment.
Four labels join the stock set for the states a report actually passes through:
needs-diagnostics, confirmed, needs-hardware, cannot-reproduce. They say what
state a thing is in. The hardware goes in the title, where it does not
multiply.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A browser download is the one route that does not work on a Mac, and the
README said so in a note *below* the commands -- which is no help to
somebody who followed the commands. macOS now leads the download section
with curl, states why in its first sentence, and Linux follows in the same
three lines.
Those three lines name no version, because the release now attaches a
version-less copy of every binary beside the stamped one. That is what
makes releases/latest/download/meshterm-macos-arm64 resolve, so the README
points at whichever release is newest instead of at one that was current
when somebody last remembered to grep the old number out of it. The
stamped files stay for anyone keeping several builds around.
And the release page itself now opens with those commands written out for
its own version, above the changelog, built from the tag -- so the links in
the example are the assets attached to the page printing them.
The release skill loses the step that re-pointed the README by hand and
gains the asset count to check instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Windows greets an unsigned one-file PyInstaller build as a stranger every
release: SmartScreen warns, and antivirus engines flag it on sight. A
signature gives Windows a publisher to accumulate trust against instead.
SignPath Foundation issue the certificate free to open-source projects and
sign on their own infrastructure, so no key exists on the runner. Their
condition is that the artifact provably came from this repository's CI,
which is why the binary is handed over as a GitHub artifact id rather than
uploaded -- SignPath reads the workflow run behind it.
Signing sits between the build's smoke test and the naming step, so the
checksum describes the file people actually download. The unsigned draft
is the one artifact the release must not attach, so it is named outside
the meshterm-* pattern the release now downloads by.
Both steps skip wholesale until SIGNPATH_ORGANIZATION_ID is set, so the
Windows binary keeps building unsigned in the meantime and a fork with no
SignPath project of its own is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Support page's text and QR code and the repository's funding file all pointed at a
tinyurl redirect, which a reader could not audit; they now carry the hosted-button address
itself. The page still renders on both platforms.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
github-release.yml gains a website job: once the release is published it rebuilds the meshterm.net site from the tagged tree and pushes it back with a deploy key scoped to that one repository. The release skill says what to do when only that job fails.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The five installers bundled Cascadia's font licence and nothing else --
not MeshTerm's own LICENSE and NOTICE, not the MIT/BSD/PSF notices
Apache-2.0 and those licences all want kept with a redistributed copy.
packaging/notices.py walks the mesh-term distribution's actual runtime
dependency closure through importlib.metadata (markers evaluated for
this platform, the dev extra excluded), gathers each package's licence
text, and fails the build loudly if one is missing rather than shipping
a silent gap -- which is exactly what it found: pyserial and the nine
winrt-* packages bleak's Windows backend pulls in all declare a licence
but ship no licence file in their wheel at all. Their real text, pulled
from each project's own repository, is vendored under
packaging/vendored-licenses/ for those two documented, verified gaps;
anything else missing still stops the build.
The spec generates THIRD-PARTY-NOTICES.txt fresh into the build
directory on every run and bundles it at the archive root alongside
LICENSE and NOTICE. installers.yml uploads the same three files once,
from the linux-x64 leg, so they land beside the downloads on the
release page too, without extracting an installer just to read them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The relicensing grant binds by conduct only if the contributor had a fair chance to see it, and the template mentioned CONTRIBUTING only inside an HTML comment the rendered PR never shows. One visible checkbox at the top of the checklist.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Hatchling's default sdist is everything git does not ignore, which packed the
Claude skills, the GitHub configuration, the PyInstaller spec, the device-side
scripts and the developer environment template into an archive whose only job
is to build and test the wheel. An exclude list keeps the package, the tests
and their fixture, the manual and the files a build reads. The build backend
floor is hatchling 1.26, the first to write the PEP 639 licence fields into the
metadata; an older one built a wheel that said nothing about the licence.
Workflow comments that reasoned from a private repo's metered minutes are
reworded for the public one, a skill's usage line stops naming one machine's
venv, and the scheduler's lock file is ignored in the tree rather than only in
one clone's info/exclude.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three things were true of the log at once: it was JSON Lines, it never rotated,
and it almost never recorded a failure. Of the last twenty thousand records in a
real one, nine were warnings and three were errors. It faithfully kept the boring
parts and missed the interesting ones, which is exactly backwards for the only
job it has.
It is plain text now, `~/.meshterm/meshterm.log`, one line per record with the
traceback indented under the line that raised it. JSON Lines was there in case
something replayed it; nothing ever did, and the actual reader is a person who has
hit a problem and is about to paste the last twenty lines into an issue. That
reader has a text editor.
It rotates, at 2MB across three files. The old handler grew without limit and a
real one reached 11MB over ten weeks — a lot of disk on a handheld, and an
unreasonable thing to ask someone to attach.
How much it keeps is a preference, `log_level` under a new Diagnostics group,
defaulting to WARNING. That is what makes the file useful rather than large: it
holds the problems, and turning it up to DEBUG for one run is how you chase
something. Loaded before logging is configured, since it decides how logging is
configured, and handed to the context afterwards so the file is not read twice.
And failures are written down at last. Every unhandled error reaches the log with
its traceback and the terminal says where to find it; expected errors — an
ambiguous device, a bad preference — go in as warnings, because a log that only
holds crashes cannot answer "what happened just before". The bug form now asks for
the file, which it had no reason to before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
`macos-13` is retired. GitHub's image list starts at macOS 14 now, and the
current x86_64 labels are `macos-15-intel` and `macos-26-intel`.
The failure mode is worth writing down, because it is not the one you would
expect: a job asking for a retired label does not fail. It queues, forever,
and holds the entire run open behind it — so the four legs that had already
built sat there under a run that never finished, and anything waiting on that
workflow would have waited with them. That includes the release workflow, which
calls it, so a tag pushed today would have produced no release and no error to
explain why.
`macos-15-intel` rather than `-26-intel`, for the same reason the Python in this
matrix is 3.10: built against an older system, runs on more machines.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pushing a version tag now builds the binaries and puts them on the Releases page,
with a combined SHA256SUMS and notes lifted out of the changelog section for that
version. It stays inside GitHub — while the repository is private the release is
private too — and it is emphatically not the PyPI publish, which is `release.yml`
and still needs a person to run it on purpose.
`installers.yml` gained a `workflow_call` trigger so the release calls it instead
of keeping a second copy of the matrix. Two matrices would agree for about a month
and then quietly stop, and the files people download should be built exactly the
way the ones CI checks on every push are.
The README now leads its install section with the download rather than with pip,
because most people who want MeshTerm have a radio and a terminal and not a Python
toolchain. It also says plainly that the first launch will be blocked: these builds
are unsigned, macOS will refuse them until the quarantine attribute is cleared, and
Windows will show its SmartScreen warning. Signing costs real money on both
platforms, so the honest move is to say so and give people the checksum file rather
than let them think something is wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first CI run went red on `ruff format --check`, on a tree that was clean on
this machine. Three separate problems, all found by pushing.
`ruff>=0.4` is not a pin. CI installed 0.16.6 where the laptop had 0.15.18, and
0.16 formats Python code blocks inside Markdown while 0.15 leaves them alone — so
the two disagreed about whether the repository was formatted, which is the one
thing a formatter exists to settle. Pinned to the 0.16 line. Dependabot will
offer the next one, and CI will say whether it is safe.
The file it tripped over is `docs/survey-ui-consistency.md`, which quotes four
lines of a dialog call as it looked on 22 August. That excerpt is a fragment —
two arguments, no call around them — so the formatter parsed it as two statements
and rewrote them into a tuple and an assignment that never existed anywhere. The
surveys are dated records of a past state, so `docs/` is now excluded: a
formatter editing a quotation is not tidying, it is falsifying.
And the installer workflow was firing for branches it had no business building.
Dependabot opened three action bumps within a minute of the push; each one edits
`.github/workflows/installers.yml`, each matched its own path filter, and each
started a five-platform build. `branches: [main]` stops it.
Everything else was green on the first try, including the part that was actually
uncertain: all seven test legs passed, macOS among them, and the installers built
on Windows, macOS on Apple silicon, and Linux on both x64 and arm64 — so the
uConsole build works and the ARM runner was available after all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Most people who want MeshTerm have a radio and a terminal, not a Python
toolchain. This is the install for them: one file, download it, run it.
`packaging/meshterm.spec` freezes the app with PyInstaller in console mode —
which is not a detail, since a windowed build of a terminal program starts with
nowhere to draw and exits. The assets land at `meshterm/assets` inside the bundle
so that `Path(__file__).parent.parent / "assets"` keeps resolving exactly as it
does in a checkout, and bleak's per-platform backends are named as hidden imports
because nothing static ever sees the one that actually gets used. UPX is off: a
few megabytes are not worth being flagged by antivirus on a first release.
The build was run here rather than only written. The Windows executable comes out
at 14 MB, runs from a directory with no source tree in sight, and draws the
specimen card — which is the real test, because the specimen reads the asset
files.
`installers.yml` does the same on five machines, because frozen builds cannot be
cross-compiled. Windows, macOS on both Apple silicon and Intel (plenty of people
still have one, and an arm64 build will not run for them), and Linux on x64 and
ARM64 — the last for the uConsole. Each leg runs the binary from a temporary
directory before uploading it, so a build that only works beside its own source
fails on the runner instead of on somebody's laptop, and each one gets a checksum
written beside it. It publishes nothing; attaching these to a release stays a
separate decision.
Also here: `meshterm --help` now opens with the same sentence as the package
summary, rather than a fourth description of the project written in its own way.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bug form now asks what JP's Discord post asks: how often it happens, whether
there's a workaround, what hardware you're on, and which companion device — with
the note that "a Xiao" doesn't narrow anything down. Three things his post didn't
cover got added: the MeshTerm version, whether it still happens under `--mock`
(which splits an app bug from a radio bug before anyone spends an evening on it),
and whether you're on the regular or PicoCalc look, since those draw differently
enough that a bug in one often doesn't exist in the other.
The README gains the part that was missing: what to expect from a maintainer
doing this on evenings and weekends. Issues welcome, ask before opening a pull
request, no promised timelines, and a nudge on a quiet issue is fine. Report on
GitHub or on Discord, whichever you have — the real ones get moved across either
way, and being told beats filing it tidily.
Install is rewritten for someone who wants to run MeshTerm rather than work on
it. It said `pip install -e ".[dev]"`, which is a contributor's command and puts
pytest and ruff on the machine of somebody who just wants to talk to a radio. Now
it's one pip line, a pipx alternative, `--mock` for people without a radio yet,
and an honest "coming" list for installers and PyPI.
Badges now include CI, the licence and Discord, so they say something checkable
rather than only decorating. The two survey documents get a line at the top
saying they're a snapshot of a date, not a to-do list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rest of the community files, which are the half of open-sourcing that is not
code: how someone tells you about a hole, what happens if a thread turns nasty,
and what you will be asked when they file a bug.
SECURITY.md points at GitHub's private vulnerability reporting rather than an
address that will be scraped, and promises only what one person in his spare time
can keep — an acknowledgement inside a week, which is not a fix inside a week, and
a decision inside ninety days. Its scope section is the MeshTerm-specific part: a
channel key or an admin password reaching a log, an export or the screen is a
vulnerability here, a crafted packet corrupting the observation database is one,
and a hole in the MeshCore firmware or in `bleak` is somebody else's to fix. It
also says plainly that the database is not encrypted, because a backup of it
carries a mesh's whole history.
The code of conduct is Contributor Covenant 2.1, taken verbatim from source rather
than paraphrased, with a real address in the enforcement section.
The bug form asks the seven things that otherwise get asked in every thread:
version, flavour, transport, OS and Python, terminal and its size, companion
firmware. Flavour is there because the PicoCalc draws itself in 53 columns and
sixteen colours, so a layout bug in one flavour frequently does not exist in the
other. It asks for the version from the splash screen rather than from
`meshterm --version`, which does not exist yet.
Dependabot is weekly and capped low, with minor and patch grouped into one pull
request: a solo maintainer facing five dependency PRs every Monday stops reading
any of them. Funding points at the three destinations the app's own support page
already offers, so the two cannot drift.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
`.github/` starts here: the three commands the contributing guide asks a
contributor to run, run for them on every push and pull request.
Two jobs rather than one. Style is a property of the source, so `lint` runs once
on Linux and a failure means one thing. Behaviour is a property of the machine,
so `test` runs across the systems the description now promises — Windows, macOS
and Linux — which until this file existed had only ever been checked on one of
them, whatever the description said.
The matrix is shaped for a private repository, where minutes are metered and
billed by a multiplier: Linux 1x, Windows 2x, macOS 10x. Linux therefore carries
the whole 3.10 to 3.13 range, and the expensive runners cover its ends, since a
version-dependent break shows up at an end rather than in the middle. Seven legs,
about fifty billed minutes a run. The comment says to widen it to the full
product once the repo is public and standard runners stop costing anything.
Two small decisions worth their lines. `fail-fast` is off: the point of a run is
the whole picture of what broke, and a first red leg cancelling its siblings
hides the rest until the next push. And `concurrency` cancels a superseded run,
because a second push makes the first run's answer obsolete and those are metered
minutes spent on a stale commit.
One line of `.gitignore` came with it. The catch-all `.*/` that hides `.venv`
and the caches was hiding `.github/` too — the same rule `.claude/` already needed
an exception to. It fails silently in the worst way: a workflow that is never
committed looks exactly like a workflow that never runs.
The version risk this was expected to surface is already gone: the suite was run
locally on 3.13 as well as the usual 3.10, and both report 1888 passing. What
remains genuinely unchecked is macOS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>