Contributing

August 13, 2026 · View on GitHub

Thanks for your interest in tsoracle. This guide covers the local setup, the checks CI will run on your PR, and the conventions we follow.

Code of Conduct

This project adopts the Contributor Covenant v2.1. By participating in the project — issues, PRs, discussions, or any other channel tied to the repo — you agree to uphold it. See CODE_OF_CONDUCT.md for the full text and the private reporting channel.

Licensing of contributions

tsoracle is licensed under Apache-2.0, and so is every crate published from this repository (license = "Apache-2.0" in each Cargo.toml). Contributions you submit follow the standard inbound = outbound convention: the changes you contribute are offered under the same Apache-2.0 terms as the project itself, per section 5 of the license and GitHub's Terms of Service §D.6. Every new first-party .rs file carries the canonical short license header (see License headers on Rust source below).

Developer Certificate of Origin

The project requires every commit to carry a Signed-off-by trailer attesting to the Developer Certificate of Origin v1.1. This is layered on top of the Apache-2.0 inbound = outbound licensing above: the licensing mechanism is unchanged, the sign-off is the explicit per-commit legal-authorization assertion.

The DCO text — abbreviated, see https://developercertificate.org/ for the canonical version — is:

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project and the open source license(s) involved.

Signing off on commits

Sign off each commit by passing -s to git commit:

git commit -s -m "feat: add feature X"

This appends a trailer:

Signed-off-by: Your Name <you@example.com>

The name and email come from your git config user.name and user.email; use a real name (DCO does not accept pseudonyms or @users.noreply.github.com for the email).

Enforcement

The DCO GitHub App is installed on this repository and runs as a required PR check. A PR with any unsigned commit fails the check and cannot be merged. To fix an unsigned commit history, use git rebase --signoff main (or git commit --amend --signoff for the most recent commit) and force-push to your PR branch.

Setup

This is a Rust project. The toolchain channel is pinned in rust-toolchain.toml; running any cargo command will install the matching version for you.

You'll also need:

  • protoc — the Protocol Buffers compiler. tonic-prost-build invokes it during cargo build. On macOS: brew install protobuf. On Debian/Ubuntu: sudo apt-get install -y protobuf-compiler.
  • LLVM/Clang (libclang)librocksdb-sys invokes bindgen during cargo build, which needs libclang to generate FFI bindings. The default features of tsoracle-openraft-toolkit (rocksdb-log-store) and tsoracle-driver-openraft (rocksdb-snapshot-store) enable rocksdb, so any workspace build (cargo build --workspace [--all-features]) triggers it. On macOS, the Xcode Command Line Tools (xcode-select --install) include it; if bindgen still can't find it, brew install llvm and export LIBCLANG_PATH="$(brew --prefix llvm)/lib" (optionally LLVM_CONFIG_PATH="$(brew --prefix llvm)/bin/llvm-config"). On Debian/Ubuntu: sudo apt-get install -y clang libclang-dev (same as CI).
  • buf (optional, only needed if you touch .proto files) — CI runs buf lint, buf format, and buf breaking against crates/tsoracle-proto/proto.
  • cargo-deny (optional) — CI runs cargo deny check against deny.toml to enforce the license allow-list and advisory policy.
  • cargo-llvm-cov (optional, only needed to reproduce coverage locally) — CI runs cargo llvm-cov and uploads the resulting lcov.info to Coveralls. Install with cargo install cargo-llvm-cov. Coverage is reported only — the build does not fail on a coverage drop.

Where to start

If you're new to the project, a few entry points:

  • Issues labeled good first issue are small, well-scoped, and don't require deep context on the consensus internals.
  • Issues labeled help wanted are larger but maintainer-flagged as places where outside help is welcome.

