development.md and agent-rules.mdc in English; progress.md and roadmap.md stay Russian as internal docs. Co-authored-by: Cursor <cursoragent@cursor.com>
6.6 KiB
SelfPost — development
What this file is. How to build, test, and ship changes. Current sprint
state lives in progress.md — read that first after /clear.
Product boundaries: product.md. As-built layout: architecture.md.
Technology stack and tools
| Component | Version / notes |
|---|---|
| Go | 1.26+ (go.mod); CGO_ENABLED=0 — pure Go, static linking |
| SQLite | modernc.org/sqlite (pure Go, no cgo) |
| Build | Makefile: vet, test, build, e2e |
| Container | Docker + Compose v2 on the dev host and in CI |
| Image (build stage) | golang:1.26-bookworm — build/Dockerfile |
| Image (runtime) | debian:bookworm-slim + Postfix, OpenDKIM, supervisord, SASL, logrotate |
| CI | GitHub Actions — .github/workflows/ |
| Image registry | ghcr.io/mixeme/selfpost |
Repository layout (brief; process details in architecture.md):
cmd/panel— HTTP panel + journal-milter + log-tailercmd/selfpost-backup— backup CLI (docker exec … selfpost-backup)internal/— domain logic, store, web, healthbuild/— Dockerfile, supervisord, Postfix/OpenDKIM, entrypointdeploy/—docker-compose.yml, proxy examples,.env.exampletest/e2e/— separate Go module; container integration tests
External libraries
The project is AGPL-3.0 (LICENSE). New Go dependencies must be permissive or GPL-family (see .cursor/rules/agent-rules.mdc).
Main module (go.mod)
| Package | Version | Repository | License |
|---|---|---|---|
github.com/emersion/go-milter |
v0.4.1 | https://github.com/emersion/go-milter | BSD-2-Clause |
golang.org/x/crypto |
v0.54.0 | https://github.com/golang/crypto | BSD-3-Clause |
modernc.org/sqlite |
v1.53.0 | https://gitlab.com/cznic/sqlite (mirror: https://github.com/modernc-org/sqlite) | BSD-3-Clause |
Transitive dependencies — go mod graph / go.sum; all indirect packages in
the tree are AGPL-3.0-compatible.
E2e module (test/e2e/go.mod)
| Package | Version | Repository | License |
|---|---|---|---|
github.com/emersion/go-msgauth |
v0.6.8 | https://github.com/emersion/go-msgauth | BSD-2-Clause |
The test module is not part of the main go build graph and is not shipped in
the image.
Debian packages in the runtime image
Postfix, OpenDKIM, supervisord, sasl2-bin, logrotate, and others come
from Debian bookworm repositories; licenses are in each package's copyright
file on https://packages.debian.org/bookworm/.
Building binaries and the image
Local binaries
Requires Go 1.26+ and CGO_ENABLED=0.
make build # bin/panel, bin/selfpost-backup (VERSION=dev by default)
make build VERSION=1.0.0
Or directly:
go build -trimpath -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=dev" -o bin/panel ./cmd/panel
The version is stamped into both binaries via -ldflags and must match the
Docker image tag — restore checks backup version compatibility.
Docker image
From the repository root:
docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .
The Dockerfile has a build stage (go vet, go build with VERSION) and a
runtime stage (Debian + mail stack). See architecture.md §
Image and processes.
Release build
The release image is published only on tag vX.Y.Z (not on every push to
main). The tag is the single source of version: it drives the image tag and
-ldflags in the binaries so they cannot drift apart.
Steps (on explicit request):
- Close
[Unreleased]in CHANGELOG.md. - Create and push git tag
vX.Y.Z. - Workflow release.yml builds, e2e-gates,
and publishes
ghcr.io/mixeme/selfpost:X.Y.Z. - Update the pinned tag in deploy/docker-compose.yml (see roadmap.md § «v1.x — documentation and deploy tail»).
Ordinary commits do not publish an image.
Testing
Static analysis and unit tests
Main module (go test ./...); e2e is a separate module — see below.
make vet # go vet ./...
make test # go test ./...
Or directly:
gofmt -l . # in CI — fails on drift
go vet ./...
go test ./...
Env documentation regression
go test ./cmd/panel -run TestLoadConfig — every new loadConfig key must
appear in the env lists in guide.md
(cmd/panel/envdoc_test.go).
End-to-end (container suite)
Separate Go module test/e2e/; not included in the main module's
go test ./....
make e2e
# same as: cd test/e2e && go test -v -timeout 20m ./...
Stack: deploy/docker-compose.yml +
test/e2e/compose.override.yml — same
cap_drop/cap_add/no-new-privileges as production. Override: high ports
(20465/20587/20080), test hostname, self-signed TLS,
PANEL_COOKIE_SECURE=false, isolated compose project. Mail is hermetic:
CoreDNS (fake zone) + Postfix smtp-sink as sink-MX; DKIM TXT is scraped from
the panel and published into the zone — the test verifies the records an
operator would actually use.
Coverage (summary): bootstrap → SMTP AUTH → delivery → DKIM verify →
send-log queued → sent; negatives (no AUTH, relay, sender/login mismatch,
L1/L2 limits, milter fail-open, bad SELFPOST_HOSTNAME, session survives
docker restart). Polling with timeouts only — no fixed sleep.
Requires Docker + Compose v2 on the machine running the suite.
CI
Workflows in .github/workflows/. What each job runs — § Testing above.
test.yml — every push and PR to main
gofmt -l → go vet ./... → go test ./... (main module, no e2e).
release.yml — push of tag vX.Y.Z or workflow_dispatch
prepare (version from tag)
→ build [matrix: ubuntu-latest / ubuntu-24.04-arm]
→ docker build --load (VERSION from tag)
→ e2e (test/e2e)
→ push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64
→ merge
→ docker buildx imagetools create → unified manifest X.Y.Z
Native per-arch matrix (no QEMU): running the full Postfix/OpenDKIM stack under emulation for e2e is impractical. E2e first, then push — the registry receives the bytes that passed the gate.
A failed e2e blocks image publication.