mirror of
https://git.pumpin.online/funkpower/typikon.git
synced 2026-10-22 18:57:13 +00:00
There is one engine and nine doors into it, and nothing until now checked that the doors agree. CI tested each binding against itself: the Python wheel imported, the C demo compiled, the npm package type-checked. None of that would have caught a field that crosses one boundary wrong, which is the failure this library is most exposed to, because a wrong answer about a fast day looks exactly like a right one. So: twelve days, chosen where the bindings could plausibly differ, and all twenty-two fields compared against what the command line says. The days are Pascha itself, the Nativity on both calendars, a Wednesday of the Great Fast under Byzantine Catholic law and again at the Typikon, a weekday of the Nativity Fast where Greek and Slavic usage part, the one Orthodox church on the Gregorian Paschalion, and a day with no feast. A release now runs what a push runs. It had been gated on cargo test alone, so the C ABI, the Lua binding, the Python parity check and the README examples were checked on the branch that led to a release and not on the release. Neither registry will reuse a version, so the gate belongs before the upload. Writing it found three things. The npm exports map did not expose the wasm by package path, so a bundler asked for typikon/web/typikon_bg.wasm was told no such path exists and had to reach into node_modules; the documented init() call was never affected. The suite could hang instead of fail, because the test web server was backgrounded inside a subshell and killed by job spec, leaving bash in do_wait forever. And pointing XDG_CONFIG_HOME at a path that does not exist isolates typikon and breaks every other program that keeps state under it, a browser included. A surface whose toolchain is missing is named as skipped, and fewer than three surfaces running is a failure rather than a pass, so a container with nothing installed cannot come out green.
146 lines
7.1 KiB
YAML
146 lines
7.1 KiB
YAML
name: release
|
|
|
|
# Tag-driven. Cargo.toml carries the version; the tag only points at it, and the
|
|
# 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.
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
|
|
# 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.
|
|
jobs:
|
|
publish:
|
|
runs-on: docker
|
|
container:
|
|
image: rust:1.98-bookworm
|
|
steps:
|
|
- 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
|
|
|
|
- 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; }
|
|
|
|
# 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.
|
|
- 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
|
|
|
|
- name: Build wheels and sdist
|
|
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
|
|
# --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
|
|
done
|
|
maturin sdist --out dist
|
|
ls -l dist
|
|
|
|
- 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
|
|
|
|
- name: Publish
|
|
env:
|
|
# A project-scoped token, not the 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, what is lost is the
|
|
# ability to publish typikon rather than every package the account owns.
|
|
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"
|