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

202 lines
6.6 KiB
Markdown

# SelfPost — development
**What this file is.** How to build, test, and ship changes. Current sprint
state lives in [progress.md](progress.md) — read that first after `/clear`.
Product boundaries: [product.md](product.md). As-built layout:
[architecture.md](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](../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](../build/Dockerfile) |
| **Image (runtime)** | `debian:bookworm-slim` + Postfix, OpenDKIM, supervisord, SASL, logrotate |
| **CI** | GitHub Actions — [.github/workflows/](../.github/workflows/) |
| **Image registry** | `ghcr.io/mixeme/selfpost` |
**Repository layout** (brief; process details in [architecture.md](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](../LICENSE)). New Go dependencies must
be permissive or GPL-family (see
[.cursor/rules/agent-rules.mdc](../.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`.
```sh
make build # bin/panel, bin/selfpost-backup (VERSION=dev by default)
make build VERSION=1.0.0
```
Or directly:
```sh
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:
```sh
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](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](../CHANGELOG.md).
2. Create and push git tag `vX.Y.Z`.
3. Workflow [release.yml](../.github/workflows/release.yml) builds, e2e-gates,
and publishes `ghcr.io/mixeme/selfpost:X.Y.Z`.
4. Update the pinned tag in
[deploy/docker-compose.yml](../deploy/docker-compose.yml) (see
[roadmap.md](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.
```sh
make vet # go vet ./...
make test # go test ./...
```
Or directly:
```sh
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](guide.md)
([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)).
### End-to-end (container suite)
Separate Go module `test/e2e/`; **not** included in the main module's
`go test ./...`.
```sh
make e2e
# same as: cd test/e2e && go test -v -timeout 20m ./...
```
**Stack:** [deploy/docker-compose.yml](../deploy/docker-compose.yml) +
[test/e2e/compose.override.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/](../.github/workflows/). What each job runs —
[§ Testing](#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.