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/postgresonlocalhost:5432; override withPOSTGRES_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:
- Clone the repo.
- Install dependencies:
mix deps.get. - Create the test databases:
mix ci.setup(createsMailglass.TestRepoand the inbound test DB). - 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
- Create a branch.
- Implement your changes and add tests.
- Inner loop (fast, seconds, no DB):
mix ci.fast— runsmix format --check-formatted,compile --warnings-as-errors(with and without optional deps), andmix credo --strict. Run this often. - 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. - (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. - 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, andmake ci-browserare thin wrappers around the Mix aliases above.
Commit Guidelines
Use Conventional Commits:
feat: ...for new featuresfix: ...for bug fixesdocs: ...for documentation changeschore: ...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 inmailglass_inbound/README.mdto 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_pinfield in.planning/publish/mailglass_inbound-publish-summary.jsonto 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:
- Generate a fine-grained PAT scoped to
szTheory/mailglasswith Administration: Read and write permission. (Settings → Developer settings → Personal access tokens → Fine-grained tokens.) - Add it as a repo secret named
BRANCH_PROTECTION_PAT. (Repo settings → Secrets and variables → Actions → New repository secret.) - Run
Branch Protection Driftonce 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.