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