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 - name: Rust tests run: cargo test # 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