Files
selfpost/docs/development.md
T
mixeme 1382f8b5f4
test / test (push) Has been cancelled
docs: translate development guide and agent rules to English
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>
2026-08-08 10:58:38 +03:00

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-bookwormbuild/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-tailer
  • cmd/selfpost-backup — backup CLI (docker exec … selfpost-backup)
  • internal/ — domain logic, store, web, health
  • build/ — Dockerfile, supervisord, Postfix/OpenDKIM, entrypoint
  • deploy/docker-compose.yml, proxy examples, .env.example
  • test/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):

  1. Close [Unreleased] in CHANGELOG.md.
  2. Create and push git tag vX.Y.Z.
  3. Workflow release.yml builds, e2e-gates, and publishes ghcr.io/mixeme/selfpost:X.Y.Z.
  4. 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 -lgo 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.