# Contributing to comail Thanks for your interest in contributing. comail is a cooperative email reputation layer for the atproto ecosystem: a relay, a labeler, and a set of web frontends that let PDS operators pool sending reputation. This document describes how to file issues, propose changes, and get a PR through review. The project is alpha. Contributions are welcome, but please keep in mind that some interfaces are still settling and the bar for behavior changes that affect deliverability is high. ## Code of conduct This project adopts the [Contributor Covenant 2.1](CODE_OF_CONDUCT.md). Participating in issues, PRs, or any other project space implies you have read and will abide by it. ## Licensing and contribution implications comail is licensed under the **GNU Affero General Public License v3.0 or later** (see [LICENSE](LICENSE)). By submitting a contribution, you agree that: - Your contribution will be distributed under the AGPL-3.0-or-later license, the same terms as the rest of the project. - You have the right to submit the work (it is your own, or you have the necessary permission from the rights holders). - You understand that AGPL is a strong copyleft license: anyone who runs a modified version of this software over a network must offer the corresponding source to its users. ### DCO, not CLA The project uses the [Developer Certificate of Origin (DCO)](https://developercertificate.org/) rather than a contributor license agreement. Sign off each commit with the `-s` flag: ``` git commit -s -m "feat(labeler): add SPF strict-pass check" ``` This appends a `Signed-off-by: Your Name ` trailer, which is your assertion of the DCO terms. No paperwork, no email exchange, no rights assignment — just a per-commit attestation. If a commit lands without a sign-off, a maintainer will ask you to amend or will rebase the trailer in before merge. ## Filing issues Issues live on the upstream Gitea instance. For source-code questions on the public mirror, the `tangled.org` issue tracker is also fine. Good issues include: - What you expected to happen - What actually happened (logs, error text, screenshots if UI) - Repro steps - Software version (commit SHA or `VERSION` file) - Whether the issue affects the relay, the labeler, a web frontend, or the infrastructure Security-relevant issues should go through [SECURITY.md](SECURITY.md) instead of public issue tracking. ## Branching and PR workflow 1. Open or comment on an issue first if the change is non-trivial. This avoids parallel work and lets us flag blockers early. 2. Branch off `main`. Branch naming follows `feat/`, `fix/`, `chore/`, `docs/`. 3. Make changes in small, reviewable commits. Each commit should build and pass tests on its own where practical. 4. Push the branch and open a PR against `main`. The PR description should state the problem, the approach, and any non-obvious tradeoffs. 5. CI will run automatically. Fix anything red before requesting review. 6. A maintainer will review. Expect questions on edge cases, deliverability impact, and test coverage. 7. After approval and a green CI, the PR is merged (usually squash, occasionally merge-commit for multi-author work). Direct pushes to `main` are not allowed. ## Commit message conventions Use [Conventional Commits](https://www.conventionalcommits.org/): ``` (): ``` Common types: - `feat` — new user-visible feature - `fix` — bug fix - `docs` — documentation only - `refactor` — code restructuring with no behavior change - `test` — tests only - `chore` — tooling, dependencies, CI config Common scopes: `relay`, `labeler`, `frontend`, `web`, `dns`, `infra`, `docs`, `ci`. **Do not include AI co-author trailers.** Lines like `Co-Authored-By: Claude ...` or anything from `anthropic.com` are not appropriate and will be rejected at review or stripped at merge. If you used an AI assistant to draft code, the contribution is still yours and you sign off as yourself. ## Code style ### Go - Standard `gofmt` and `goimports` formatting. `go vet ./...` must be clean. - Errors are values: wrap with `fmt.Errorf("doing X: %w", err)`, do not swallow. - No `panic` in production paths — return errors and let the caller decide. `panic` is acceptable in `main` startup wiring and in tests. - Tests use the standard library `testing` package. Table-driven tests are preferred for multi-case logic. - Database access goes through `internal/store`. Do not open SQLite connections directly from callers. ### Templates and frontend - Go templates live under `web/`. The `templ-generate-check` and `template-escape-lint` CI jobs gate any template edit; run them locally before pushing. - The web frontends under `frontend/` and `web-frontend/` use the framework set up in each subdirectory. Match the existing style — do not introduce a new toolchain in a casual PR. - Tailwind classes follow the existing patterns. The `check-tailwind-build` job catches accidental regressions. ### Shell and infra - Shell scripts: `set -euo pipefail`, quote variables, run through `shellcheck`. - Nix and Terraform: match existing patterns under `infra/`. Format with `nixfmt` / `terraform fmt`. ## Running tests The full suite is what CI runs. Locally: ```bash # Go unit tests go test ./... # Go vulnerability check govulncheck ./... # SML rule tests (Osprey worker logic) make sml-test # Template escape lint make template-lint ``` Integration tests that need Docker or external services are gated by build tags or environment variables. See individual `_test.go` files for the conventions. ### Migration dry-run gate (operator note) CI runs a pre-flight migration dry-run job (`migration-dryrun` in `.gitea/workflows/relay-deploy.yml`) that executes every registered structural + data migration against either a sanitized prod-shape SQLite fixture or a synthetic representative dataset, with `ATMOS_DATA_MIGRATION_DRY_RUN=true` so the fixture is not modified. The job blocks `deploy-relay` — a broken migration cannot reach prod. By default the test (`TestMigrationsDryRunAgainstFixture` in `internal/relaystore/migrations_dryrun_test.go`) falls back to a tiny synthetic seed (3 members + 5 domains + 10 messages) built through the public `Store` API. That catches schema / runner regressions but not data-shape regressions on pre-Phase-1 rows whose column shape no longer exists in the current schema. To enable full prod-shape coverage, an operator may commit a sanitized snapshot to `internal/relaystore/testdata/prod-snapshot.sqlite`. Requirements: - PII scrubbed: `contact_email`, `forward_to`, `pending_email`, encrypted DKIM blobs replaced with synthetic placeholders, and any `oauth_*` tables stripped or rekeyed. - Structurally representative: keeps the row distributions that matter for migration paths (e.g. legacy `member_domains` rows that predate the account-level `members.contact_email` column). - Read-only: the test copies it to a temp working file before the dry-run touches it. Never modified in place. The snapshot is treated as a binary fixture (no diff review). Refresh it any time the prod schema or shape drifts meaningfully — quarterly is usually fine. ## Reviewing PRs If you'd like to help with review, the things we look for: - **Spec fidelity**: does the change match what the issue asked for? - **Test coverage**: are the edge cases, error paths, and failure modes covered? - **Deliverability impact**: changes to the relay, signing, or DNS verification can affect every member's deliverability. These need extra scrutiny and usually a staging deploy before merge. - **Security surface**: input validation, authentication, secrets handling. - **Backwards compatibility**: lexicons, label schemas, and the enrollment flow have external consumers. Breaking changes need a migration plan. ## Getting unstuck If you're blocked, comment on the issue or PR. Maintainers usually respond within a few business days. The project has one primary maintainer at the moment, so urgency varies — feel free to ping if a PR has been quiet for more than a week. Thanks again for contributing.