From c92f4bc616f4900f7fbb1c43e118326bc9f39acd Mon Sep 17 00:00:00 2001 From: mixeme Date: Sat, 8 Aug 2026 10:56:22 +0300 Subject: [PATCH] docs: restructure development guide and move agent rules to .cursor Reorganize development.md (stack, deps, build, release, testing, CI); relocate agent rules to .cursor/rules; remove dev-host and mixfed.ru references from docs. Co-authored-by: Cursor --- .cursor/rules/agent-rules.mdc | 24 ++++ .gitignore | 2 +- CHANGELOG.md | 7 +- docs/development.md | 258 ++++++++++++++++++++-------------- docs/progress.md | 21 +-- docs/roadmap.md | 12 +- test/e2e/smtp_client.go | 4 +- 7 files changed, 205 insertions(+), 123 deletions(-) create mode 100644 .cursor/rules/agent-rules.mdc diff --git a/.cursor/rules/agent-rules.mdc b/.cursor/rules/agent-rules.mdc new file mode 100644 index 0000000..819d6ef --- /dev/null +++ b/.cursor/rules/agent-rules.mdc @@ -0,0 +1,24 @@ +--- +description: Правила работы агента в репозитории SelfPost (ранее spec §12) +alwaysApply: true +--- + +# Правила для агентов + +1. **Без git-коммитов** без явной инструкции в промпте. +2. После правок Go: `go build`, `go vet`; исправить все проблемы. `go test`, + когда тесты есть. +3. Перед «готово» для контейнерной задачи: образ собирается, контейнер стартует. +4. Итерации: минимальный скелет, затем фичи. +5. Требования безопасности — `docs/security.md`; внедрять с фичей, не откладывать. +6. Не делать out-of-scope (`docs/product.md`); не менять зафиксированные + допущения без согласования. +7. Крупные задачи — план до кода, если пользователь не утвердил план ранее. +8. Совместимость лицензий новых Go-зависимостей (permissive или GPL-family для + AGPL-3.0). + +Маршрутизация моделей — `docs/progress.md` § «Модель по типу работы». +Предрелизная **ревизия** безопасности (не авторство) — Fable. + +Протокол коммитов, CHANGELOG и закрытия фазы — `docs/progress.md` § «Коммиты» +и «Протокол закрытия фазы». diff --git a/.gitignore b/.gitignore index b204197..e8ba910 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Development-only material (secrets, server access notes) — never published. dev/ -# Build artifacts (binaries are built on the dev server, not committed). +# Build artifacts (not committed). /bin/ *.exe diff --git a/CHANGELOG.md b/CHANGELOG.md index 0920fa0..1c6060f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version and outcome are the line under it, so what the message was and how it ended are both on the first line. The page takes the whole column rather than the reading measure, as the other three monitoring pages already did. +- `docs/development.md` restructured into stack, dependencies, build, release, + testing, and CI; agent rules moved to `.cursor/rules/agent-rules.mdc`; + dev-host-specific workflow and `mixfed.ru` references removed from docs. ## [0.6.0] - 2026-08-08 @@ -617,5 +620,5 @@ of `docs/implementation-plan.md`). - Security pass against spec 7.6 (exec safety, config-write sanitization, server-side validation, rate limiting, session/cookie hardening, output escaping, non-root panel) — full compliance, no code changes required. -- Live production deployment on `selfpost.mixfed.ru` with a real Let's - Encrypt certificate; end-to-end delivery confirmed (DKIM pass, SPF pass). +- Live production deployment with a real Let's Encrypt certificate; + end-to-end delivery confirmed (DKIM pass, SPF pass). diff --git a/docs/development.md b/docs/development.md index 7dfe632..489307d 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,146 +1,198 @@ -# SelfPost — development +# SelfPost — разработка -**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`. +**Что это.** Как собрать, проверить и выкатить изменения. Текущий спринт — +[progress.md](progress.md) (читать первым после `/clear`). -Product boundaries: [product.md](product.md). As-built layout: +Границы продукта: [product.md](product.md). As-built устройство: [architecture.md](architecture.md). --- -## Repository layout +## Технологический стек и инструменты -- `cmd/panel` — panel HTTP server + milter + log-tailer -- `cmd/selfpost-backup` — CLI backup (`docker exec … selfpost-backup`) -- `internal/` — domain logic, store, web handlers, health checks -- `build/` — Dockerfile, supervisord, Postfix/OpenDKIM wiring, entrypoint -- `deploy/` — `docker-compose.yml`, proxy examples, `.env.example` -- `test/e2e/` — **separate Go module**; container integration tests +| Компонент | Версия / заметки | +|---|---| +| **Go** | 1.26+ (`go.mod`); `CGO_ENABLED=0` — чистый Go, статическая линковка | +| **SQLite** | `modernc.org/sqlite` (pure Go, без cgo) | +| **Сборка** | [Makefile](../Makefile): `vet`, `test`, `build`, `e2e` | +| **Контейнер** | Docker + Compose v2 на хосте разработки и в CI | +| **Образ (build stage)** | `golang:1.26-bookworm` — [build/Dockerfile](../build/Dockerfile) | +| **Образ (runtime)** | `debian:bookworm-slim` + Postfix, OpenDKIM, supervisord, SASL, logrotate | +| **CI** | GitHub Actions — [.github/workflows/](../.github/workflows/) | +| **Реестр образов** | `ghcr.io/mixeme/selfpost` | + +**Структура репозитория** (кратко; детали процессов — в [architecture.md](architecture.md)): + +- `cmd/panel` — HTTP-панель + journal-milter + log-tailer +- `cmd/selfpost-backup` — CLI бэкапа (`docker exec … selfpost-backup`) +- `internal/` — доменная логика, store, web, health +- `build/` — Dockerfile, supervisord, Postfix/OpenDKIM, entrypoint +- `deploy/` — `docker-compose.yml`, примеры прокси, `.env.example` +- `test/e2e/` — **отдельный Go-модуль**; контейнерные интеграционные тесты --- -## Local Go workflow +## Внешние библиотеки -Requires Go 1.26+ and `CGO_ENABLED=0` (pure Go SQLite). +Проект — **AGPL-3.0** ([LICENSE](../LICENSE)). Новые Go-зависимости — только +permissive или GPL-family (см. [.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)). + +### Основной модуль (`go.mod`) + +| Пакет | Версия | Репозиторий | Лицензия | +|---|---|---|---| +| `github.com/emersion/go-milter` | v0.4.1 | | BSD-2-Clause | +| `golang.org/x/crypto` | v0.54.0 | | BSD-3-Clause | +| `modernc.org/sqlite` | v1.53.0 | (зеркало: ) | BSD-3-Clause | + +Транзитивные зависимости — `go mod graph` / `go.sum`; все indirect в дереве +совместимы с AGPL-3.0. + +### Модуль e2e (`test/e2e/go.mod`) + +| Пакет | Версия | Репозиторий | Лицензия | +|---|---|---|---| +| `github.com/emersion/go-msgauth` | v0.6.8 | | BSD-2-Clause | + +Тестовый модуль не входит в граф основного `go build` и не попадает в образ. + +### Пакеты Debian в runtime-образе + +Postfix, OpenDKIM, `supervisord`, `sasl2-bin`, `logrotate` и др. — +из репозиториев Debian bookworm; лицензии — в `copyright` соответствующих +пакетов на . + +--- + +## Сборка исполняемого файла и образа + +### Локальные бинарники + +Требуется Go 1.26+ и `CGO_ENABLED=0`. + +```sh +make build # bin/panel, bin/selfpost-backup (VERSION=dev по умолчанию) +make build VERSION=1.0.0 +``` + +Или напрямую: + +```sh +go build -trimpath -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=dev" -o bin/panel ./cmd/panel +``` + +Версия вшивается в оба бинарники через `-ldflags` и **должна совпадать с тегом +Docker-образа** — restore проверяет совместимость версий бэкапа. + +### Docker-образ + +Из корня репозитория: + +```sh +docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev . +``` + +В Dockerfile: build stage (`go vet`, `go build` с `VERSION`), runtime stage +(Debian + почтовый стек). См. [architecture.md](architecture.md) § Image and +processes. + +--- + +## Сборка релиза + +Релизный образ публикуется **только по тегу** `vX.Y.Z` (не на каждый push в +`main`). Тег — единственный источник версии: из него берутся тег образа и +`-ldflags` в бинарниках, чтобы они не расходились. + +**Шаги (по явному запросу):** + +1. Закрыть `[Unreleased]` в [CHANGELOG.md](../CHANGELOG.md). +2. Создать и запушить git-тег `vX.Y.Z`. +3. Workflow [release.yml](../.github/workflows/release.yml) собирает, гейтит + e2e и публикует `ghcr.io/mixeme/selfpost:X.Y.Z`. +4. Обновить закреплённый тег в [deploy/docker-compose.yml](../deploy/docker-compose.yml) + (см. [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя»). + +Обычные коммиты **не** публикуют образ. + +--- + +## Тестирование + +### Статический анализ и unit-тесты + +Основной модуль (`go test ./...`); e2e — отдельный модуль, см. ниже. ```sh make vet # go vet ./... make test # go test ./... -make build # bin/panel, bin/selfpost-backup (VERSION=dev by default) -make build VERSION=1.0.0 ``` -Or directly: +Или напрямую: ```sh +gofmt -l . # в CI — fail при расхождении go vet ./... go test ./... -go build -trimpath -ldflags "-X github.com/mixeme/selfpost/internal/buildinfo.Version=dev" -o bin/panel ./cmd/panel ``` -**Env documentation regression:** `go test ./cmd/panel -run TestLoadConfig` — -new `loadConfig` keys must appear in guide.md env lists +### Регресс документации env + +`go test ./cmd/panel -run TestLoadConfig` — каждый новый ключ `loadConfig` +должен появиться в списках env в [guide.md](guide.md) ([cmd/panel/envdoc_test.go](../cmd/panel/envdoc_test.go)). ---- +### End-to-end (контейнерный suite) -## End-to-end tests - -Hermetic container suite (implemented as `test/e2e/`, separate Go module): +Отдельный Go-модуль `test/e2e/`; **не** входит в `go test ./...` основного +модуля. ```sh make e2e -# equivalent: cd test/e2e && go test -v -timeout 20m ./... +# то же: cd test/e2e && go test -v -timeout 20m ./... ``` -**Stack:** shipped [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 uses 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 so the test verifies the record the operator would -use. +**Стек:** [deploy/docker-compose.yml](../deploy/docker-compose.yml) + +[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — те же +`cap_drop`/`cap_add`/`no-new-privileges`, что в production. Override: высокие +порты (`20465`/`20587`/`20080`), тестовый hostname, self-signed TLS, +`PANEL_COOKIE_SECURE=false`, изолированный compose-проект. Почта герметична: +CoreDNS (fake zone) + Postfix `smtp-sink` как sink-MX; DKIM TXT скрапится с +панели и публикуется в зону — тест проверяет записи, которые оператор реально +использует. -**Coverage (summary):** full 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`. +**Покрытие (сводка):** bootstrap → SMTP AUTH → доставка → DKIM verify → +send-log `queued → sent`; негативы (без AUTH, relay, sender/login mismatch, +L1/L2 лимиты, fail-open milter, неверный `SELFPOST_HOSTNAME`, сессия после +`docker restart`). Только polling с таймаутами — без фиксированных `sleep`. -Requires **Docker + Compose v2** on the test host. Not included in `go test ./...` -of the main module. - -**CI** ([release.yml](../.github/workflows/release.yml)): tag `vX.Y.Z` triggers -`prepare` (version from tag) → native matrix `[ubuntu-latest, ubuntu-24.04-arm]` -— build `--load`, e2e, push per-arch tag → `merge` publishes unified -`ghcr.io/...:X.Y.Z` via `docker buildx imagetools create`. Failed e2e blocks the -image. Ordinary pushes still run only `vet`/`test` in [test.yml](../.github/workflows/test.yml). +Требует **Docker + Compose v2** на машине, где гоняется suite. --- -## Dev server (full container) +## CI -When unit tests are not enough — Postfix, OpenDKIM, supervisord, real SMTP: +Workflows в [.github/workflows/](../.github/workflows/). Что именно гоняется — +в [§ Тестирование](#тестирование) выше. -**Typical setup:** edit locally → sync tree to dev host → build/test there. +### `test.yml` — каждый push и PR в `main` -Documented dev host: `selfpost.mixfed.ru` (Debian 12). Sync example from -[progress.md](progress.md): +`gofmt -l` → `go vet ./...` → `go test ./...` (основной модуль, без e2e). -```sh -tar -czf - --exclude=.git . | ssh root@selfpost.mixfed.ru \ - 'rm -rf /root/selfpost-src && mkdir -p /root/selfpost-src && tar -xzf - -C /root/selfpost-src' +### `release.yml` — push тега `vX.Y.Z` или `workflow_dispatch` + +``` +prepare (версия из тега) + → build [matrix: ubuntu-latest / ubuntu-24.04-arm] + → docker build --load (VERSION из тега) + → e2e (test/e2e) + → push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64 + → merge + → docker buildx imagetools create → единый манифест X.Y.Z ``` -On the server (Go in `/usr/local/go/bin` if not in PATH): +Нативная матрица per-arch (без QEMU): полный стек Postfix/OpenDKIM под +эмуляцией для e2e непрактичен. Сначала e2e, затем push — в registry попадают +байты, прошедшие гейт. -```sh -cd /root/selfpost-src -/usr/local/go/bin/go vet ./... -/usr/local/go/bin/go test ./... -docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev . -``` - -Manual smoke on production-like host: panel at `https://selfpost.mixfed.ru`, -real LE cert, live deliverability — not replaceable by e2e alone (no outbound -25 on CI runners, no real PTR/reputation). - ---- - -## Commit and changelog protocol - -From [progress.md](progress.md): - -1. Meaningful step → entry under `[Unreleased]` in [CHANGELOG.md](../CHANGELOG.md) - (Keep a Changelog format). -2. Commit on `main` unless asked for a branch. **Do not commit unless the user - asks.** -3. Before `/clear` at end of a phase: update `progress.md`, verify acceptance - criteria, CHANGELOG, final commit. - -Documentation changes that add/rename env vars, panel routes, or observable -mail behaviour ship in the **same commit** as the code change. - -Release tagging and image push — only on explicit request. - ---- - -## Agent rules (formerly spec §12) - -1. **No git commits** without explicit instruction in the prompt. -2. After Go changes: `go build`, `go vet`; fix all issues. Run `go test` when - tests exist. -3. Before calling a container task done: image builds and container starts. -4. Iterate: minimal skeleton first, then features. -5. Security requirements in [security.md](security.md) — implement with the feature, - not deferred. -6. Do not implement out-of-scope items ([product.md](product.md)) or change - fixed assumptions without agreement. -7. For large tasks: propose a plan before coding unless the user already approved - one. -8. Check licence compatibility of new Go dependencies (permissive or - GPL-family for AGPL-3.0 project). - -Model routing (from progress): security/infra → Opus; UI/docs → Sonnet; -trivial mechanics → Haiku. Pre-release security **review** (not authorship) → -Fable. +Провал e2e **блокирует** публикацию образа. diff --git a/docs/progress.md b/docs/progress.md index df140ac..99a2444 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -43,21 +43,24 @@ - **Выполнено и принято:** базовый линейный план 0→11 (v1.0; аудит безопасности ТЗ 7.6 — полное соответствие), Фаза 12 (UI/UX), Фаза 13 (страница `/status`, DNS-проверки домена) и Фаза 14 (security-заголовки, проверка origin, cookie `__Host-` + обнаружение дублей, документация про `/data/setup-token`). Что именно сделано — в `git log` и `CHANGELOG.md`, здесь не дублируется. - **B.1 реализован** (не выкачен на прод): сессии переехали в SQLite (`internal/store/migrations/0002_sessions.sql`, `internal/store/sessions.go`, `internal/web/session.go`) — хранится SHA-256 токена, не сам токен; скользящий срок бездействия `PANEL_SESSION_IDLE_DAYS` (по умолчанию 7 дней, без абсолютного потолка); запись в БД продлевается не чаще раза в час (`renewThreshold`); опросы мониторинга (`GET` с `HX-Request`) продление не триггерят (`isSessionActivity` в `internal/web/middleware.go`); `Max-Age` cookie выставляется тем же значением при логине и при продлении (`setSessionCookie`); смена пароля разлогинивает все сессии кроме текущей (уже было, теперь через БД). Проверено на стенде: логин → рестарт процесса панели → сессия жива по старой cookie; HX-Request-опрос и повторный GET внутри часового окна не шлют `Set-Cookie`. `go vet`/`go test ./...`/`gofmt -l .` чистые. -- **B.2 реализован** (не выкачен на прод): ротация `mail.log` ушла с `copytruncate` на «переименовать + `postfix reload`» — `build/logrotate-mail.conf` (`nocreate` заменён на `create 0644 root root` **не по плану, а по стендовой проверке**: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом `0600`, недоступным непривилегированной панели, — `create` в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); `follow()` в `internal/logtail/logtail.go` при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; `readLogTail()` в `internal/web/handlers_monitor.go` считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (`selfpost.mixfed.ru`, отдельный контейнер `selfpost:b2test2`): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает `mail.log` сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. `go vet`/`go test ./...`/`gofmt -l .` чистые (на dev-сервере; локально на Windows `TestFollowTailsAndRotates` падает — rename открытого файла запрещён ОС, к делу не относится). -- **B.3 реализован** (не выкачен на прод): `build/entrypoint.sh` проверяет `SELFPOST_HOSTNAME` до `postfix-config.sh` и до `supervisord` — при пустом значении `exit 1` с развёрнутым текстом ошибки (что это за имя, почему обязательно, пример, где задаётся); плюс синтаксическая проверка через `case`: минимум одна точка, без схемы/порта/пробелов (`*://*`, `*:*`, пробел/таб — тот же класс тихого спам-отказа, что и пустое значение). `saslRealm()` и fallback в `postfix-config.sh` не тронуты — после гейта эти ветки мертвы. Заодно отмечена обязательность переменной в `README.md` и `deploy/.env.example`. Проверено на стенде (`selfpost.mixfed.ru`, отдельный образ `selfpost:b3test`, cap-list как в поставляемом compose): без переменной — `exit 1` с ожидаемым текстом, без бесконечного тихого retry; `https://mail.example.com:465` и `localhost` отклонены с понятными сообщениями; валидный `mail.example.com` — обычный старт, все процессы supervisord поднимаются. `go vet`/`go test ./...` чистые. -- **C.4 реализован** (не выкачен на прод — это CI/тестовая инфраструктура, а не образ): герметичный контейнерный e2e отдельным Go-модулем `test/e2e/` (свой `go.mod`, не подхватывается `go test ./...` основного модуля) поверх поставляемого `deploy/docker-compose.yml` плюс `test/e2e/compose.override.yml` (самоподписанный сертификат, `PANEL_COOKIE_SECURE=false`, `SELFPOST_HOSTNAME=mail.e2e.test`, высокие порты `20465/20587/20080`, изолированный compose-проект `selfpost-e2e`, свой `--project-directory` — прод на том же хосте не задет). Герметичная почта: CoreDNS (`test/e2e/dns/Corefile` — авторитетна только для `e2e.test`, `file`-плагин с саб-директивой `reload` перечитывает `db.zone` по mtime, без сигналов) плюс `smtp-sink` из пакета postfix (`test/e2e/sink/`) как sink-MX. Сценарий (`test/e2e/*_test.go`): старт контейнера → все supervisord-программы `RUNNING` (`postfix-reload` — `STOPPED`) → токен из `/data/setup-token` → setup → login → добавление домена → DKIM-запись **скраплена со страницы панели** и опубликована в фейковую зону → добавление приложения → SMTP AUTH на 465 → письмо на sink → DKIM-подпись проверена (`go-msgauth/dkim` с кастомным `LookupTXT` через CoreDNS) против ключа **из DNS**, не из панели напрямую → send-log `queued → sent`. Негативы: без AUTH, relay на чужой домен без AUTH, sender/login mismatch (`reject_sender_login_mismatch` репортится Postfix'ом на RCPT, не MAIL — `smtpd_delay_reject=yes` по умолчанию), L1-лимит (anvil, override `RATE_LIMIT_MESSAGES_PER_IP=50` — специально высокий, чтобы остальные под-тесты не расходовали общий бюджет по IP раньше времени; сам тест шлёт до 60 раз, ждёт отказа), L2-лимит через панель (домен/приложение → `rejected`-строка в send-log), fail-open journal-milter'а (`supervisorctl stop panel`, письмо всё равно принято, контейнер жив), пустой/синтаксически неверный `SELFPOST_HOSTNAME` (отдельный один-разовый контейнер, не общий стенд), сессия переживает `docker restart` (плюс явное ожидание готовности smtps-порта после рестарта — панель и Postfix поднимаются независимо). `make e2e` — локальный/dev-server прогон. Найдено и исправлено по ходу стендовой проверки: `reload` — саб-директива `file`-плагина CoreDNS, а не отдельный топ-левел плагин (топ-левел `reload` следит за самим Corefile, не за зоной); `docker compose build.context` резолвится относительно `--project-directory`, а не относительно файла, где объявлен; `smtp-sink` отказывается стартовать от root без `-u`; `html/template` эскейпит `+` в `+` даже в тексте — скрапер значений со страницы обязан `html.UnescapeString`; проверки состояния сразу после `up`/`restart` должны поллиться, а не разово опрашиваться (supervisord/postfix поднимаются не мгновенно). **Проверено на dev-сервере (`selfpost.mixfed.ru`)**: `make e2e` — зелёный (`go vet`/`gofmt -l` тоже чистые в обоих модулях). `release.yml` переработан: job `prepare` (версия из тега) → матрица `[ubuntu-latest, ubuntu-24.04-arm]` — каждая нативно собирает образ (`--load`), прогоняет e2e, пушит тег `X.Y.Z-amd64`/`X.Y.Z-arm64` → job `merge` — `docker buildx imagetools create` в единый тег `X.Y.Z`; `setup-qemu-action` убран. Не проверено вживую (нельзя без реального тега): сам workflow на GitHub Actions — синтаксис вычитан, логика идентична локальному `make e2e` пути. +- **B.2 реализован** (не выкачен на прод): ротация `mail.log` ушла с `copytruncate` на «переименовать + `postfix reload`» — `build/logrotate-mail.conf` (`nocreate` заменён на `create 0644 root root` **не по плану, а по стендовой проверке**: после reload Postfix пересоздаёт лог сам только в момент следующей фактической записи и с режимом `0600`, недоступным непривилегированной панели, — `create` в logrotate закрывает это, отдавая файл ей же на 644 сразу после переименования); `follow()` в `internal/logtail/logtail.go` при обнаружении смены inode дочитывает старый дескриптор ещё раз перед переключением; `readLogTail()` в `internal/web/handlers_monitor.go` считает отсутствующий файл пустым экраном, а не ошибкой. Проверено на стенде (отдельный контейнер `selfpost:b2test2`): цикл трафик → принудительная ротация → файл пуст и сразу читаем непривилегированным uid панели (0 читает `mail.log` сразу после rename, без окна недоступности) → новый трафик после ротации уходит в новый файл на 644, ничего не потеряно по обе стороны rename. `go vet`/`go test ./...`/`gofmt -l .` чистые (на стенде; локально на Windows `TestFollowTailsAndRotates` падает — rename открытого файла запрещён ОС, к делу не относится). +- **B.3 реализован** (не выкачен на прод): `build/entrypoint.sh` проверяет `SELFPOST_HOSTNAME` до `postfix-config.sh` и до `supervisord` — при пустом значении `exit 1` с развёрнутым текстом ошибки (что это за имя, почему обязательно, пример, где задаётся); плюс синтаксическая проверка через `case`: минимум одна точка, без схемы/порта/пробелов (`*://*`, `*:*`, пробел/таб — тот же класс тихого спам-отказа, что и пустое значение). `saslRealm()` и fallback в `postfix-config.sh` не тронуты — после гейта эти ветки мертвы. Заодно отмечена обязательность переменной в `README.md` и `deploy/.env.example`. Проверено на стенде (отдельный образ `selfpost:b3test`, cap-list как в поставляемом compose): без переменной — `exit 1` с ожидаемым текстом, без бесконечного тихого retry; `https://mail.example.com:465` и `localhost` отклонены с понятными сообщениями; валидный `mail.example.com` — обычный старт, все процессы supervisord поднимаются. `go vet`/`go test ./...` чистые. +- **C.4 реализован** (не выкачен на прод — это CI/тестовая инфраструктура, а не образ): герметичный контейнерный e2e отдельным Go-модулем `test/e2e/` (свой `go.mod`, не подхватывается `go test ./...` основного модуля) поверх поставляемого `deploy/docker-compose.yml` плюс `test/e2e/compose.override.yml` (самоподписанный сертификат, `PANEL_COOKIE_SECURE=false`, `SELFPOST_HOSTNAME=mail.e2e.test`, высокие порты `20465/20587/20080`, изолированный compose-проект `selfpost-e2e`, свой `--project-directory` — прод на том же хосте не задет). Герметичная почта: CoreDNS (`test/e2e/dns/Corefile` — авторитетна только для `e2e.test`, `file`-плагин с саб-директивой `reload` перечитывает `db.zone` по mtime, без сигналов) плюс `smtp-sink` из пакета postfix (`test/e2e/sink/`) как sink-MX. Сценарий (`test/e2e/*_test.go`): старт контейнера → все supervisord-программы `RUNNING` (`postfix-reload` — `STOPPED`) → токен из `/data/setup-token` → setup → login → добавление домена → DKIM-запись **скраплена со страницы панели** и опубликована в фейковую зону → добавление приложения → SMTP AUTH на 465 → письмо на sink → DKIM-подпись проверена (`go-msgauth/dkim` с кастомным `LookupTXT` через CoreDNS) против ключа **из DNS**, не из панели напрямую → send-log `queued → sent`. Негативы: без AUTH, relay на чужой домен без AUTH, sender/login mismatch (`reject_sender_login_mismatch` репортится Postfix'ом на RCPT, не MAIL — `smtpd_delay_reject=yes` по умолчанию), L1-лимит (anvil, override `RATE_LIMIT_MESSAGES_PER_IP=50` — специально высокий, чтобы остальные под-тесты не расходовали общий бюджет по IP раньше времени; сам тест шлёт до 60 раз, ждёт отказа), L2-лимит через панель (домен/приложение → `rejected`-строка в send-log), fail-open journal-milter'а (`supervisorctl stop panel`, письмо всё равно принято, контейнер жив), пустой/синтаксически неверный `SELFPOST_HOSTNAME` (отдельный один-разовый контейнер, не общий стенд), сессия переживает `docker restart` (плюс явное ожидание готовности smtps-порта после рестарта — панель и Postfix поднимаются независимо). `make e2e` — локальный прогон. Найдено и исправлено по ходу стендовой проверки: `reload` — саб-директива `file`-плагина CoreDNS, а не отдельный топ-левел плагин (топ-левел `reload` следит за самим Corefile, не за зоной); `docker compose build.context` резолвится относительно `--project-directory`, а не относительно файла, где объявлен; `smtp-sink` отказывается стартовать от root без `-u`; `html/template` эскейпит `+` в `+` даже в тексте — скрапер значений со страницы обязан `html.UnescapeString`; проверки состояния сразу после `up`/`restart` должны поллиться, а не разово опрашиваться (supervisord/postfix поднимаются не мгновенно). **Проверено на стенде**: `make e2e` — зелёный (`go vet`/`gofmt -l` тоже чистые в обоих модулях). `release.yml` переработан: job `prepare` (версия из тега) → матрица `[ubuntu-latest, ubuntu-24.04-arm]` — каждая нативно собирает образ (`--load`), прогоняет e2e, пушит тег `X.Y.Z-amd64`/`X.Y.Z-arm64` → job `merge` — `docker buildx imagetools create` в единый тег `X.Y.Z`; `setup-qemu-action` убран. Не проверено вживую (нельзя без реального тега): сам workflow на GitHub Actions — синтаксис вычитан, логика идентична локальному `make e2e` пути. - **Документация:** план D1–D9 закрыт ([documentation-plan.md](documentation-plan.md) — только метод и правила поддержки). Хвост v1.x — [roadmap.md](roadmap.md) § «v1.x — хвост документации и деплоя»; из него остался только бамп тега образа (Quick start и `docs/logo` закрыты). - **Рецензирование кодовой базы** (2026-08-05, `522425a`): 10 разделов (архитектура, качество, docs, GUI, legacy, риски) плюс приоритизированный план доработок фазами 0–3. Критичных багов не найдено; единственным блокером релиза названа § D. **План выполнен целиком** (см. записи ниже), поэтому сам документ `docs/code-review.md` удалён — незакрытые пункты унесены в [roadmap.md](roadmap.md) (разбиение `internal/web`, индекс документации в README, адаптивный интервал опроса, `CONTRIBUTING.md`), остальное либо сделано, либо уже описано в architecture.md / security.md / комментариях кода. Текст ревизии — в git-истории. - **§ D выполнен (2026-08-06):** предрелизная ревизия безопасности моделью Fable — диф от аудита v1.0 (Фаза 11, `bd64e80`) до HEAD + полный проход по чек-листу [security.md](security.md) (бывшее ТЗ 7.6). Эксплуатируемых находок нет; одна правка defence-in-depth (`--` перед логином в argv `saslpasswd2`, `internal/app/sasl.go` + тест). Принятые риски не пополнились. Детали — [implementation-plan.md](implementation-plan.md) § D и CHANGELOG `[Unreleased]/Security`. Локально `go vet`/`go test ./internal/app/...` чистые; падения `internal/domain` (`TestWriteLoadPrivateKeyRoundtrip`, `TestRenderTables`) и `internal/logtail` (`TestFollowTailsAndRotates`) — Windows-специфика (права файлов/`\` в путях/rename открытого файла), на Linux CI зелено. -- **Фаза 1 плана ревизии выполнена (2026-08-06)** (doc/code hygiene, P1): cleanup ~30 stale «Phase N» комментариев в коде и shell-скриптах; исправлен stale-комментарий в `handlers_domains.go`; ADR CSRF (Origin vs токены) добавлен в [security.md](security.md); known-limitations по log-tailer уже был в [architecture.md](architecture.md) § Log tailer — отдельного действия не потребовалось; `docs/logo` в [roadmap.md](roadmap.md) закрыт (каталога нет, критерию соответствует); `gofmt -l` добавлен в CI (`.github/workflows/test.yml`). `gofmt`/`go vet`/`go test ./...` чистые в обоих модулях (dev-server). -- **Фаза 1.5 плана ревизии выполнена (2026-08-06)** (шифрование резервных копий, P1): новый пакет `internal/secretfile` — конверт `magic SELFPOST1 | type | scrypt-параметры | salt | nonce-prefix` + поток 64 KiB чанков AES-256-GCM, каждый с AAD `header+counter+last`, поэтому обрезка, перестановка и подмена не открываются (стриминг в обе стороны — полный бэкап не держится в памяти). Панель: чекбокс «Encrypt with a password» в форме полного бэкапа и экспорта домена (общий партиал `templates/encrypt_fields.html`, показ/очистка полей — `panel.js`, без inline-скриптов), импорт домена принимает `.spde` (шифрование определяется по magic, не по расширению) с полем пароля. CLI `selfpost-backup`: пишет `.spbk` при заданном пароле и умеет `-decrypt` (иначе зашифрованный бэкап нечем распаковать при restore); пароль — только `SELFPOST_BACKUP_PASSWORD` / `-password-file`, никогда argv. Умолчание не изменилось: галочка снята — прежние `.tar.gz` / `.json` байт в байт. Тесты: round-trip по размерам (0, границы чанка, несколько чанков), неверный пароль, обрезка, перестановка чанков, порча байта, чужие KDF-параметры; валидация формы пароля; round-trip CLI create→decrypt→tar. Docs: README § *Encrypting a backup or export*, [security.md](security.md) § «Резервная копия и экспорт домена» + принятый риск (шифрование опционально), [architecture.md](architecture.md) § Persistence. `gofmt`/`go vet`/`go test ./...` чистые (кроме известных Windows-падений `internal/domain`, `internal/logtail`). E2E-сценарий не добавлялся: в `test/e2e/` бэкапа не было и раньше, а прогнать новый тест локально нечем (нет Docker) — кандидат при следующем прогоне на dev-сервере. -- **Фаза 2 плана ревизии выполнена (2026-08-06)** (GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на dev-сервере. -- **Фаза 3 плана ревизии выполнена (2026-08-06)** (operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на dev-сервере. +- **Фаза 1 плана ревизии выполнена (2026-08-06)** (doc/code hygiene, P1): cleanup ~30 stale «Phase N» комментариев в коде и shell-скриптах; исправлен stale-комментарий в `handlers_domains.go`; ADR CSRF (Origin vs токены) добавлен в [security.md](security.md); known-limitations по log-tailer уже был в [architecture.md](architecture.md) § Log tailer — отдельного действия не потребовалось; `docs/logo` в [roadmap.md](roadmap.md) закрыт (каталога нет, критерию соответствует); `gofmt -l` добавлен в CI (`.github/workflows/test.yml`). `gofmt`/`go vet`/`go test ./...` чистые в обоих модулях. +- **Фаза 1.5 плана ревизии выполнена (2026-08-06)** (шифрование резервных копий, P1): новый пакет `internal/secretfile` — конверт `magic SELFPOST1 | type | scrypt-параметры | salt | nonce-prefix` + поток 64 KiB чанков AES-256-GCM, каждый с AAD `header+counter+last`, поэтому обрезка, перестановка и подмена не открываются (стриминг в обе стороны — полный бэкап не держится в памяти). Панель: чекбокс «Encrypt with a password» в форме полного бэкапа и экспорта домена (общий партиал `templates/encrypt_fields.html`, показ/очистка полей — `panel.js`, без inline-скриптов), импорт домена принимает `.spde` (шифрование определяется по magic, не по расширению) с полем пароля. CLI `selfpost-backup`: пишет `.spbk` при заданном пароле и умеет `-decrypt` (иначе зашифрованный бэкап нечем распаковать при restore); пароль — только `SELFPOST_BACKUP_PASSWORD` / `-password-file`, никогда argv. Умолчание не изменилось: галочка снята — прежние `.tar.gz` / `.json` байт в байт. Тесты: round-trip по размерам (0, границы чанка, несколько чанков), неверный пароль, обрезка, перестановка чанков, порча байта, чужие KDF-параметры; валидация формы пароля; round-trip CLI create→decrypt→tar. Docs: README § *Encrypting a backup or export*, [security.md](security.md) § «Резервная копия и экспорт домена» + принятый риск (шифрование опционально), [architecture.md](architecture.md) § Persistence. `gofmt`/`go vet`/`go test ./...` чистые (кроме известных Windows-падений `internal/domain`, `internal/logtail`). E2E-сценарий не добавлялся: в `test/e2e/` бэкапа не было и раньше, а прогнать новый тест локально нечем (нет Docker) — кандидат при следующем прогоне на стенде. +- **Фаза 2 плана ревизии выполнена (2026-08-06)** (GUI polish, P2): опрос мониторинговых страниц не уходит на сервер, пока вкладка скрыта — фильтр повешен на `htmx:beforeRequest` в `panel.js`, а не на встроенный в htmx фильтр триггера (тот вычисляется через `new Function`, что CSP панели `default-src 'self'` без `unsafe-eval` молча ломает); тёмная тема переписана с каскада `!important` на переопределение CSS-переменных в одном блоке `prefers-color-scheme: dark`; дублирующее правило `main { max-width }` сведено к одному базовому плюс задокументированные постраничные оверрайды. Только CSS/JS, поведения сервера не касается; вживую не проверялось (нет Docker локально) — кандидат на следующий прогон на стенде. +- **Фаза 3 плана ревизии выполнена (2026-08-06)** (operational improvements, P2–P3): (1) log-tailer сохраняет позицию чтения — таблица `logtail_state` (миграция `0003`, `internal/store/logtail.go`) хранит offset + отпечаток первых 512 байт лога, `internal/logtail/offset.go` решает откуда стартовать: отпечаток совпал → продолжаем с offset (дочитывается хвост, написанный пока панель лежала); не совпал (лог сменился/пересоздан) → читаем файл с начала (повторный разбор безвреден, `UpdateStatus` идемпотентен); записи нет вовсе (первый запуск) → с конца, как раньше. Запись offset — не чаще раза в 5 с, плюс форс при ротации и на выключении; сохраняется позиция *потреблённых* байт (минус недочитанная частичная строка). (2) L2-лимит перестал промахиваться при параллельных сессиях: между проверкой на MAIL FROM и вставкой строки на end-of-message сообщение не видно в БД, поэтому N одновременных сессий пропускали друг друга — теперь к счёту из БД добавляются «в полёте» (`internal/milter/inflight.go`, общий на процесс реестр резерваций); резервация освобождается после записи в send-log, на ABORT и по TTL 10 минут (у go-milter нет колбэка на закрытие соединения, а вечная резервация — это fail-closed-дрейф, которого у лимитера быть не должно). Транзакция «count+insert», как предлагал review, невозможна буквально: эти два шага разнесены по разным стадиям SMTP-транзакции. Тесты: restart/rotation-resume для tailer'а, четыре сценария резерваций для лимита. `gofmt`/`go vet` чистые; `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). Не проверено на стенде (нет Docker локально) — кандидат на следующий прогон на стенде. - **Добор по плану ревизии выполнен (2026-08-06):** (1) проект переехал на единственную площадку — GitHub (Codeberg уходит): вместе с URL, лицензионными шапками SVG/HTML и docs переехал путь Go-модуля на `github.com/mixeme/selfpost` (`go.mod`, `test/e2e/go.mod`, все импорты, `MODULE` в Makefile, `-ldflags` в Dockerfile и development.md) — оставлять импорты на исчезающем хосте нельзя, `go get`/`go install` сломались бы; (2) ссылки на архивную спецификацию убраны из кода целиком — не только «spec 7.x», как просило ревью, но и «spec 4/5/6/8/9», страдавшие тем же, каждая заменена на живой документ с секцией там, где документ большой; (3) [architecture.md](architecture.md) § Code layers — диаграмма слоёв (A2); (4) `TestParseDelivery` расширен экзотикой mail.log — и **вскрыл реальный баг**: шаблон брал `status=` жадно, то есть последнее вхождение в строке, а Postfix дописывает ответ удалённого сервера дословно, поэтому отказ с `status=sent` в тексте ответа попадал в журнал как доставленный (исправлено на ленивый разбор); (5) `CONTRIBUTING.md` перенесён в 2.x, бамп тега образа и git-тег оставлены в [roadmap.md](roadmap.md) § v1.x. `gofmt`/`go vet` чистые в обоих модулях, `go test ./...` — падения только известные Windows-специфичные (`internal/domain`, `TestFollowTailsAndRotates`). На стенде не проверялось (нет Docker локально). - **Дальше:** релизный гейт (Фаза 0) закрыт по существу — e2e C.4 и ревизия § D пройдены; остаются только шаги, которые делаются в момент резки версии (бамп тега образа в compose, git tag) по явной команде пользователя. Все polish-фазы плана ревизии (1, 1.5, 2, 3) закрыты. - **Принятые риски** — [security.md](security.md). **Опционально v1.x / 2.x** — [roadmap.md](roadmap.md) (хвост документации, send-log gaps, Фаза O1+, роль администратора домена). -- **Прод:** `selfpost.mixfed.ru`, реальный Let's Encrypt сертификат, живой e2e (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает). +- **Прод:** инстанс с реальным Let's Encrypt сертификатом и живым deliverability (DKIM/SPF pass). Контейнер там всё ещё на образе v1.0 — Фаза 14 в него не выкатывалась. При апгрейде: админа один раз разлогинит (сменилось имя cookie), а от reverse-proxy требуется передача исходного `Host` (Apache-фрагмент из `deploy/` это делает). ## Рабочая петля (dev loop) — ВАЖНО -Локально (Windows, `D:\Local\Git\selfpost`) **нет Go и Docker** — только редактирование и git. Вся сборка/тесты идут на dev-сервере `selfpost.mixfed.ru` (Debian 12 bookworm, тот же, что базовый образ; провижён под разработку). Цикл: править локально → залить дерево на сервер → `go build`/`go vet`/`docker build`/тесты там. `rsync` в локальном git-bash нет, поэтому дерево едет tar'ом по ssh: `tar -czf - --exclude=.git . | ssh root@selfpost.mixfed.ru 'rm -rf /root/selfpost-src && mkdir -p /root/selfpost-src && tar -xzf - -C /root/selfpost-src'`; Go на сервере — в `/usr/local/go/bin` (не в `PATH` по умолчанию); образ — `docker build -f build/Dockerfile -t selfpost:dev --build-arg VERSION=dev .`. Источник истины и git-история — локальный репозиторий; сервер — только исполнитель сборки/тестов. Подключение: `ssh root@selfpost.mixfed.ru` (по ключу). +Сборка, unit-тесты и e2e требуют Go 1.26+ и Docker + Compose v2 — см. +[development.md](development.md). Конкретная связка машин (всё локально, +отдельный сервер, только CI) у каждого разработчика своя; источник истины — +git-репозиторий. diff --git a/docs/roadmap.md b/docs/roadmap.md index 6c346c5..7ebd2b5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,7 +2,8 @@ **Статус:** здесь собран объём, отнесённый к релизной линии **2.x.x** — вне базового объёма v1.0/v1.x (v1.x — только исходящий релей). Реализация — -только после явного согласования ([product.md](product.md), [development.md](development.md) § Agent rules): +только после явного согласования ([product.md](product.md), +[.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)): [product.md](product.md) явно исключает часть этого объёма (приём входящей почты; несколько пользователей/роли), поэтому включение — сознательное расширение границ проекта, а не доработка по своей инициативе. Присутствие @@ -128,7 +129,7 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок **Риски:** open relay/backscatter (снимается `relay_domains` + `relay_recipient_maps` + `reject_unauth_destination`); потеря origin IP для фильтрации на backend'е при пересылке (снимается milter-хуком антиспама + нативным DNSBL на входном хопе, где origin IP ещё виден); порт 25 на приём расширяет поверхность атаки (по умолчанию выключено). **Модель:** Opus (инфра/безопасность, риск open relay). **Внешняя зависимость деплоя:** опциональный antispam-контейнер — вне образа SelfPost, поднимается оператором при включении опции. -**Зависимости:** не является частью v1.0, зависит только от готового исходящего тракта (уже реализован) и требует отдельного согласования ([development.md](development.md)) до кодирования. +**Зависимости:** не является частью v1.0, зависит только от готового исходящего тракта (уже реализован) и требует отдельного согласования ([.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)) до кодирования. --- @@ -139,7 +140,7 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок **Почему 2.x, а не v1.x.** [product.md](product.md) относит «несколько пользователей панели, роли» к out of scope (один администратор), поэтому появление второго субъекта — расширение границ проекта, как и Фаза O1: сначала согласование -([development.md](development.md)), только потом код. Цена — уровня фазы, а не патча: таблица пользователей и их привязка к доменам, роль в сессии, авторизация в каждом хендлере (а не только на маршруте — сейчас `{id}`/`{aid}` не сверяются ни с чем, кроме существования), пересмотр первичного setup'а и смены пароля под нескольких пользователей, учёт нового субъекта в бэкапе и экспорте домена. +([.cursor/rules/agent-rules.mdc](../.cursor/rules/agent-rules.mdc)), только потом код. Цена — уровня фазы, а не патча: таблица пользователей и их привязка к доменам, роль в сессии, авторизация в каждом хендлере (а не только на маршруте — сейчас `{id}`/`{aid}` не сверяются ни с чем, кроме существования), пересмотр первичного setup'а и смены пароля под нескольких пользователей, учёт нового субъекта в бэкапе и экспорте домена. *(Прежняя формулировка этого пункта — «2FA и несколько администраторов» — заменена: 2FA снята с рассмотрения, а «несколько администраторов» уточнено до одной конкретной роли, потому что нужна не вторая копия всевластного админа, а ограниченный доступ владельца отдельного домена.)* @@ -147,9 +148,8 @@ git-тег `vX.Y.Z`; `implementation-plan.md` в `docs/archive/`, ссылок ## `CONTRIBUTING.md` — кандидат на 2.x -**Что это.** Точка входа для стороннего контрибьютора: dev loop (правка на -Windows → сборка и прогон на Debian-сервере, потому что Docker локально нет), -маршрутизация моделей по типу работы, протокол коммитов, требование +**Что это.** Точка входа для стороннего контрибьютора: dev loop, маршрутизация +моделей по типу работы, протокол коммитов, требование `gofmt`/`vet`/`test`/`make e2e` до PR. Сейчас всё это есть, но в [development.md](development.md) и [progress.md](progress.md) — то есть на русском и вперемешку с внутренним состоянием проекта. diff --git a/test/e2e/smtp_client.go b/test/e2e/smtp_client.go index 1fe805a..e89f867 100644 --- a/test/e2e/smtp_client.go +++ b/test/e2e/smtp_client.go @@ -9,8 +9,8 @@ import ( ) // smtpsAddr is where compose.override.yml publishes the primary implicit-TLS -// submission port (465 in the shipped compose, remapped here to stay off the -// dev server's real ports). +// submission port (465 in the shipped compose, remapped here to avoid clashing +// with a local deployment on the same host). const smtpsAddr = "127.0.0.1:20465" // sendAttempt is one SMTP transaction against the stand. An empty authLogin