Files

146 lines
7.1 KiB
YAML
Raw Permalink Normal View History

2026-09-18 12:03:43 -04:00
name: release
# Tag-driven. Cargo.toml carries the version; the tag only points at it, and the
2026-09-18 12:50:56 -04:00
# first step refuses to go on if the two disagree — a release tagged v0.3.0 that
# publishes 0.2.0 cannot be undone, because PyPI will not let a version be reused
# even after it is yanked.
2026-09-18 12:03:43 -04:00
on:
push:
tags: ['v*']
2026-09-18 12:50:56 -04:00
# One job on purpose. Splitting build from publish would mean passing wheels
# between jobs through actions/upload-artifact, which is a JavaScript action: the
# runner executes it with `node` inside this container, and a Rust image has none.
# Everything a release needs fits in one job, so nothing has to be handed over.
2026-09-18 12:03:43 -04:00
jobs:
2026-09-18 12:50:56 -04:00
publish:
2026-09-18 12:03:43 -04:00
runs-on: docker
container:
image: rust:1.98-bookworm
steps:
2026-09-18 12:50:56 -04:00
- name: Checkout
run: |
git init -q .
git remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
git fetch -q --depth 1 origin "$GITHUB_SHA"
git checkout -q FETCH_HEAD
2026-09-18 12:03:43 -04:00
- 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; }
2026-09-18 12:50:56 -04:00
# What CI runs on every push, run again here. A release is the one build
# nobody gets to re-run after the fact: PyPI will not reuse a version and
# neither will npm, so the gate belongs before the upload rather than
# only on the branch that led to it.
2026-09-18 12:03:43 -04:00
- name: Tests
run: |
rustup component add clippy
cargo clippy --all-targets -- -D warnings
cargo clippy --features python --all-targets -- -D warnings
cargo clippy --features capi --all-targets -- -D warnings
rustup target add wasm32-unknown-unknown
cargo clippy --lib --features wasm --target wasm32-unknown-unknown -- -D warnings
cargo test
cargo test --features capi
2026-09-18 12:03:43 -04:00
2026-09-18 12:50:56 -04:00
- name: Build wheels and sdist
2026-09-18 12:03:43 -04:00
run: |
apt-get update && apt-get install -y --no-install-recommends python3 python3-venv
python3 -m venv /tmp/venv
# readme-renderer[md] is what makes `twine check` actually parse the
# long description. Without it the render returns None and the
# check reports PASSED regardless, even with --strict.
/tmp/venv/bin/pip install 'maturin[zig]' twine 'readme-renderer[md]'
# The venv has to be first on PATH, not merely called by absolute
# path. maturin looks for zig by running `python3 -m ziglang`, and
# `maturin[zig]` puts ziglang in the venv only, so a bare python3
# resolves to the system one, which does not have it: "Failed to find
# zig", after apt and rustup have already run.
export PATH="/tmp/venv/bin:$PATH"
python3 -m ziglang version
2026-09-18 12:50:56 -04:00
# --zig cross-compiles against an older glibc than this image ships, so
# the wheel is not silently pinned to bookworm's.
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
rustup target add "$target"
maturin build --release --zig --target "$target" --out dist
2026-09-18 12:50:56 -04:00
done
maturin sdist --out dist
2026-09-18 12:50:56 -04:00
ls -l dist
2026-09-18 12:03:43 -04:00
- name: Build the npm package
run: |
# Node from nodesource rather than apt. `npm stage` below wants npm
# 11.15 and Node 22.14, and bookworm ships Node 18.
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt-get install -y --no-install-recommends nodejs
npm install --global npm@^11.15
rustup target add wasm32-unknown-unknown
version=$(awk '/^name = "wasm-bindgen"$/{getline; gsub(/[",]/,""); print $3; exit}' Cargo.lock)
curl -sSfL "https://github.com/rustwasm/wasm-bindgen/releases/download/$version/wasm-bindgen-$version-x86_64-unknown-linux-musl.tar.gz" \
| tar xz -C /tmp
export PATH="/tmp/wasm-bindgen-$version-x86_64-unknown-linux-musl:$PATH"
npm install --global typescript@5
cargo build --release
npm/build.sh
node npm/test.mjs
# The whole point of a release is that every door leads to the same
# engine. Checked here against what is about to be uploaded, and again
# after the fact with --published against what actually was.
- name: Every binding agrees
run: |
apt-get install -y --no-install-recommends chromium luajit
version=$(awk '/^name = "wasm-bindgen"$/{getline; gsub(/[",]/,""); print $3; exit}' Cargo.lock)
export PATH="/tmp/wasm-bindgen-$version-x86_64-unknown-linux-musl:$PATH"
tests/parity.sh
python3 tests/readme-examples.py
2026-09-18 12:03:43 -04:00
- name: Publish
env:
2026-09-18 12:50:56 -04:00
# A project-scoped token, not the account-wide one. PyPI's trusted
2026-09-18 12:03:43 -04:00
# 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
2026-09-18 12:50:56 -04:00
# mitigation available: if the forge is compromised, what is lost is the
# ability to publish typikon rather than every package the account owns.
2026-09-18 12:03:43 -04:00
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
# By absolute path: each step gets its own shell, so the PATH the
# build step exported for zig's sake does not reach this one.
/tmp/venv/bin/twine check --strict dist/*
# --skip-existing so a retry after a partial upload finishes the job
# instead of dying on the first file. A version cannot be reused,
# so a half-published release would otherwise burn it.
/tmp/venv/bin/twine upload --non-interactive --skip-existing dist/*
# Staged, not published. npm is withdrawing direct publish from any token
# that bypasses 2FA, and its trusted publishing is OIDC against GitHub
# Actions and GitLab, which this forge is neither. So the runner does the
# part a token may do and stages the tarball, and a maintainer approves it
# with 2FA: `npm stage list`, then `npm stage approve <id>`, or the Staged
# Packages tab on npmjs.com. Nothing reaches the registry until then.
- name: Stage the npm release
env:
# Granular, write on typikon alone, and it does not need to bypass
# 2FA, because staging is exactly what a token that cannot bypass it
# is still allowed to do.
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "$NPM_TOKEN" ]; then
echo "::warning::NPM_TOKEN is not set; nothing staged on npm"
exit 0
fi
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
version="${GITHUB_REF_NAME#v}"
if npm view "typikon@$version" version >/dev/null 2>&1; then
echo "::notice::typikon@$version is already published"
exit 0
fi
cd npm/dist && npm stage publish --access public
echo "::notice::staged typikon@$version; approve it with 2FA to publish"