go-doc/data/docs/go-bananas-development.md

230 lines
8.5 KiB
Markdown
Raw Normal View History

2026-10-07 15:05:48 +02:00
<!-- go-doc:generated:begin source=go-bananas:docs/development.md signature=7d8b696ba971c1cbd2e19854d26de304 generated=2026-10-05 18:14 UTC -->
Imported from docs/development.md in the go-bananas repository.
---
# Development
Working on go-bananas.
## Requirements
| Tool | Version | Why |
| --- | --- | --- |
| Go | 1.27 or later | Static binaries, `net/http` routing primitives |
| Node.js | 22 or later | Vite 8 requires it |
| Docker | 24 or later | BuildKit for the multi-stage image |
| Helm | 3.14 or later | Chart validation and `helm lint --strict` |
Optional, installed by `make tools`: `air` for hot reload, `golangci-lint` for
linting, `sqlc` if you add a query.
## Setup
```bash
git clone https://github.com/urpi/go-bananas
cd go-bananas
make install # Go modules, admin UI dependencies, SDK dependencies
make tools # air, golangci-lint, sqlc
```
## Running
```bash
make dev-all # API on :8080, admin UI on :5173 with a proxy
```
Open <http://localhost:5173>. The Vite dev server proxies `/api`, `/rt` and `/auth`
to the Go process, so the session cookie works exactly as it will in production.
Same-origin matters: a cross-origin setup would not authenticate at all, and the
problem would look like a bug rather than a topology difference.
Other entry points:
```bash
make dev-api # the API alone, with hot reload
make dev-web # the admin UI alone
make run # build everything, run the real binary
```
### Trying the theme preference locally
The theme switcher in the header writes to the account, not just to the browser, so the
selection follows you to another browser and survives a re-login. To see that end to
end without a second browser:
```bash
# 1. Start the stack and sign in. `make dev-all` defaults to AUTH_MODE=dev, so the
# dev-user query parameter is enough to be somebody:
# http://localhost:5173/?dev-user=designer@example.test
# 2. Pick a published theme and a scheme in the header switcher. That is a PUT to
# /api/v1/me/preferences.
# 3. Read back what the server decided, which is the authoritative value:
curl -s localhost:8080/api/v1/me/preferences \
-H 'Cookie: go-bananas-session=...' | jq
# 4. Confirm it arrives with the session, which is how the UI gets it on first paint:
curl -s localhost:8080/api/v1/auth/me \
-H 'Cookie: go-bananas-session=...' | jq .preferences
# 5. Prove it is account state rather than cookie state by clearing local storage,
# or by asking in a second browser profile: the theme you chose is still selected.
```
Two things are deliberately not local state:
- **The theme is a preference, not a session claim.** The token comes from the identity
provider, so this service cannot put anything in it; `/api/v1/me/preferences` is the
contract instead.
- **`localStorage` is still read on the first render.** A server round trip cannot
happen before paint, and waiting for one flashes the wrong theme on every load. The
stored preference is adopted as soon as the session arrives, and a change writes both
places, so the next paint is correct even offline.
Pointing it at a draft theme returns 400, and pointing it at a theme that has since been
archived reads back as no choice. Both are covered in `docs/api.md`.
## Layout
```
cmd/go-bananas/ main(): signals, exit codes, nothing else
internal/
app/ dependency injection and the process lifecycle
auth/ OIDC, sessions, roles
config/ environment parsing and validation
domain/ entities, invariants, sentinel errors
httpapi/ routing, middleware, handlers
logging/ slog configuration
seed/ the built-in themes
store/ SQLite: migrations and repositories
tokenengine/ pure logic: colour, validation, rendering
store/migrations/ embedded SQL, applied at start-up
web/ the administration UI (React, Vite, Tailwind)
sdk/ the runtime SDKs, served from /rt/v1
deploy/helm/go-bananas/ the chart
docs/ this, plus the ADRs
```
See [architecture.md](architecture.md) for the dependency rule and
[standards.md](standards.md) for the conventions. Read both before a substantial
change.
## Tests
```bash
make test # everything
make test-go # Go, with the race detector
make test-web # the admin UI
make test-sdk # the runtime SDK
make test-cover # Go, with a coverage profile
make bench # the token engine benchmarks
```
Every test run in CI is `go test -race`. A data race in a cache or a store is a
production incident waiting for load.
## The pre-merge gate
```bash
make verify # check + helm lint
```
This is exactly what CI runs. A pull request that does not pass it will not merge.
## Things that will surprise you
**The admin UI consumes the runtime.** It loads themes from `/rt/v1` and maps the
variables onto Tailwind's namespaces. Switching the UI to the "Light" or "Dark" mode
in the top bar loads a published theme's real stylesheet. A change to the token engine
that breaks delivery breaks the editor, immediately.
**Publishing bumps the version, and the version is the cache key.** Every mutation
does, not only publication. If you change `store.Update`, check whether the version
needs to move.
**History is append-only.** A rollback restores a snapshot as a *new* version. There
is no code path that removes a version, by design: an application deployed against
version 7 must never have version 7 taken away.
**The built-in themes are reconciled, not overwritten.** `internal/seed` compares the
stored version with the seed version. An edited built-in theme is recognised by its
higher version and left alone.
**Validation gates publication, not editing.** A draft can be incomplete. That is
what makes building a theme up incrementally practical.
## Common changes
### Adding a token group
1. Add a `GroupSpec` to `tokenengine.Groups`.
2. Add the tokens to the built-in themes in `internal/seed`.
3. Run `make test-go`. The seed tests validate every built-in theme and check WCAG
contrast for body text on every surface, so an inaccessible colour fails the build.
### Adding an API endpoint
1. The handler in `internal/httpapi`. No business logic; call the store, encode the
result.
2. The route in `internal/httpapi/router.go`, with the role it requires.
3. The type in `web/src/lib/types.ts` and the client function in `web/src/lib/api.ts`.
4. The documentation in `docs/api.md`.
5. Tests for each branch and each authorisation decision.
### Adding a configuration variable
1. The field and default in `internal/config`.
2. The parsing in `loadFrom`.
3. A validation rule in `Validate` if it can be wrong.
4. The environment variable in the chart's `configmap.yaml` and `values.yaml`, and in
`values.schema.json`.
5. The table in `docs/deployment.md`.
A variable with no validation is a variable that will be set to something wrong.
### Changing the runtime contract
Add, do not change. `/rt/v1` is a contract with applications that have no way to
upgrade in step with you. Adding an optional query parameter or a response field is
safe. Changing the meaning of an existing one is not.
## Conventions
Enforced, not suggested. `make check` is the same command CI runs.
| | |
| --- | --- |
| Go formatting | `gofmt`, `make fmt-check` |
| Go lint | `golangci-lint`, `make lint-go` |
| Go imports | `gci`, configured in `.golangci.yml` |
| TypeScript | `tsc --build`, strict |
| Frontend lint | `eslint --max-warnings 0` |
| Frontend format | `prettier`, including the Tailwind plugin |
| Chart | `helm lint --strict` plus `values.schema.json` |
Read [standards.md](standards.md) before contributing. It is the difference between a
change that fits and one that has to be rewritten.
## Troubleshooting
**Port 8080 in use.** `make dev-api` with `BANANAS_HTTP_ADDR=:8081`, or stop whatever
holds the port.
**The admin UI cannot reach the API.** The Vite proxy targets
`http://localhost:8080`. If the API is elsewhere, edit `server.proxy` in
`vite.config.ts`.
**`npm ci` fails.** A stale `node_modules`. `make clean-cache`.
**`go build` fails after a dependency change.** `make tidy`, then `make install`.
**A test is flaky.** Do not re-run it and hope. Find the ordering dependency. The most
common cause here is module-level state in a frontend test: use `vi.resetModules()`
and a fresh module instance.
**The chart will not render.** Read the error. Every chart failure names the value to
change — that is deliberate, and the alternative is a failure at run time instead.
<!-- go-doc:generated:end -->