Files
typikon/.forgejo/workflows/ci.yml
T
funkpower d90d4c171f a C ABI, so this can be embedded rather than spawned
Every desktop shell that reads typikon today runs the binary and parses
its JSON. That costs about 1.5ms and is the right answer for a calendar
that changes once a day, but it is the wrong shape for anything with an
FFI and no business spawning a process on every redraw.

Dates cross as Julian Day Numbers, so no struct layout is part of the
contract. A day is opaque and owns its strings, built once when it is
reckoned: an accessor returns the same borrowed pointer every time, so a
statusline polling one in a loop allocates nothing. The accessors carry
every field of the JSON, so nothing on the other side needs a parser.

The Python bindings and this were resolving a jurisdiction by two copies
of the same logic, and the copy here leaked one per call. Both now share
jurisdictions::resolve_full, which interns the combinations instead.

The header is a promise made in a second file, and a second file drifts:
a test reads both and fails if either has a symbol the other does not,
and does the same for the LuaJIT cdef, where a stale declaration is a
segfault rather than a compile error.
2026-09-18 14:46:25 -04:00

104 lines
4.6 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
- 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
- name: README examples
run: |
cargo build --release
python3 tests/readme-examples.py