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. 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). 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)
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 <you@example.com> 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
VERSIONfile) - Whether the issue affects the relay, the labeler, a web frontend, or the infrastructure
Security-relevant issues should go through SECURITY.md instead of public issue tracking.
Branching and PR workflow #
- Open or comment on an issue first if the change is non-trivial. This avoids parallel work and lets us flag blockers early.
- Branch off
main. Branch naming followsfeat/<slug>,fix/<slug>,chore/<slug>,docs/<slug>. - Make changes in small, reviewable commits. Each commit should build and pass tests on its own where practical.
- Push the branch and open a PR against
main. The PR description should state the problem, the approach, and any non-obvious tradeoffs. - CI will run automatically. Fix anything red before requesting review.
- A maintainer will review. Expect questions on edge cases, deliverability impact, and test coverage.
- 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:
<type>(<scope>): <short summary>
<optional body explaining what and why>
<optional footer with trailers, e.g. Signed-off-by, Fixes #123>
Common types:
feat— new user-visible featurefix— bug fixdocs— documentation onlyrefactor— code restructuring with no behavior changetest— tests onlychore— 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
gofmtandgoimportsformatting.go vet ./...must be clean. - Errors are values: wrap with
fmt.Errorf("doing X: %w", err), do not swallow. - No
panicin production paths — return errors and let the caller decide.panicis acceptable inmainstartup wiring and in tests. - Tests use the standard library
testingpackage. 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/. Thetempl-generate-checkandtemplate-escape-lintCI jobs gate any template edit; run them locally before pushing. - The web frontends under
frontend/andweb-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-buildjob catches accidental regressions.
Shell and infra #
- Shell scripts:
set -euo pipefail, quote variables, run throughshellcheck. - Nix and Terraform: match existing patterns under
infra/. Format withnixfmt/terraform fmt.
Running tests #
The full suite is what CI runs. Locally:
# 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 anyoauth_*tables stripped or rekeyed. - Structurally representative: keeps the row distributions that
matter for migration paths (e.g. legacy
member_domainsrows that predate the account-levelmembers.contact_emailcolumn). - 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.