mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-20 11:01:56 +00:00
* docs: keep only server-specific contributor notes * docs: trim README to the code and link to beacon-docs * chore: strip tracking parameters from two airport website URLs
275 lines
10 KiB
Markdown
275 lines
10 KiB
Markdown
# Contributing to Beacon
|
|
|
|
Thank you for your interest in contributing. Beacon is a focused project and we
|
|
want contributions to be high quality and sustainable. Please read this guide
|
|
before opening a PR.
|
|
|
|
---
|
|
|
|
## Workflow
|
|
|
|
Branches, the pull request flow, commit message format and the release process are the same
|
|
for every Beacon repo and live in
|
|
[beacon-docs/CONTRIBUTING.md](https://github.com/MeshCore-Beacon/beacon-docs/blob/main/CONTRIBUTING.md).
|
|
In short: open or comment on an issue first, branch from `dev`, keep each PR to one change,
|
|
write conventional commit messages, and open the PR against `dev`. Running beacon-web against
|
|
a local server is described there too.
|
|
|
|
Before opening a PR here:
|
|
|
|
```
|
|
go build ./... # must compile
|
|
gofmt -l . # must be empty (no unformatted files)
|
|
go vet ./... # no warnings
|
|
go test ./... # all tests pass
|
|
govulncheck ./... # no reachable vulnerabilities
|
|
swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependency
|
|
# if you changed any handler or api type (see below)
|
|
```
|
|
|
|
---
|
|
|
|
## Code style
|
|
|
|
- Follow the existing patterns in each package before introducing new ones
|
|
- Keep functions small and single-purpose
|
|
- Prefer explicit error handling over panic
|
|
|
|
---
|
|
|
|
## Tests
|
|
|
|
- **Add tests for any new pure functions.** Pure functions (no DB, no network,
|
|
no side effects) should have unit tests. See `internal/hub/hub_test.go`,
|
|
`internal/api/nodes_test.go`, and `internal/keystore/keystore_test.go` for
|
|
examples of the style we use.
|
|
- Integration tests (`*_integration_test.go`) need a real PostgreSQL 16 and skip
|
|
unless `BEACON_TEST_POSTGRES_DSN` is set. CI runs a selected set; run the ones
|
|
touching your change locally:
|
|
`BEACON_TEST_POSTGRES_DSN=postgres://... go test ./db/ -run Postgres`
|
|
- If you are fixing a bug, add a test that would have caught it.
|
|
|
|
---
|
|
|
|
## Backup export tests
|
|
|
|
CI runs export, restore round-trip, offline verify and truncated-archive rejection against
|
|
PostgreSQL 16. Locally, with `PG*` pointing at a throwaway server whose role can create
|
|
databases:
|
|
|
|
```sh
|
|
go build -o "$PWD/beacon-backup" ./cmd/beacon-backup
|
|
BEACON_BACKUP_TEST_POSTGRES=1 BEACON_BACKUP_TEST_BINARY="$PWD/beacon-backup" \
|
|
go test ./internal/backup -run '^TestExportPostgres$' -v
|
|
```
|
|
|
|
The test creates, migrates, restores and drops two randomly named databases. The export
|
|
format and operator usage are documented in
|
|
[Backup and export](https://github.com/MeshCore-Beacon/beacon-docs/blob/main/docs/backup-export.md).
|
|
|
|
---
|
|
|
|
## Database changes
|
|
|
|
All schema changes must include a proper migration path:
|
|
|
|
- Add a new migration file to `db/migrations/` following the existing naming
|
|
convention (e.g. `002_add_observation_count.sql`). `001_baseline.sql` is the
|
|
2.0.0 schema; do not modify existing migration files; append only via new files.
|
|
- Update `db/queries/queries.sql` with any new or modified queries
|
|
- Re-run `sqlc generate` to regenerate `db/sqlc/`
|
|
- Update the store layer in `db/` to expose the new functionality
|
|
- Update `internal/api/reader.go` if the change needs to be exposed via the API
|
|
- Historical stats read hourly rollups, never raw observations; follow
|
|
[docs/historical-stats.md](docs/historical-stats.md) to add a metric
|
|
|
|
Never edit files under `db/sqlc/` by hand; they are generated by sqlc and will
|
|
be overwritten. If you need to work around a sqlc limitation, document it
|
|
clearly in the query comment.
|
|
|
|
To regenerate after modifying `db/queries/queries.sql`:
|
|
|
|
```bash
|
|
sqlc generate
|
|
```
|
|
|
|
---
|
|
|
|
## API changes
|
|
|
|
### Router configuration and optional admin endpoints
|
|
|
|
Keep `router.New`'s signature stable. Pass startup settings by name in
|
|
`router.Options`; a new optional setting must not require unrelated callers and
|
|
tests to supply another positional argument.
|
|
|
|
An optional admin feature owns its subrouter, dependencies and HTTP methods.
|
|
Register it once through `Options.AdminRoutes` in the startup wiring, using a
|
|
literal path such as `/accounts`. The parent admin router applies bearer
|
|
authentication to the entire subtree; the feature must not mount itself beside
|
|
that boundary. `/config` remains owned by the shared admin router. No global
|
|
registration or `init` side effects are needed.
|
|
|
|
When concurrent PRs touch the same startup wiring, stack them in an explicit
|
|
merge order, regenerate Swagger rather than hand-merging it, and build the
|
|
combined result even when GitHub says it merges cleanly.
|
|
|
|
Any new or modified REST endpoint must have swagger annotations and regenerated
|
|
docs:
|
|
|
|
- Add or update `// @Summary`, `// @Param`, `// @Success`, `// @Failure`, and
|
|
`// @Router` comments on the handler function
|
|
- Response types are defined in `internal/api/`; add new types there, not
|
|
inline in handlers
|
|
- After changing any handler or API type, rerun the `swag init` command from the
|
|
checklist and commit the updated `docs/` directory
|
|
|
|
Install swag if you don't have it:
|
|
|
|
```bash
|
|
go install github.com/swaggo/swag/cmd/swag@latest
|
|
```
|
|
|
|
Each handler function should have a godoc-style annotation block:
|
|
|
|
```go
|
|
// listThings godoc
|
|
//
|
|
// @Summary Short description shown in the UI
|
|
// @Tags TagName
|
|
// @Produce json
|
|
// @Param paramName query string false "Description"
|
|
// @Param id path string true "Resource ID"
|
|
// @Success 200 {object} api.MyResponseType
|
|
// @Failure 400 {object} handlers.APIError
|
|
// @Failure 500 {object} handlers.APIError
|
|
// @Router /things [get]
|
|
func listThings(reader api.Reader) http.HandlerFunc {
|
|
```
|
|
|
|
For paginated responses use the generic page wrapper:
|
|
|
|
```go
|
|
// @Success 200 {object} api.Page[api.MyType]
|
|
```
|
|
|
|
---
|
|
|
|
## Caching
|
|
|
|
Beacon uses a Redis-backed caching layer in `internal/cache`. If you add a new
|
|
REST endpoint backed by a new `api.Reader` method, consider whether it should be
|
|
cached:
|
|
|
|
- Add the method to `CachedReader` in `internal/cache/reader.go`
|
|
- Pass-through to `cr.inner` if the data is highly dynamic, paginated with many
|
|
filter combinations, or low traffic
|
|
- Use `getOrSet` with an appropriate TTL category (`cr.ttl.Stats`,
|
|
`cr.ttl.Reference`, `cr.ttl.Nodes`, or `cr.ttl.Observers`) for read-heavy,
|
|
slow-changing responses
|
|
- If the data is mutated by the ingest path, add an invalidation call in
|
|
`internal/ingest/side_effects.go` via the `onNodeUpsert` or `onObserverUpsert`
|
|
callbacks, or add a new callback following the same pattern
|
|
|
|
Cache keys live as constants at the top of `reader.go`. Use the `beacon:`
|
|
namespace prefix and include all parameters that affect the response in the key.
|
|
|
|
---
|
|
|
|
## Updating the IATA database
|
|
|
|
Beacon includes a static IATA → country/continent mapping compiled into the
|
|
binary, generated from the [OurAirports](https://ourairports.com/data/) public
|
|
dataset.
|
|
|
|
To refresh it with the latest airport data:
|
|
|
|
```bash
|
|
rm internal/iatadb/gen/airports.csv
|
|
go generate ./internal/iatadb/
|
|
```
|
|
|
|
This fetches a fresh `airports.csv` from OurAirports, saves it locally, and
|
|
regenerates `internal/iatadb/db.go`. Commit both files.
|
|
|
|
To use a local CSV instead (e.g. in a restricted network environment):
|
|
|
|
```bash
|
|
AIRPORTS_CSV=/path/to/airports.csv go run ./internal/iatadb/gen
|
|
```
|
|
|
|
---
|
|
|
|
## Adding a new dependency
|
|
|
|
- Run `go get <module>` and `go mod tidy`
|
|
- Add an entry to [SHOULDERS.md](SHOULDERS.md) with a brief description of what
|
|
the dependency does and why it was added
|
|
|
|
---
|
|
|
|
## Project structure
|
|
|
|
```
|
|
cmd/beacon/ main entry point, wiring, startup
|
|
cmd/beacon-backup/ standalone backup export/verify command
|
|
db/ store layer: sqlc-generated code + thin mapping layer
|
|
migrations/ SQL schema (2.0.0 baseline + numbered files, append only)
|
|
queries/ SQL queries (input to sqlc)
|
|
sqlc/ generated Go code (do not edit by hand)
|
|
internal/
|
|
api/ response types and Reader interface
|
|
handlers/ HTTP handlers (validation, routing, response)
|
|
middleware/ auth, rate limiting, logging
|
|
router/ chi router wiring
|
|
background/ periodic maintenance (retention, rollups, age-out)
|
|
backup/ database + config bundle export and verification
|
|
borders/ foreign-repeater classification against IATA borders
|
|
cache/ Redis caching layer over the Reader
|
|
config/ config loading and scope key derivation
|
|
hub/ WebSocket fan-out broker
|
|
iatadb/ in-memory IATA airport lookup
|
|
ingest/ MQTT packet ingestion and side effects
|
|
keystore/ channel key lookup
|
|
logging/ slog setup
|
|
lora/ LoRa airtime calculation
|
|
meshmapper/ MeshMapper scope and zone imports
|
|
presence/ coalesced last-seen writes
|
|
profiling/ opt-in CPU profiling
|
|
scopestore/ transport scope key lookup
|
|
ws/ WebSocket connection handling
|
|
docs/ generated swagger and design notes (swagger: do not edit by hand)
|
|
```
|
|
|
|
Key patterns to understand before contributing:
|
|
|
|
- **Store layer** (`db/`): thin wrappers around sqlc-generated queries. Each
|
|
method maps between the ingest/api param structs and sqlc param structs. Never
|
|
put business logic here.
|
|
- **Ingest layer** (`internal/ingest/`): processes raw MQTT packets, calls the
|
|
store, and broadcasts hub events. The `DB` interface in `ingest.go` defines
|
|
exactly what the ingest layer needs from the store; keep it minimal.
|
|
- **Hub** (`internal/hub/`): pure fan-out broker. Events are pre-serialised JSON
|
|
before entering the hub so the broadcast loop never touches encoding.
|
|
- **Reader interface** (`internal/api/reader.go`): defines everything the API
|
|
layer can read. The store implements it. All handler tests use a stub reader.
|
|
|
|
---
|
|
|
|
## Releases
|
|
|
|
The flow is in
|
|
[Releases and versioning](https://github.com/MeshCore-Beacon/beacon-docs/blob/main/docs/releases.md#cutting-a-release).
|
|
The server-specific step is bumping `@version` in `cmd/beacon/main.go` and regenerating Swagger:
|
|
|
|
```bash
|
|
swag init -g cmd/beacon/main.go -o docs --parseInternal --parseDependency
|
|
```
|
|
|
|
---
|
|
|
|
## Recognition
|
|
|
|
If you'd like to be listed as a contributor, add yourself to
|
|
[CONTRIBUTORS.md](CONTRIBUTORS.md) in your PR.
|