Code contributions that improve the quality and reliability of BenchBox are always welcomed.
This document provides guidelines and instructions for contributing.
- Python 3.11+
- Git
- uv (fast Python package manager)
- GitHub CLI (
gh) — required for the one-shot PR flow below
-
Clone the repository:
git clone https://github.com/BenchBox-dev/BenchBox.git cd BenchBox -
Install the package in development mode:
make develop
make developis equivalent touv sync --group dev. -
Install the pre-commit + pre-push hooks:
pre-commit install
This installs two hooks (configured in
.pre-commit-config.yaml):- pre-commit: ruff format/check, codespell, YAML / markdown lint, timing-policy check.
- pre-push:
pr-preflight— runsmake pr-preflight(ruff on the changed Python files and the tests mapped from them) before each push. Existing clones that installed hooks before the pre-push stage was added should re-runpre-commit installonce.
If
pre-commit installerrors withCowardly refusing to install hooks with core.hooksPath setand points at the default.git/hookslocation, rungit config --unset-all core.hooksPathand try again — that's a redundant override left over from earlier tooling.
develop is the long-lived development branch and the repository's default branch; all changes land via PR. release is release-only (handled by the version-branch flow — see docs/internal/release-guide.md). PRs target develop and squash-merge with linear history.
Required CI on develop reports six unit results (core, explorer, results-data, docs, landing, tooling) from .github/workflows/ci.yml, plus oracle-review, which passes on a result-affecting PR only when the benchbox-oracle app's latest review of its current head reports success (or a listed attester has posted the stand-in approval for that head) and the oracle's threads are resolved. .github/ci-units.yml and .github/path-filters.yml classify each PR: content-only PRs run content validation and skip Python fast tests, while code, infra, workflow, tooling, and unknown paths run the lint/type + Ubuntu 3.12 fast-test checks. Soundness-path review applies even when the author uses the owner's account; once a finished branch is armed, auto-merge lands it when required checks are green and all review threads are resolved.
The canonical loop is branch → edit → preflight → make pr-open → arm → monitor to merge. Once the branch is finished, arm it with make pr-arm, which checks the live PR for a hold and arms auto-merge on the exact head; GitHub merges it when its required checks are green. After the merge, trunk.yml tests develop; if it fails because of your change, revert first with make trunk-revert PR=<n>. You are done when the PR is merged, not when it is open or green: re-arm (make pr-arm again) after a spurious failure and fix and push after a real failure. Stop and hand back only for the exceptions listed in AGENTS.md [WRITE-CLOSEOUT-001].
-
Create a feature worktree off
develop. Agents must keep the main clone read-only:WORKTREE_PATH=../BenchBox.wt-feat-your-thing make worktree-create BRANCH=feat/your-thing WORKTREE_PATH="$WORKTREE_PATH" cd "$WORKTREE_PATH" make agent-write-preflight uv sync --group dev
-
Make your changes. Iterate with the fast lane:
make test make format make lint make typecheckmake testruns the fast lane (about a minute);make formatrunsruff format .,make lintrunsruff check ., andmake typecheckrunsty check.The pre-commit hook re-runs format/lint/etc. at commit time, so if you forget, the commit will fix or block as appropriate.
-
Commit using Conventional Commits (
feat:,fix:,docs:,test:,chore:,ci:,refactor:):git add path/to/file.py path/to/test.py git commit -m "fix: resolve race in foo loader"Stage explicit paths; never use
git add -A. -
Run the local preflight, then open the PR:
make pr-preflight make pr-open
make pr-preflightruns the local lint and the path-aware content guard or fast tests.make pr-openpushes and runsgh pr create --base develop; it refuses to run fromdeveloporrelease. -
Arm it and monitor until it merges:
make pr-arm
PR=<n>is optional.make pr-armrefuses on a hold label, requested changes, a draft, or a head that is not pushed.make pr-armreads the live PR first, so ano-auto-mergelabel or a requested change stops it instead of being mistaken for a failing check; remove a hold deliberately to release it. Before editing an armed PR, withdraw it with the revision transaction (make pr-landing-withdraw, seedocs/agent/review-protocol.md), push the correction, rerun CI and obtain the oracle's success review (or the stand-in approval) on the new head before arming: a later--match-head-commitcannot undo a merge of the old head.make pr-ready PR=<n> HEAD=<sha>arms an open PR throughmake pr-arm. WithEVIDENCEorBATCHit runs the readiness evidence transaction used to deliver a prepared batch, where the evidence file must declaredelivery_modeand the complete prepared-batch binding; a single PR does not need it. Theno-auto-mergelabel blocks arming only; to hold an armed PR, rungh pr merge <n> --disable-autoormake pr-landing-withdraw PR=<n> HEAD=<sha>, then add the label. -
After merge, remove the clean linked worktree. The remote branch normally auto-deletes through the repository setting; sweep stale local branches separately:
make worktree-remove WORKTREE_PATH="$WORKTREE_PATH" make branch-prune-merged DRY_RUN=1 make branch-prune-mergedmake worktree-removeremoves the linked worktree.make branch-prune-merged DRY_RUN=1previews branches with complete historical merge evidence (it requiresgh); withoutDRY_RUNit deletes only exact, merge-reachable historical heads.Inspect open PRs at any time with
make pr-status.
There are two layers, and you only need the first:
Required gate (mirrors what CI enforces):
make pr-preflightThis runs ruff check and ruff format --check on the Python files you changed, then runs the tests for them without xdist, so it does not wait on the shared test lock. A changed tests/**/test_*.py runs itself; a changed benchbox/**/<module>.py or scripts/<module>.py runs every tests/**/test_<module>.py. Tests that failed in the previous run go first (--ff), then the rest run. If no test maps to your change it runs no tests and says CI will run the fast lane.
It does not run the full fast lane, type checking, or the other lint guards. CI runs all of them on every PR within about 20 minutes and is the authoritative check. To run the full fast lane before pushing, use make pr-preflight-fast-tests; to run the CI lint guards use make ci-lint.
Optional thoroughness check — only useful when you've changed cross-cutting things (CI workflows, packaging, docs build, integration paths):
make ci-localThis runs the broader CI mirror:
| Step | Target |
|---|---|
| Lint + format + type checking | make ci-lint |
| Fast tests with coverage | make ci-test |
| Integration smoke tests | make test-integration-smoke |
| Documentation build | make ci-docs |
| Package build + install test | make test-package |
Or run any of those individually. Additional one-offs: make security-audit and make spellcheck.
Skip make ci-local for everyday changes — make pr-preflight is the right gate. The required checks gate the merge, so the cost of being wrong is a re-push and make pr-arm for the new head.
This section is the canonical starting point for contributor test commands. The root README covers product use and links here instead of duplicating the test matrix. See tests/README.md for test categories, markers, parallel-run rules, and troubleshooting.
We use pytest for testing. Common testing commands:
make test
make test-unit
make test-integration
make test-fast
make test-cimake test is a fast local run without coverage. make test-unit and make test-integration run only unit or
integration tests, make test-fast runs tests marked fast (under a second), and make test-ci is the CI profile with
coverage and reports.
The default pytest.ini is optimized for quick local feedback. Use the CI profile when you need coverage, HTML reports,
or JUnit XML output.
uv run -- python -m pytest
uv run -- python -m pytest -c pytest-ci.ini
uv run -- python -m pytest -m fastThe first command is the fast local run, the second uses the CI-equivalent instrumentation, and the third runs only fast tests.
The collection hook applies simple defaults: tests in tests/unit automatically receive the unit and fast markers, tests/integration receive integration with a default medium speed, and files in tests/performance are marked performance and slow. Add explicit markers to override those defaults for individual tests.
PySpark 4.x only supports Java 17 and 21, so we provide scripts/with_supported_java.sh to automatically pick a compatible JDK (it prefers macOS’ /usr/libexec/java_home -v 21, then 17, and falls back to whatever JAVA_HOME you already exported). No shims are added for older runtimes.
To run every PySpark unit test:
make test-pysparkThe target shells out to the helper script, which validates the Java version and executes tests/unit/platforms/dataframe/test_pyspark_df.py. You can also wrap any BenchBox command that touches PySpark - benchmarks, smoke runs, etc.-like this:
scripts/with_supported_java.sh benchbox run --platform pyspark-df --benchmark tpch --scale 0.01 --non-interactiveCI jobs install Temurin 21 and invoke the same helpers so PySpark stays active everywhere automatically.
To add a new benchmark, create a new file in the benchbox directory and implement the BaseBenchmark interface. See existing benchmarks for examples.
BenchBox uses a family-based architecture for DataFrame platforms. To add a new platform:
- Determine the family: Expression (Polars, PySpark) or Pandas (Pandas and cuDF)
- Implement DataFrameContext: Provides table access and family-specific helpers
- Implement platform adapter: Handles data loading and query execution
- Register the platform: Add to
DATAFRAME_ADAPTERSregistry - Add tests: Unit tests and integration tests
See Adding a DataFrame Platform for detailed instructions.
When implementing new DataFrame queries:
- Each query needs both
expression_implandpandas_implfunctions - Use the
DataFrameContextprotocol for table access - Use
ctx.colandctx.litfor expression family implementations - Use string-based column access for pandas family implementations
- Test both families independently
We use ruff for fast Python linting and formatting with a 120-character line length. Type hints are required and enforced with ty. Our pre-commit hooks will help enforce these standards.
- Line length: 120 characters
- Formatter: ruff (replaces black and isort)
- Linter: ruff (replaces flake8)
- Type checker: ty
Please update documentation when adding or modifying features. We use Sphinx for generating documentation.
benchbox.dev, including the Results Explorer, is built and deployed from
BenchBox-dev/benchbox-site,
not from this repository.
Data and pipeline changes reach the site automatically. After each successful
push to develop, .github/workflows/site-inputs.yml builds the certified
site-inputs-<sha> bundle and sends a core-bundle dispatch to the site
repository, whose deploy fetches and verifies that bundle.
A failed bundle build, or a site deploy that cannot use the bundle, opens or
updates a site-bundle-failed issue in this repository.
Changes to the site source kept here do not deploy. They reach production only
when they are replayed into benchbox-site by pull request:
| Path here | Path in benchbox-site |
|---|---|
results-explorer/ |
explorer/ |
docs/blog/ |
blog/ |
_blog/ |
drafts/ |
_project/design/site-inventory/ |
inventory/ |
website/ |
website/ |
landing/ |
landing/ |
If your pull request touches one of these paths, say in its description that
it needs a replay. The steps are in the "Replaying core changes" section of
the AGENTS.md in benchbox-site. make site-build and make site-check
build and test the site locally from this tree; they do not publish anything.
External contributions land via PR against develop (squash-merge). Releases
are cut by maintainers via the version-branch flow documented in
docs/internal/release-guide.md:
make release-cut VERSION=X.Y.Zfrom a clean linked worktree whose HEAD is the freshly fetchedorigin/developcommit. It cutsvX.Y.Zwith a release-curated tree and opens a PR againstrelease. The target refuses the primary clone and any checkout that is dirty, ahead, or behind.- Review the PR and wait for
validate-baseandrelease-required-result. make release-finalize VERSION=X.Y.Zmerges and tags the release;release.ymlpublishes to PyPI.
Use make release-cut-abort VERSION=X.Y.Z to back out an uncommitted cut from
the worktree that created it.
We follow semantic versioning.
By contributing to BenchBox, you agree that your contributions will be licensed under the project's MIT license.