Skip to content

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 build produces a working binary; make dev enables hot reload via air

Running tests

go test -race ./...
# or, equivalent
make test

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 fmt before 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:

  1. Branch from main. Never commit to main directly; branch protection enforces this.
  2. Use a conventional-commit prefix (feat:, fix:, docs:, chore:, refactor:, perf:, ci:, test:, etc.) on the squash commit.
  3. Install the git hooks once with make hooks, then just push. The pre-push hook runs scripts/pre-push-gate.sh for 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 with make 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.

  1. Open one PR per logical change; never stack PRs.
  2. Apply at least one of the labels listed below so the release-notes generator (.github/release.yml) buckets your change correctly.
  3. 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.