Contributing to Mailglass

Copy Markdown View Source

We welcome contributions! Mailglass is developed using a phase-based roadmap found in .planning/PROJECT.md.

Local Setup

Prerequisites:

  • Elixir ~> 1.18 / OTP 27+ (the supported floor).
  • PostgreSQL running locally (the test suite and every contract lane are DB-backed). Defaults expect postgres/postgres on localhost:5432; override with POSTGRES_HOST / POSTGRES_USER / POSTGRES_PASSWORD.
  • Node 22+ — only if you run the optional admin browser gate (mix ci.browser). It is NOT needed for a normal contribution: mailglass's zero-Node guarantee is for adopters, and the required checks need no Node.

Steps:

  1. Clone the repo.
  2. Install dependencies: mix deps.get.
  3. Create the test databases: mix ci.setup (creates Mailglass.TestRepo and the inbound test DB).
  4. Run the tests: mix test.

To click around the admin UI against seeded data — the fastest way to iterate on mailglass_admin — run the demo with Docker: make demo (see guides/run-the-demo.md).

Development Workflow

  1. Create a branch.
  2. Implement your changes and add tests.
  3. Inner loop (fast, seconds, no DB): mix ci.fast — runs mix format --check-formatted, compile --warnings-as-errors (with and without optional deps), and mix credo --strict. Run this often.
  4. Before you push — full local↔CI parity: mix ci — run from the repo root. Mirrors every required merge gate plus the standard hygiene lanes across all three sibling packages: the core and admin support contracts, the full core test suite, inbound tests, mix dialyzer, mix docs --warnings-as-errors, mix hex.audit, the reference-host trust lane, and the installer host smoke (which generates a throwaway Phoenix app, so this step needs network access and takes a few minutes — it runs last, after everything cheap has already passed). Requires Postgres; the installer step also requires network access.
  5. (Optional) Admin browser gate: mix ci.browser — runs the Playwright operator-UI checks. Needs Node 22+ and downloads a Chromium build. This lane is advisory (it does not block merge), so skip it unless you touched the admin UI.
  6. Open a PR.

If a step in mix ci fails, it names the failing check and stops there (fail-fast). Fix it and re-run — mix ci is safe to run repeatedly.

Prefer make? make ci, make ci-fast, and make ci-browser are thin wrappers around the Mix aliases above.

Commit Guidelines

Use Conventional Commits:

  • feat: ... for new features
  • fix: ... for bug fixes
  • docs: ... for documentation changes
  • chore: ... for maintenance

PR Expectations

  • All CI checks must pass.
  • New features must include documentation and tests.
  • Maintain atomic commits.

How release-please syncs README install pins (and what it no longer does)

.github/workflows/release-please.yml runs a sed step on the release-please--branches--main PR branch after the action runs. It syncs:

  • The {:mailglass_inbound, "~> X.Y"} install hint in mailglass_inbound/README.md to the new inbound major.minor.
  • The {:mailglass, "~> X.Y"} and {:mailglass_admin, "~> X.Y"} install hints in the three READMEs to the new core major.minor.
  • The mailglass_inbound_publish_pin field in .planning/publish/mailglass_inbound-publish-summary.json to the new ~> constraint.

What the sed step does NOT touch: the sibling mix.exs core-dep declarations. As of v1.15 Phase 125 the sibling packages use hand-maintained pessimistic ~> constraints (mailglass_inbound uses ~> 1.10 and >= 1.10.2, mailglass_admin uses ~> 1.10). A core patch release requires no sibling change at all. A core minor (e.g. 1.11.0) requires a deliberate fix(inbound): commit in mailglass_inbound/mix.exs updating the floor — asserting "verified against core 1.11" — before or alongside the release PR.

Recursion-safety guarantee: the sync push uses GITHUB_TOKEN, which by GitHub's anti-recursion guarantee does NOT trigger further workflow runs.

If a release publishes but the tags/publish never fire

release-please.yml runs only on: push: main. When the release PR (chore: release main) is merged by GitHub-native auto-merge, the resulting push is authored by GITHUB_TOKEN, and GitHub's anti-recursion guarantee suppresses the push event — so release-please does not re-run to tag the release, and the release: published fan-out in publish-hex.yml never starts. Symptom: the manifest on main is at the new version and the release PR is merged with label autorelease: pending, but no mailglass-vX.Y.Z GitHub release exists and Hex still shows the prior version.

Recovery: land any subsequent commit on main via a non-GITHUB_TOKEN identity (e.g. a maintainer merging a small PR with gh pr merge rather than arming auto-merge). That push wakes release-please; its preflight sees the pending, untagged release PR, creates the vX.Y.Z releases (via RELEASE_PLEASE_PAT), and the release: published events drive publish-hex. The publish jobs are idempotent (mix hex.info guards), so a re-trigger is always safe. Manually creating the releases with gh release create <tag> is an equivalent fallback — release: published is the canonical publish trigger.

One-time setup: branch protection automation

main is protected with required status checks (Tests, Credo Strict, Dialyzer, actionlint, PR title (semantic)). This protection is configured idempotently by scripts/setup_branch_protection.sh and re-asserted daily by .github/workflows/branch-protection-drift.yml.

To enable the drift-detection workflow, add a repo secret BRANCH_PROTECTION_PAT:

  1. Generate a fine-grained PAT scoped to szTheory/mailglass with Administration: Read and write permission. (Settings → Developer settings → Personal access tokens → Fine-grained tokens.)
  2. Add it as a repo secret named BRANCH_PROTECTION_PAT. (Repo settings → Secrets and variables → Actions → New repository secret.)
  3. Run Branch Protection Drift once via the Actions tab to confirm.

Without the secret, the drift workflow no-ops and posts a notice in its workflow summary. Without it, you can still call the script directly:

GH_TOKEN=<admin-PAT> scripts/setup_branch_protection.sh main

Verifying the Tests gate blocks failing PRs

scripts/check_tests_gate.sh runs in CI's actionlint job and fails if continue-on-error: true is reintroduced on the Tests job. Static guard.

For an end-to-end check, run the Gate Self-Test workflow via the Actions tab. It creates a temporary branch with a synthetic assert false, opens a draft PR, polls until the Tests check finishes, asserts FAILED, then closes the PR and deletes the branch. ~5 minutes round-trip.