Files
typikon/.forgejo/workflows/ci.yml
T
funkpower 33441113ce ask every binding the same question and insist on the same answer
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.
2026-09-18 16:08:41 -04:00

137 lines
6.5 KiB
YAML

name: ci
# Deliberately not triggered by pull_request.
#
# The forge this runs on is public and takes pull requests from strangers, and a
# runner executes whatever the workflow checks out. Triggering on pull_request
# would mean a stranger's first contribution runs code on the runner host before
# anyone has read it. Triggering only on push means the runner only ever executes
# code that someone with push rights put there.
#
# The cost is that a pull request gets no automatic check. Run it by pushing the
# branch yourself once you have read the diff.
on:
push:
# Branches only. A tag push does not match a branch filter, so release.yml
# owns tags and this owns everything else.
branches: ['**']
jobs:
test:
runs-on: docker
container:
image: rust:1.98-bookworm
steps:
# Not actions/checkout. That is a JavaScript action: the runner executes it
# with `node` *inside this container*, and a Rust image carries no node, so
# the job dies at the first step with "node: executable file not found in
# $PATH". actions/cache and actions/upload-artifact are the same. Using an
# image that also carries node would mean installing Rust on every run;
# cloning by hand costs four lines and keeps the lean image.
- 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
# No `cargo fmt --check`. The tables in this crate are aligned by hand —
# MONTHS, JURISDICTIONS — and rustfmt wants one entry per line, which loses
# the shape that makes them readable. A gate that fights the house style is
# a gate that gets ignored.
- name: Lint
run: |
rustup component add clippy
cargo clippy --all-targets -- -D warnings
# The binding code only exists under the feature, so a bare clippy
# run never looks at src/python.rs.
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
- name: Rust tests
run: |
cargo test
cargo test --features capi
# The header and the LuaJIT cdef are checked against the library by the
# Rust tests; this checks that a C compiler agrees, which they cannot.
- name: C ABI
run: |
apt-get update && apt-get install -y --no-install-recommends gcc luajit
cargo build --release --features capi
cc -Wall -Wextra -Werror -Iinclude examples/c/demo.c \
-Ltarget/release -ltypikon -o /tmp/demo
LD_LIBRARY_PATH=target/release /tmp/demo rocor
LD_LIBRARY_PATH=target/release /tmp/demo --list
TYPIKON_LIB=target/release/libtypikon.so \
luajit examples/lua/statusline.lua rocor
# The CLI and the bindings are the same engine, so they have to agree. This
# is the check that caught the bindings resolving their own default
# jurisdiction while the CLI read the configured one.
- name: Python bindings
run: |
apt-get update && apt-get install -y --no-install-recommends python3 python3-venv python3-dev
python3 -m venv /tmp/venv
/tmp/venv/bin/pip install --quiet maturin pytest mypy
cargo build --release
/tmp/venv/bin/maturin build --release --out dist
/tmp/venv/bin/pip install --quiet --no-index --find-links dist typikon
/tmp/venv/bin/python -m pytest tests/python -q
# The bindings resolve the default jurisdiction themselves, and once
# read only the config file while the CLI read the environment first.
# The two then answered for different churches on the same machine.
cli=$(TYPIKON_JURISDICTION=rocor ./target/release/typikon \
--date 2026-03-18 --format tsv --fields jurisdiction | tail -1)
py=$(TYPIKON_JURISDICTION=rocor /tmp/venv/bin/python -c "import typikon, datetime; print(typikon.describe(datetime.date(2026, 3, 18)).jurisdiction)")
test "$cli" = "$py" || { echo "cli=$cli python=$py"; exit 1; }
- name: Type stubs
run: |
cat > /tmp/typecheck.py <<'EOF'
import datetime, typikon
d: typikon.Day = typikon.describe(datetime.date(2026, 4, 12))
day: datetime.date = d.date
feast: str | None = d.feast
n: int = d.days_to_pascha
EOF
/tmp/venv/bin/python -m mypy --strict /tmp/typecheck.py
# The npm package is the same engine again, so it has to agree with the
# command line again. The test compares a year of days field by field
# for four jurisdictions, and type-checks the published .d.ts the way the
# Python bindings are type-checked.
- name: JavaScript package
run: |
apt-get update && apt-get install -y --no-install-recommends nodejs npm
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
- name: README examples
run: |
cargo build --release
python3 tests/readme-examples.py
# Every binding is the same engine reached by a different door, so the
# answers have to be identical, and the only way to know is to ask them
# all the same question. Twelve days chosen where they could plausibly
# differ, twenty-two fields each, against what the command line says.
#
# Chromium is installed for this: the web build is the one artifact whose
# real home is somewhere Node cannot stand in for, and loading it with
# Node's bytes proves the glue, not the fetch.
- name: Every binding agrees
run: |
apt-get install -y --no-install-recommends chromium
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