Commit Graph
4 Commits
Author SHA1 Message Date
funkpower 0e8ffcebf1 put the venv first on PATH, or maturin cannot find zig
The release job failed on its first run. maturin locates zig by running
`python3 -m ziglang`, and `maturin[zig]` installs ziglang into the venv only,
so calling maturin by absolute path left a bare python3 resolving to the
system one, which does not have it. It died with 'Failed to find zig' after
apt and rustup had already run, and published nothing.

Reproduced locally, both the failure and the fix: with the venv first on PATH
the same command builds a manylinux_2_17 wheel, which is the whole reason for
--zig. The step now also prints the zig version before the loop, so a missing
toolchain fails in one line rather than three minutes in, and pip is no longer
quiet, so the log shows what was installed.
2026-09-18 13:58:51 -04:00
funkpower 7970f785c8 fix what a 1.0 would have welded into PyPI metadata
An adversarial review of the release, and four of its findings were things
PyPI never lets you replace once uploaded.

  - rust-version said 1.74. clap has needed 1.85 since 4.6, and cargo
    hard-errors rather than warning, so the sdist advertised a floor it could
    not build on. docs/releasing.md sends macOS and Windows users to that
    sdist.
  - The wheel was classified Development Status :: 4 - Beta on a release the
    changelog calls stable, with none of the Python version classifiers PyPI
    indexes on.
  - Six README links were relative. PyPI resolves them against the project
    page, so every one 404'd there, as they do on the live 0.2.0 page now.
  -  in that order packs the wheel into the
    sdist. The tarball on disk was 431KB with an x86-64 binary inside it.

Also: twine upload gains --skip-existing, so a retry after a partial upload
finishes rather than dying on a version that cannot be reused; twine check
gains the renderer that makes it parse the description at all, without which
it returns None and reports PASSED; and the README no longer claims a
twenty-year sweep the test did not run, because 366 * 21 / 4 is five years.
The test now runs the twenty-one it was named for.

The crates.io name belongs to an unrelated project, which the README now says
rather than leaving a reader to find out from cargo.
2026-09-18 13:41:41 -04:00
funkpower bccf5c4ad8 clone by hand; the runner has no node
Every run died at the first step: "node: executable file not found in $PATH".
actions/checkout is a JavaScript action, and the runner executes it with node
*inside the job container* — which here is a Rust image that has never carried
one. actions/cache and actions/upload-artifact are the same.

The usual answer is an image carrying node as well, which for a Rust project
means installing the toolchain on every run. Cloning by hand is four lines and
keeps the lean image, and the repository is public so it needs no credential.

release.yml collapses to a single job for the same reason. Build and publish were
split across four, which meant handing wheels between them through
upload-artifact — another JS action. Everything a release needs fits in one job,
so nothing has to be handed over at all.
2026-09-18 12:50:56 -04:00
funkpower 792fd1d26f build it on tags, not on trust
Tag-driven releases. main is always releasable, a vX.Y.Z tag on it builds the
sdist and the manylinux wheels and publishes them; Cargo.toml carries the version
and the first job refuses to go on if the tag disagrees with it, because PyPI
will not let a version be reused even after it is yanked.

CI fires on push and on nothing else. This forge is public and takes pull
requests from strangers, and a runner executes whatever the workflow checks out,
so a pull_request trigger would run unread code from a stranger on the runner
host. Push-only means the runner only ever runs what someone with push rights put
there. A pull request therefore gets no automatic check: read it, push the branch,
let it run then.

No cargo fmt gate. The tables here are aligned by hand and rustfmt wants one entry
per line, which loses the shape that makes MONTHS and JURISDICTIONS readable — a
gate that fights the house style is a gate that gets ignored. Clippy stays, and
passes.

The Python job builds the bindings against the CLI it just built and runs them
against each other, because they are one engine and the only way that stays true
is if something checks.

docs/releasing.md covers what does not exist yet: Actions is off on this forge on
purpose, there is no runner, and PyPI's trusted publishing does not speak to
Forgejo — so a long-lived token has to live here, project-scoped rather than
account-wide so a compromised forge costs this package rather than all of them.
2026-09-18 12:03:43 -04:00