Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-29 10:03:32 +02:00
commit f8ed33df83
206 changed files with 44165 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
# 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.