Three areas that frequently have well-bounded contribution opportunities:

  1. Documentation polish — fixing broken links in docs/, clarifying CLI examples, expanding getting-started.md for additional platforms.
  2. Additional fuzz targets — the harnesses under fuzz/fuzz_targets/ focus on decoder paths; new targets exercising codec roundtrips or protocol invariants are welcome.
  3. Additional integration tests — tests under crates/*/tests/ are composable; new scenarios exercising specific fault patterns (network jitter, slow disks, partial partitions) add coverage without requiring driver-internal changes.

If you have an idea outside these areas, open a discussion issue first to confirm direction before investing time.

Workspace layout

The repo is a Cargo workspace. The crates under crates/ are:

CratePurpose
tsoracle-protogRPC service & message definitions
tsoracle-corewindow allocator, epoch, monotonicity invariants
tsoracle-openraft-toolkitreusable openraft glue: TypeConfig macro, RocksDB log store, helpers
tsoracle-consensusthe ConsensusDriver trait and shared types
tsoracle-driver-filesingle-node, fsync-backed driver
tsoracle-driver-openraftopenraft-backed ConsensusDriver for multi-node deployments
tsoracle-serverthe tonic service and leader handoff
tsoracle-clientgRPC client with leader discovery and coalescing
tsoracle-binthe tsoracle CLI

Runnable examples live under examples/ (embedded-server, failover-demo, openraft-standalone, openraft-piggyback) and are part of the default workspace members, so cargo check covers them too.

Documentation

tsoracle's prose documentation lives in two trees with different audiences:

  1. Root docs/ — the deep dive. Covering everything from getting started through architecture, allocator internals, consensus integration patterns, operations, and per-example walkthroughs. Browsable on GitHub; indexed by DeepWiki. See docs/README.md for the table of contents.

  2. In-crate docs modules — the docs.rs subset. Three chapters, each pulled into a crate's docs module via #[doc = include_str!(...)] so it renders on docs.rs alongside the API reference:

    ChapterSourceRendered at
    algorithmcrates/tsoracle-core/src/docs/algorithm.mdtsoracle_core::docs::algorithm
    consensus_integrationcrates/tsoracle-consensus/src/docs/consensus_integration.mdtsoracle_consensus::docs::consensus_integration
    operationscrates/tsoracle-server/src/docs/operations.mdtsoracle_server::docs::operations

    The "guide" badge in the README points at tsoracle-server's docs index, which cross-links into the other two crates' docs modules.

Which file do I update?

  • Algorithm, ConsensusDriver contract, or operations content that belongs on docs.rs → update the in-crate file. The matching root docs/ chapter (the-allocator.md, consensus-integration.md, operations.md) should mirror or link to it; keep them consistent in the same PR.
  • Anything else — getting started, sub-topics like the failover fence or monotonicity proof, deployment topologies, example walkthroughs — lives only in root docs/. Edit the chapter directly.
  • Protocol-visible behavior or allocator changes — update the relevant chapter(s) (in-crate and root deep-dive, where both apply) in the same PR.

Preview the in-crate chapters locally with:

cargo doc -p tsoracle-server --no-deps --open

Then navigate to the docs module on the generated page.

Running the checks locally

CI runs the commands below — match them before pushing and your PR will be green on the first try:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo build --workspace --all-features
cargo test  --workspace --all-features

The --all-features flag activates the failpoints Cargo feature on each opting-in crate, so the failpoint suite (see docs/failpoint-testing.md) is part of the normal cargo test run. --all-features also activates the yieldpoints Cargo feature, which gates async yield-point tests in tsoracle-driver-openraft (see docs/yieldpoint-testing.md — the async counterpart of failpoints, for tests that need to park production code in an async path without blocking a tokio worker). To run just the failpoint suite:

make test-failpoints

Pre-commit hook

A tracked pre-commit hook in .husky/pre-commit runs the first two of those checks (cargo fmt --check and clippy) and blocks the commit on failure.

It auto-installs on your first cargo test (any flavor — workspace-wide, or -p tsoracle-core) via husky-rs, a dev-dependency on tsoracle-core that sets core.hooksPath = .husky for this clone. No manual setup is needed as long as you run cargo before your first commit. If you want the hook active before any cargo invocation, run make install-hooks to set core.hooksPath = .husky directly.

Bypass with git commit --no-verify when you know what you're doing — CI runs the same checks regardless, so a bypassed commit will still fail upstream.

If you touched anything under crates/tsoracle-proto/proto/:

buf lint     crates/tsoracle-proto/proto
buf format   --diff --exit-code crates/tsoracle-proto/proto
buf breaking crates/tsoracle-proto/proto \
  --against ".git#branch=main,subdir=crates/tsoracle-proto/proto"

Supply-chain check:

cargo deny check

Coverage (library crates only — tsoracle-bin and examples/ are excluded):

make coverage   # writes lcov.info at the workspace root

Test policy

Every contribution that adds new functionality must include automated tests that exercise it, in the same PR as the change. Every bug fix must include at least one regression test that fails on the unfixed code and passes on the fix. Tests run as part of cargo test --workspace --all-features (a CI required check).

Commits with the docs:, refactor:, or chore: Conventional Commits prefix are exempt because they do not change runtime behavior. A refactor: that changes observable behavior is misclassified — relabel it feat: or fix: and add the tests.

Enforcement is at review time, backed by the workspace's measured line coverage (cargo-llvm-cov → Coveralls, currently ~95%). A PR that drops coverage materially without justification will not be merged.

Panic policy: unwrap and expect

Library and binary crates avoid .unwrap() and .expect(...) in non-test code. Each library/binary crate root carries:

#![cfg_attr(not(test), warn(clippy::unwrap_used, clippy::expect_used))]

This is an inner attribute, scoped to that crate's compilation unit. Two consequences worth understanding before editing it:

  • The lint is off during cargo test --lib. cfg(not(test)) is false when the lib is compiled under --test, so #[cfg(test)] mod tests { ... } blocks inside src/ are exempt by construction. This is more reliable than clippy.toml's allow-unwrap-in-tests setting, which has known bugs for helper functions inside test modules (rust-clippy #9612).
  • Integration tests, examples, and benches are separate compilation units. They don't inherit the lib's inner attributes. They aren't linted, full stop — no per-file #![allow] needed, even for module-scope helpers in tests/foo.rs. This sidesteps rust-clippy #13981, where allow-unwrap-in-tests fails to cover tests/, examples/, and benches/.

tsoracle-proto is excluded — it's tonic-prost-build-generated wire code with #![allow(clippy::all)]. Example and benchmark crates don't carry the attribute either; demonstration code is allowed to be terse.

Because CI runs cargo clippy ... -- -D warnings, an unannotated .unwrap() or .expect() in runtime code is a hard build failure.

When you genuinely need .unwrap() or .expect(...) in runtime code — typically because the invariant is statically guaranteed by a const, by surrounding control flow, or by a build-time artifact — annotate the callsite with #[expect(clippy::expect_used, reason = "...")] (or the unwrap_used variant). The reason field is required by convention:

  1. Explain the invariant. What makes this call unreachable in practice? Name the const, the cfg, or the upstream check that holds.
  2. Link to a tracking issue. If a follow-up to replace the panic with typed-error propagation exists, write Tracked by #N. so the marker stays connected to ongoing work.

Place the #[expect] on the smallest enclosing item: prefer a let-statement attribute, fall back to the enclosing function when the call isn't bound to a let. #[expect] is preferred over #[allow] because it warns if the expected lint stops firing — the marker self-clears when the panic path is removed.

Example:

#[expect(
    clippy::expect_used,
    reason = "`KEY` is a `const &'static str` of valid ASCII; `MetadataKey::from_bytes` cannot fail here. Tracked by #5."
)]
let key = MetadataKey::from_bytes(KEY.as_bytes()).expect("valid key");

License headers on Rust source

Every first-party .rs file carries the canonical short license header at the very top — above any #![...] inner attributes, since those are part of compilation and not a shebang. The header is checked in CI by the header-check job in .github/workflows/ci.yml, which runs scripts/check-ts-header.py across the whole tree. A missing or stale header is a hard build failure.

When you add a new .rs file (or land a refactor that moves files around), run:

python3 scripts/check-ts-header.py --fix           # repair missing/stale headers in the whole repo
python3 scripts/check-ts-header.py path/to/file.rs # check (or repair, with --fix) a specific file or directory

The pre-commit hook does not check headers — it runs cargo fmt and clippy only — so run --fix locally before pushing if you've added or moved .rs files. The canonical header text lives in a sibling file next to the script so non-Python tooling can read the same string; do not paste a hand-edited variant.

Performance-critical-path rules

A handful of files sit on the request-handling hot path. They carry a // #[PerformanceCriticalPath] marker as their first line, and CI's critical-path job (scripts/check-critical-path.sh) enforces a small set of source-level rules against them — no tracing::info!/warn!/error!, no println!, no synchronous I/O, no long synchronous compute. See docs/performance-critical-path.md for the full rule set, the marker-placement contract, and the current marked-file list. If your edit touches a marked file, run CRITICAL_PATH_STRICT=1 ./scripts/check-critical-path.sh locally before pushing.

Working with git

  • Write commit messages like an email to your teammates. This repo follows Conventional Commits prefixes (feat:, fix:, chore:, docs:, refactor:, test:), optionally with a scope (feat(server): ...). See How to Write a Git Commit Message for guidance on the body itself — explain why, not what.
  • Do rebase and squash onto the latest main before opening a PR.
  • Do NOT rebase after publishing a PR. Push fixup commits on top so reviewers can see what changed between rounds; squash happens at merge.

Releases

Releases run on release-plz. Each crate is versioned independently — a change to one crate bumps only that crate (and any dependents whose version = "..." pin release-plz updates), not the whole workspace. The flow is:

  1. Land commits on main using Conventional Commits prefixes (feat:, fix:, chore:, etc.). The prefix determines the semver bump, and release_commits = "^(feat|fix|perf|refactor)" in release-plz.toml means feat:/fix:/perf:/refactor: commits trigger a release for the crate they touch — chore:, docs:, style:, test:, and build: do not.
  2. The release-plz PR workflow opens (or updates) a "Release PR" with the version bump and per-crate CHANGELOG.md diffs.
  3. Reviewing and merging that PR triggers the release-plz release job: it tags each crate (e.g. tsoracle-core-v0.2.0) and runs cargo publish in dependency order. A GitHub Release is created per tag.

Adding public API to a library crate (e.g. tsoracle-proto) must use feat: or fix:, never refactor:. The API surface is user-visible to dependent crates even when the gRPC wire contract (the v1 proto package) is unchanged — a new pub item, re-export, or function is not a behavior-preserving refactor. Use the accurate prefix so release notes and semantic-version intent describe the published API change correctly. Keep the proto package version (tsoracle.v1) and the crate version (tsoracle-proto on crates.io) distinct in your head — the former is the wire contract, the latter is the Rust packaging semver that dependents compile against.

Before the first publish (one-time bootstrap)

release-plz can only manage crates that already exist on crates.io, so the first publish has to happen out-of-band:

  1. Generate a crates.io API token with publish-new + publish-update scopes and add it as the CARGO_REGISTRY_TOKEN secret in the GitHub repo settings.
  2. Run make release-dry-run locally to confirm every crate packages cleanly.
  3. Publish each crate manually in dependency order with cargo publish -p <crate>. The order is in RELEASE_CRATES in the Makefile; crates.io rejects publishes whose path-resolved deps aren't yet on the registry, so order matters. If a publish fails mid-list, fix the issue and resume from that crate.
  4. From the next merge onward, release-plz takes over.

Pre-flight check for manifest changes

Before opening a PR that touches Cargo.toml metadata (license, readme, keywords, the per-dep version = "..." pins, etc.), run:

make release-dry-run

It iterates cargo publish --dry-run -p <crate> over every publishable crate in dependency order. Catches packaging issues (missing readme, license-allow-list violations, broken include lists) before they reach the release PR.