Files
volumen/docs/DEVELOPMENT.md
petrbalvin f8ed33df83
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
Initial commit
Assisted-by: GLM 5.3
2026-09-29 10:03:32 +02:00

7.8 KiB

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 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

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.