Contributing to Stillwater¶
Thanks for your interest in contributing to Stillwater. This document explains how to set up a development environment, the expectations around code style and pull requests, and where to ask questions.
Code of conduct¶
This project follows the Contributor Covenant. By participating you agree to abide by its terms.
Development environment¶
See Dev setup for the full setup. Quick summary:
- Go 1.26 or newer
- Tailwind CSS standalone CLI (no Node.js required; see Dev setup for download instructions)
make buildproduces a working binary;make devenables hot reload viaair
Running tests¶
Integration tests use real SQLite via modernc.org/sqlite. The race detector
is required when changing concurrent code (goroutines, shared state,
background workers).
Code style¶
- No emoji in code, commits, comments, or documentation
- No em-dashes in any output
- Run
make fmtbefore committing (Go and templ formatters) - Run
make lint(golangci-lint) before opening a PR - Follow the patterns documented in .golangci.yml and the
per-package guidance in
.github/instructions/
Style and conventions live in CLAUDE.md, which doubles as project-wide guidance for both human contributors and AI tools.
Pull request workflow¶
The full workflow is documented in PR workflow. Short version:
- Branch from
main. Never commit tomaindirectly; branch protection enforces this. - Use a conventional-commit prefix (
feat:,fix:,docs:,chore:,refactor:,perf:,ci:,test:, etc.) on the squash commit. - Install the git hooks once with
make hooks, then just push. The pre-push hook runsscripts/pre-push-gate.shfor you, so there is no separate pre-push step to remember and no reason to invoke the gate by hand (a manual standalone run only duplicates the hook's work). Verify the wiring any time withmake doctor, which checks the hooks without changing anything.
Every check in the gate's default path is blocking: if it exits 0 and
prints "All hard checks passed", every check that ran, passed (#2983). A
check that should not block does not belong in the default path, and
scripts/check-gate-invariant.sh enforces that from inside the gate and
from CI's Gate Invariant job.
The local test step is a fast, changed-packages-only, non-race run over the
exact packages whose files changed -- a quick "did I obviously break a
test" signal, not a full CI-equivalent pass. It blocks on a failing
assertion and on a compile error alike; both exit 1. The two get different
messages, but the wording is a best-effort hint only -- it is picked
from whether a coverage profile was produced, and that signal is not
reliable (on Go 1.26 a deliberate syntax error produced [build failed]
and a non-empty profile). Read the test output for the real cause.
Force the full, CI-equivalent local run with
RUN_RACE=1 git push, or skip the local test run and patch-coverage check
together with RUN_RACE=0 -- worth reaching for when the changed package's
own suite is expensive. CI's required Test job runs the full -race
suite and its Coverage Floor job runs the per-package ratchet; both are
authoritative. The opt-in/opt-out accepts any of 1, true, yes, on,
0, false, no, or off (case-insensitive, surrounding whitespace
ignored). Anything else is refused: a typo such as RUN_RACE=truee exits 2
naming the variable and the value, rather than being read as "unset" and
silently skipping a tier you asked to run.
The accessibility (axe-core) smoke tests, the provider-failure smoke test,
and govulncheck are skipped by default and each has a blocking
opt-in: RUN_A11Y=1, RUN_PROVIDER_SMOKE=1, RUN_VULN=1. Each boots a
server, drives a browser, or downloads the vulnerability database, and each
duplicates a required CI check -- "A11y Smoke Tests (Playwright +
axe-core)", "Provider Failure Smoke", and "Go Vulnerability Check"
respectively (all configured as required status checks in the Protect
main ruleset, which lives in repo settings rather than in-repo). Run the
a11y tier locally when your change touches templates, CSS, or
tests/a11y/: RUN_A11Y=1 git push downloads the Playwright browsers and
boots an ephemeral server, so it adds minutes, but CI catching the same
violation costs a red check and a re-push.
Bruno route parity is CI-only; the required "Bruno Route Parity" job runs
scripts/check-bruno-parity.sh on every PR.
- Open one PR per logical change; never stack PRs.
- Apply at least one of the labels listed below so the release-notes
generator (
.github/release.yml) buckets your change correctly. - Address review feedback, then squash-merge. Delete the branch after merge.
Labels¶
The release notes generator buckets changes by label. Apply one or more when opening a PR or filing an issue:
| Bucket | Labels |
|---|---|
| Features | enhancement |
| Bug fixes | bug |
| Performance | performance |
| Security | security |
| Documentation | documentation |
| CI / Build | ci |
| Dependencies | dependencies |
| Refactoring | technical-debt, chore |
Triage-only labels (duplicate, invalid, wontfix, question) are
excluded from release notes.
Suggesting a feature¶
Open an issue using the appropriate issue template. For larger ideas, draft a short scope sketch in the issue body so we can talk through the design before any code lands.
Questions¶
Tag your issue with the question label, or comment on an existing issue
or pull request.