Assisted-by: GLM 5.3
7.8 KiB
Development
How to work on Volumen.
Prerequisites
- Go 1.27.1, the newest stable release; the
godirective ingo.moddeclares exactly that version, and nothing older builds the module. - just for the recipes.
- gcc, but only for the race detector:
just raceneeds cgo. The build itself is pure Go withCGO_ENABLED=0. - Perl for the scripted recipe lines and
scripts/notices.pl; the base interpreter with builtins only is enough.
Setup
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.
Running a single test
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
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
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. Benchmark on an
idle machine, and compare only runs made in one process against each other.
Debugging the build
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.