162 lines
7.8 KiB
Markdown
162 lines
7.8 KiB
Markdown
# Development
|
|||
|
|
|
||
|
|
How to work on **Volumen**.
|
||
|
|
|
||
|
|
## Prerequisites
|
||
|
|
|
||
|
|
- Go 1.27.1, the newest stable release; the `go` directive in `go.mod`
|
||
|
|
declares exactly that version, and nothing older builds the module.
|
||
|
|
- [just](https://github.com/casey/just) for the recipes.
|
||
|
|
- gcc, but only for the race detector: `just race` needs cgo. The build
|
||
|
|
itself is pure Go with `CGO_ENABLED=0`.
|
||
|
|
- Perl for the scripted recipe lines and `scripts/notices.pl`; the base
|
||
|
|
interpreter with builtins only is enough.
|
||
|
|
|
||
|
|
## Setup
|
||
|
|
|
||
|
|
```sh
|
||
|
|
git clone https://sourcedock.dev/petrbalvin/volumen.git
|
||
|
|
cd volumen
|
||
|
|
just build
|
||
|
|
```
|
||
|
|
|
||
|
|
`just run` starts the server against `./config.toml` and `./posts` on the
|
||
|
|
configured host and port (default `[::]:9091`); `just dev` binds it to
|
||
|
|
`127.0.0.1:9091`. `config.toml` and `users.toml` are git-ignored, because the
|
||
|
|
config holds secrets: on a fresh clone the server runs on the built-in
|
||
|
|
defaults and the overridden content directory. Both recipes pass
|
||
|
|
`-buildvcs=true`, because plain `go run` does not stamp the build, so the
|
||
|
|
reported version would be `(devel)`. Go has no built-in reload; restart the
|
||
|
|
process after code changes. The whole binary rebuilds in seconds, which
|
||
|
|
avoids waiting for a reload cycle.
|
||
|
|
|
||
|
|
## Recipes
|
||
|
|
|
||
|
|
Every recipe in the project's file, and what it does. Taken from the file
|
||
|
|
itself, so the names and the list match it exactly, in the order the file
|
||
|
|
declares them.
|
||
|
|
|
||
|
|
| Recipe | What it does |
|
||
|
|
|---|---|
|
||
|
|
| `just` | the `default` recipe, `just --list`: every recipe with its one-line summary |
|
||
|
|
| `just build` | compiles `cmd/volumen` into `bin/volumen` with `CGO_ENABLED=0`, `-trimpath` and `-buildvcs=true`, stripped, zero errors and zero warnings |
|
||
|
|
| `just test` | the full suite as CI runs it, with the 80 percent coverage floor |
|
||
|
|
| `just race` | the same suite under the race detector; the expensive one |
|
||
|
|
| `just unit ./internal/store 'TestName'` | a scoped run for iterating, cached, no race, no coverage |
|
||
|
|
| `just fuzz <target> <package>` | a time-boxed fuzz of one target in one package, never a gate |
|
||
|
|
| `just bench` | benchmarks with `-benchmem` and five counts, on an idle machine only |
|
||
|
|
| `just fmt` | gofmt over the tree, in place |
|
||
|
|
| `just fmt-check` | zero diff; prints nothing when everything is formatted |
|
||
|
|
| `just vet` | `go vet` and `go fix -diff` |
|
||
|
|
| `just gates` | the definition of done in one command: `build`, `fmt-check`, `vet`, `test`, `race`, in that order |
|
||
|
|
| `just clean` | removes `bin/` and `coverage.out` |
|
||
|
|
| `just install` | builds, then copies the binary into `~/.local/bin` |
|
||
|
|
| `just uninstall` | removes the installed binary |
|
||
|
|
| `just run` | runs the server against `./config.toml` and `./posts`, with the build stamped |
|
||
|
|
| `just dev` | the same, bound to `127.0.0.1:9091` |
|
||
|
|
|
||
|
|
`just fmt` and `just fmt-check` are the formatting authority, `just vet` the
|
||
|
|
static one, and the three of them plus `build`, `test` and `race` are the
|
||
|
|
whole gate. Style rules the tools cannot see (error wrapping, `log/slog`
|
||
|
|
only, no panics outside `main`, British English names and comments) are
|
||
|
|
written in [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||
|
|
|
||
|
|
## Running a single test
|
||
|
|
|
||
|
|
```sh
|
||
|
|
go test -run TestName ./internal/store
|
||
|
|
```
|
||
|
|
|
||
|
|
Narrow selections do not apply the coverage floor (that lives in the `test`
|
||
|
|
recipe), so use them freely while iterating. Or stay on the recipe:
|
||
|
|
`just unit ./internal/store 'TestName'`. Add `-v` for the sub-test names,
|
||
|
|
`-race` when the change touches concurrency, and `-count=1` when a cached
|
||
|
|
result looks stale; the gate itself runs with `-count=1`, so a cached pass
|
||
|
|
never stands in for a fresh one.
|
||
|
|
|
||
|
|
The suite mirrors the package layout (`*_test.go` next to the code), shares
|
||
|
|
no global fixtures, and uses `t.TempDir()` and `net/http/httptest` rather
|
||
|
|
than a real network:
|
||
|
|
|
||
|
|
| Package | Covers |
|
||
|
|
|---|---|
|
||
|
|
| `cmd/volumen` | CLI dispatch and every subcommand against temp directories. |
|
||
|
|
| `internal/admin` | Login, CSRF, post CRUD, settings, media, roles. |
|
||
|
|
| `internal/app` | Server assembly, the route table, `/healthz`, `robots.txt`, media serving, rate-limit headers. |
|
||
|
|
| `internal/audit` | The JSON-lines audit log, and a disabled or unwritable one. |
|
||
|
|
| `internal/backup` | Archive round-trips, the restore allowlist and the decompression budget. |
|
||
|
|
| `internal/biblio` | The `refs` frontmatter into a numbered, linked reference list. |
|
||
|
|
| `internal/config` | Loading, merging, defaults, overrides, validation, and the example file matching the embedded template. |
|
||
|
|
| `internal/diff` | The line-based LCS diff behind the revision comparison view. |
|
||
|
|
| `internal/fediverse` | The `@user@host` handle rule. |
|
||
|
|
| `internal/identifiers` | The scholarly identifier rules: DOI syntax and normalisation, ORCID shape and ISO 7064 check digit. |
|
||
|
|
| `internal/feeds` | RSS, Atom, JSON Feed and sitemap output. |
|
||
|
|
| `internal/frontmatter` | TOML frontmatter parsing, key order and round-trips. |
|
||
|
|
| `internal/httpapi` | The public API over `httptest`: ETags, CORS, pagination, token writes, and the committed contract fixtures. |
|
||
|
|
| `internal/i18n` | The admin interface catalogue: English keys, Czech translations, plural rules. |
|
||
|
|
| `internal/imagefile` | The accepted extensions, the WebP, AVIF and SVG signatures, the detected type and the header dimensions. |
|
||
|
|
| `internal/markdown` | Rendering, sanitisation, the mathematics and diagram passes, the table of contents, and the golden corpus in `testdata/`. |
|
||
|
|
| `internal/password` | scrypt hashing and verification. |
|
||
|
|
| `internal/payloads` | Payload builders, filtering and validation. |
|
||
|
|
| `internal/post` | The Post model: parsing, metadata, rendering, cloning. |
|
||
|
|
| `internal/preview` | The signed share links for unpublished posts. |
|
||
|
|
| `internal/ratelimit` | The sliding-window limiter. |
|
||
|
|
| `internal/scheduler` | Due-post selection, the publish sweep and the interval loop. |
|
||
|
|
| `internal/session` | HMAC-signed cookies and middleware. |
|
||
|
|
| `internal/store` | Loading, saving, deleting posts, revisions, tombstones, media. |
|
||
|
|
| `internal/templates` | The `templates.toml` post templates. |
|
||
|
|
| `internal/tokens` | Minting, scopes, authentication and revocation. |
|
||
|
|
| `internal/tomlfile` | Atomic `0600` TOML writes and the shape helpers. |
|
||
|
|
| `internal/updater` | Release checks, version comparison and the verified self-update. |
|
||
|
|
| `internal/users` | `users.toml`, roles and last-admin safeguards. |
|
||
|
|
| `internal/version` | The release identity from `debug.ReadBuildInfo`. |
|
||
|
|
| `internal/web` | Middleware: gzip, the cross-origin gate, security headers, the CSP nonce, the request logger and the client IP. |
|
||
|
|
| `internal/webhooks` | Signed deliveries, retries, payload shapes. |
|
||
|
|
|
||
|
|
## Coverage
|
||
|
|
|
||
|
|
```sh
|
||
|
|
just test
|
||
|
|
go tool cover -func=coverage.out
|
||
|
|
```
|
||
|
|
|
||
|
|
The `total:` line is the number that matters, and it stays at 80 percent or
|
||
|
|
more; the `test` recipe fails below the floor.
|
||
|
|
|
||
|
|
## Benchmarks
|
||
|
|
|
||
|
|
```sh
|
||
|
|
just bench
|
||
|
|
```
|
||
|
|
|
||
|
|
The recipe is `go test -run '^$' -bench=. -benchmem -count=5 ./...` over the
|
||
|
|
whole module. Two benchmarks live in the tree, `BenchmarkRender` in
|
||
|
|
`internal/markdown` and `BenchmarkServer` in `internal/app`; the binding
|
||
|
|
measurement method is in [BENCHMARKING.md](BENCHMARKING.md). Benchmark on an
|
||
|
|
idle machine, and compare only runs made in one process against each other.
|
||
|
|
|
||
|
|
## Debugging the build
|
||
|
|
|
||
|
|
```sh
|
||
|
|
go build -gcflags='-m' ./... # inlining decisions
|
||
|
|
go build -gcflags='-S' ./... # what the compiler generated
|
||
|
|
```
|
||
|
|
|
||
|
|
## Continuous integration
|
||
|
|
|
||
|
|
Workflows live in `.gitea/workflows/` and run on the project's own runners.
|
||
|
|
They are written by hand rather than through `just`, but they enforce the same
|
||
|
|
set of gates, so a green `just gates` locally is the fastest way to a green
|
||
|
|
pipeline.
|
||
|
|
|
||
|
|
## Releases
|
||
|
|
|
||
|
|
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`. The
|
||
|
|
tag drives the release workflow, which builds the assets and publishes the
|
||
|
|
notes it extracted from `CHANGELOG.md`.
|
||
|
|
|
||
|
|
Dependency changes touch one more file: `NOTICE.md` in the root
|
||
|
|
reproduces the licence of every module the binary is compiled from, and
|
||
|
|
`perl scripts/notices.pl` writes it again after a module is added, removed or
|
||
|
|
upgraded.
|