Files
typikon/.forgejo/workflows/release.yml
T
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

97 lines
3.2 KiB
YAML

name: release
# Tag-driven. Cargo.toml carries the version; the tag only points at it, and the
# first job refuses to go on if the two disagree — a release tagged v0.3.0 that
# publishes 0.2.0 is not recoverable, because PyPI will not let a version be
# reused even after it is yanked.
on:
push:
tags: ['v*']
jobs:
verify:
runs-on: docker
container:
image: rust:1.98-bookworm
steps:
- uses: actions/checkout@v4
- name: Tag must match Cargo.toml
run: |
tag="${GITHUB_REF_NAME#v}"
crate=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)
echo "tag=$tag crate=$crate"
[ "$tag" = "$crate" ] || { echo "::error::tag $tag != Cargo.toml $crate"; exit 1; }
- name: Tests
run: cargo test
build:
needs: verify
runs-on: docker
container:
image: rust:1.98-bookworm
strategy:
matrix:
target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu]
steps:
- uses: actions/checkout@v4
- name: Build wheel
run: |
apt-get update && apt-get install -y --no-install-recommends python3 python3-venv
python3 -m venv /tmp/venv
/tmp/venv/bin/pip install --quiet maturin[zig]
# --zig cross-compiles against an older glibc than the build image
# ships, so the wheel is not silently pinned to bookworm's.
/tmp/venv/bin/maturin build --release --zig \
--target ${{ matrix.target }} --out dist
- uses: actions/upload-artifact@v3
with:
name: wheels-${{ matrix.target }}
path: dist
sdist:
needs: verify
runs-on: docker
container:
image: rust:1.98-bookworm
steps:
- uses: actions/checkout@v4
- name: Build sdist
run: |
apt-get update && apt-get install -y --no-install-recommends python3 python3-venv
python3 -m venv /tmp/venv
/tmp/venv/bin/pip install --quiet maturin
/tmp/venv/bin/maturin sdist --out dist
- uses: actions/upload-artifact@v3
with:
name: sdist
path: dist
publish:
needs: [build, sdist]
runs-on: docker
container:
image: python:3.12-bookworm
steps:
- uses: actions/download-artifact@v3
with:
path: dist
- name: Collect
run: |
mkdir -p upload
find dist -name '*.whl' -o -name '*.tar.gz' | xargs -I{} cp {} upload/
ls -l upload
- name: Publish
env:
# A PROJECT-SCOPED token, not an account-wide one. PyPI's trusted
# publishing is OIDC and supports GitHub, GitLab, Google Cloud Build and
# ActiveState — not Forgejo — so this forge has to hold a long-lived
# credential. Scoping it to this one project is the whole of the
# mitigation available: if the forge is compromised, the blast radius is
# releases of typikon rather than every package the account owns.
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
pip install --quiet twine
twine check upload/*
twine upload --non-interactive upload/*