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-buildinvokes it duringcargo build. On macOS:brew install protobuf. On Debian/Ubuntu:sudo apt-get install -y protobuf-compiler.- LLVM/Clang (
libclang) —librocksdb-sysinvokesbindgenduringcargo build, which needslibclangto generate FFI bindings. The default features oftsoracle-openraft-toolkit(rocksdb-log-store) andtsoracle-driver-openraft(rocksdb-snapshot-store) enablerocksdb, so any workspace build (cargo build --workspace [--all-features]) triggers it. On macOS, the Xcode Command Line Tools (xcode-select --install) include it; ifbindgenstill can't find it,brew install llvmandexport LIBCLANG_PATH="$(brew --prefix llvm)/lib"(optionallyLLVM_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.protofiles) — CI runsbuf lint,buf format, andbuf breakingagainstcrates/tsoracle-proto/proto.cargo-deny(optional) — CI runscargo deny checkagainstdeny.tomlto enforce the license allow-list and advisory policy.cargo-llvm-cov(optional, only needed to reproduce coverage locally) — CI runscargo llvm-covand uploads the resultinglcov.infoto Coveralls. Install withcargo 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 issueare small, well-scoped, and don't require deep context on the consensus internals. - Issues labeled
help wantedare larger but maintainer-flagged as places where outside help is welcome.
Three areas that frequently have well-bounded contribution opportunities:
- Documentation polish — fixing broken links in
docs/, clarifying CLI examples, expandinggetting-started.mdfor additional platforms. - Additional fuzz targets — the harnesses under
fuzz/fuzz_targets/focus on decoder paths; new targets exercising codec roundtrips or protocol invariants are welcome. - 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:
| Crate | Purpose |
|---|---|
tsoracle-proto | gRPC service & message definitions |
tsoracle-core | window allocator, epoch, monotonicity invariants |
tsoracle-openraft-toolkit | reusable openraft glue: TypeConfig macro, RocksDB log store, helpers |
tsoracle-consensus | the ConsensusDriver trait and shared types |
tsoracle-driver-file | single-node, fsync-backed driver |
tsoracle-driver-openraft | openraft-backed ConsensusDriver for multi-node deployments |
tsoracle-server | the tonic service and leader handoff |
tsoracle-client | gRPC client with leader discovery and coalescing |
tsoracle-bin | the 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:
-
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. Seedocs/README.mdfor the table of contents. -
In-crate
docsmodules — the docs.rs subset. Three chapters, each pulled into a crate'sdocsmodule via#[doc = include_str!(...)]so it renders on docs.rs alongside the API reference:Chapter Source Rendered at algorithmcrates/tsoracle-core/src/docs/algorithm.mdtsoracle_core::docs::algorithmconsensus_integrationcrates/tsoracle-consensus/src/docs/consensus_integration.mdtsoracle_consensus::docs::consensus_integrationoperationscrates/tsoracle-server/src/docs/operations.mdtsoracle_server::docs::operationsThe "guide" badge in the README points at
tsoracle-server'sdocsindex, which cross-links into the other two crates'docsmodules.
Which file do I update?
- Algorithm,
ConsensusDrivercontract, oroperationscontent that belongs on docs.rs → update the in-crate file. The matching rootdocs/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 insidesrc/are exempt by construction. This is more reliable thanclippy.toml'sallow-unwrap-in-testssetting, 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 intests/foo.rs. This sidesteps rust-clippy #13981, whereallow-unwrap-in-testsfails to covertests/,examples/, andbenches/.
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:
- Explain the invariant. What makes this call unreachable in practice? Name the const, the cfg, or the upstream check that holds.
- 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
mainbefore 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:
- Land commits on
mainusing Conventional Commits prefixes (feat:,fix:,chore:, etc.). The prefix determines the semver bump, andrelease_commits = "^(feat|fix|perf|refactor)"inrelease-plz.tomlmeansfeat:/fix:/perf:/refactor:commits trigger a release for the crate they touch —chore:,docs:,style:,test:, andbuild:do not. - The
release-plz PRworkflow opens (or updates) a "Release PR" with the version bump and per-crateCHANGELOG.mddiffs. - Reviewing and merging that PR triggers the
release-plz releasejob: it tags each crate (e.g.tsoracle-core-v0.2.0) and runscargo publishin dependency order. A GitHub Release is created per tag.
Adding public API to a library crate (e.g.
tsoracle-proto) must usefeat:orfix:, neverrefactor:. The API surface is user-visible to dependent crates even when the gRPC wire contract (thev1proto package) is unchanged — a newpubitem, 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-protoon 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:
- Generate a crates.io API token with
publish-new+publish-updatescopes and add it as theCARGO_REGISTRY_TOKENsecret in the GitHub repo settings. - Run
make release-dry-runlocally to confirm every crate packages cleanly. - Publish each crate manually in dependency order with
cargo publish -p <crate>. The order is inRELEASE_CRATESin 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. - 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.