Internal merge-train bot for a single monorepo.
Chute is an internal merge-train controller built around GitHub pull requests and labels.
The intended model is:
- GitHub is the control plane
- labels drive behavior
- webhooks mark pull requests dirty
- a reconciler recomputes desired state from GitHub truth
- SQLite stores local queue state and action history
The long-term train design is a stacked PR queue:
- first queued PR targets
main - second queued PR targets the first PR branch
- third queued PR targets the second PR branch
- and so on
The core bet is that downstream PRs may stay green when GitHub retargets them after the head merges, as long as the effective patch is unchanged.
Implemented today:
uv-managed Python project- FastAPI app
- Swagger UI at
/ - SQLite schema and repositories
- GitHub webhook receiver with signature verification
- startup bootstrap stub
- reconciler loop
- pure planner for admission and queue/head state
- read-only state endpoints
- planner unit tests
Not implemented yet:
- real GitHub App authentication
- real GitHub read model
- PR base retargeting
- merge execution
- notification delivery
- branch/restack repair logic
Treat the current codebase as a working controller skeleton, not a production-ready merge bot.
Create the environment and install dependencies:
uv syncRun the service in normal mode:
uv run chuteRun the service with hot reload:
uv run chute-devIf you add or change script entries in pyproject.toml, run uv sync again before invoking them.
Right now Chute runs as a single process with:
- FastAPI for HTTP
- one in-process reconciler task
- one SQLite database on local disk
That is intentional. It keeps local development, Fly.io, and k8s all viable while the controller semantics are still being built.
Chute currently recognizes these labels:
AutomergeMeans the PR is armed, but should not enter the queue until it is eligible.Automerge-queue-nowMeans the PR should enter the queue immediately, even if checks are still pending.
If both labels are present, Automerge-queue-now wins.
The current planner behavior is:
Automergestaysarmeduntil checks, reviews, and mergeability are acceptableAutomerge-queue-nowenters the queue immediately- existing queue order is preserved
- new queue candidates are appended
- the front PR becomes
readyif eligible - the front PR becomes
waiting_checksif still pending or blocked - a failed head is marked
failedand ejected from the active queue
This is local planning behavior only. GitHub writes are not wired yet.
Configuration is driven by environment variables with the CHUTE_ prefix.
Key settings:
CHUTE_DATABASE_PATHCHUTE_GITHUB_OWNERCHUTE_GITHUB_REPOCHUTE_GITHUB_WEBHOOK_SECRETCHUTE_GITHUB_APP_IDCHUTE_GITHUB_INSTALLATION_IDCHUTE_GITHUB_PRIVATE_KEY_PATHCHUTE_RECONCILE_INTERVAL_SECONDS
Example local .env:
CHUTE_ENV=development
CHUTE_HOST=0.0.0.0
CHUTE_PORT=8000
CHUTE_LOG_LEVEL=INFO
CHUTE_DATABASE_PATH=.data/chute.sqlite3
CHUTE_RECONCILE_INTERVAL_SECONDS=10
CHUTE_GITHUB_OWNER=your-org
CHUTE_GITHUB_REPO=your-monorepo
CHUTE_GITHUB_WEBHOOK_SECRET=replace-me
CHUTE_GITHUB_APP_ID=123456
CHUTE_GITHUB_INSTALLATION_ID=78901234
CHUTE_GITHUB_PRIVATE_KEY_PATH=.secrets/github-app-private-key.pemYou can start configuring the GitHub side now even though the full integration is not finished.
Create a GitHub App scoped to the single repository.
Recommended baseline permissions:
- Repository permissions:
Pull requests: Read and writeContents: Read and writeMetadata: Read-onlyChecks: Read-onlyCommit statuses: Read-only
Event subscriptions:
Pull requestPull request reviewCheck runCheck suiteStatus
You may later need additional permissions depending on how merge execution is implemented, but this is the right starting shape.
Install the GitHub App into the target organization or user account, restricted to the single monorepo Chute will manage.
Record:
- App ID
- Installation ID
- webhook secret
- private key file path
These map directly to the CHUTE_GITHUB_* environment variables above.
Point the GitHub App webhook to:
https://your-chute-host/webhooks/github
If you are running locally, use a tunnel such as ngrok, cloudflared, or an equivalent internal ingress path.
Create these labels in the repository:
AutomergeAutomerge-queue-now
Use the exact spelling above for now. The code currently treats these label names as fixed constants.
- Create
.env - Run
uv sync --extra dev - Start the service with
uv run chute-dev - Open
http://localhost:8000/ - Confirm the Swagger UI loads
- Send a health check to
GET /healthz
The current HTTP surface is:
GET /Swagger UIGET /openapi.jsonOpenAPI specGET /healthzGET /readyzGET /trainactive queue summaryGET /prstracked pull requestsGET /prs/{number}one tracked pull requestGET /eventsrecently received webhook eventsGET /actionsrecent controller actionsPOST /webhooks/githubGitHub webhook receiver
SQLite is currently the only state store.
Tables:
pull_requestsqueue_entrieseventsactionsnotifications
GitHub is still intended to be the source of truth for live PR state. SQLite is there for:
- restart safety
- queue state
- action history
- debugging
Run the current unit tests:
uv run pytestThe next milestones in code are:
- Real GitHub App authentication and API client
- Startup scrape of open relevant PRs from GitHub
- Webhook-driven PR refresh against GitHub truth
- Richer planner outputs for desired base chaining
- GitHub writes for retargeting and merge execution
uv run chuteis the non-reloading modeuv run chute-devis the reloading development mode- if hot reload does not reflect recent code changes, rerun
uv syncfirst and then startuv run chute-dev