Contributing
Thank you for contributing to Pennant. This page covers how to run the project locally, execute the test suite, and understand the conformance fixture system.
Development setup
Prerequisites
- Go 1.22+ with CGO enabled (required for SQLite)
- Bun 1.x (for the frontend)
golangci-lint(for linting)- Docker (optional, for integration tests with PostgreSQL)
Clone and run
git clone https://github.com/sanskarpan/pennant
cd pennant
# Start the server with in-memory storage (no database required)
make run
# Or with SQLite persistence
SQLITE_PATH=./dev.db make run
The server starts on http://localhost:8080. Login with admin@pennant.local / admin.
Frontend development
The frontend (React + Vite) runs on its own dev server with API proxying:
Changes to the frontend are hot-reloaded. The backend must be running separately (make run in another terminal).
Running tests
All tests
Runs go test ./... with standard flags. Uses in-memory storage.
Race detector
Runs go test ./... -race -count=3. Essential for SSE hub and analytics pipeline code. Always run this before opening a PR that touches concurrency.
Conformance suite
Runs the evaluation engine against all JSON fixtures in conformance/:
1. Executes go test -run TestConformance ./... against the Go engine.
2. Executes the TypeScript SDK test runner against the same fixtures.
3. Both must pass for the suite to succeed.
Integration tests
Requires Docker. Spins up a real PostgreSQL instance and runs the full server against it. These tests cover flag CRUD, SSE streaming, and analytics ingestion end-to-end.
Load testing
# Start 50 concurrent SSE clients
make loadgen CLIENTS=50
# Or run the binary directly
PENNANT_URL=http://localhost:8080 \
PENNANT_SDK_KEY=sdk-server-default-prod \
./loadgen -clients 50 -flags 100 -updates-per-sec 10
The load generator is in cmd/loadgen/. It creates SDK clients, subscribes to the SSE stream, and simulates flag updates from a concurrent writer. Output includes client reconnect counts and p50/p95/p99 snapshot latencies.
Conformance fixture format
Each fixture in conformance/ is a subdirectory with two files:
input.json:
{
"snapshot": {
"version": 1,
"flags": {
"my-flag": {
"key": "my-flag",
"type": "boolean",
"enabled": true,
"defaultVariation": "off",
"variations": [
{"key": "on", "value": true},
{"key": "off", "value": false}
],
"rules": [
{
"id": "rule-1",
"clauses": [
{"attribute": "plan", "op": "in", "values": ["beta"]}
],
"variation": "on"
}
],
"rollout": [],
"targets": [],
"prerequisites": []
}
},
"segments": {}
},
"context": {
"key": "user-123",
"attributes": {
"plan": "beta"
}
},
"flagKey": "my-flag"
}
expected.json:
Adding a new fixture
- Create a new subdirectory under
conformance/, e.g.conformance/rule-semver-gt/. - Write
input.jsonwith a snapshot, context, and flag key that exercises the behavior you want to test. - Run
go test -run TestConformance ./...— it will print the actual result. If it matches your expectation, createexpected.jsonwith that result. - Run
make conformanceto verify both Go and TypeScript produce the expected output.
Fixture naming conventions
| Prefix | Covers |
|---|---|
rule- |
Rule matching with specific operators |
rollout- |
Percentage rollout bucketing |
segment- |
Segment membership |
prereq- |
Prerequisite chains |
target- |
Individual target overrides |
disabled- |
Off flag behavior |
bigint- |
Cases where float64 bucketing would give wrong results |
Code structure
cmd/
server/ Entry point, HTTP server setup
loadgen/ Load testing binary
internal/
api/ HTTP handlers, router
auth/ JWT, RBAC, user management
analytics/ Event ingestion pipeline
eval/ Flag evaluation engine (bucket.go, clause.go, evaluate.go)
events/ Internal pub/sub bus
experiment/ Experiment store and results engine
metrics/ Prometheus instrumentation
model/ Core data types
ratelimit/ Token bucket middleware
sdkauth/ SDK key registry
snapshot/ Snapshot builder and delta computation
stats/ Statistical functions (z-test, mSPRT, Welford, SRM)
store/ Storage backends (memory, sqlite, postgres)
stream/ SSE hub and EventRing
sdk/
go/pennant/ Go SDK
ts/src/ TypeScript SDK
conformance/ Evaluation conformance fixtures
frontend/ React dashboard (Vite + Bun)
Pull request guidelines
- Run
make test-raceandmake conformancelocally before pushing. - Add a conformance fixture for any new evaluation behavior.
- Update the relevant docs page in
docs/if your change affects documented behavior. - Keep PRs focused — one logical change per PR.
- Include a description of what changed and why.
Linting
Uses golangci-lint with the config in .golangci.yml. The CI pipeline runs lint on every push.