Skip to main content

Contributing

Thanks for helping with Converger. This page covers the workflow from a fresh clone to a merged pull request: local setup, branch and commit conventions, the checks CI runs (and how to run them first), Windows specifics, and the rule that every change ships with its documentation.

Code of conduct, issues and license​

  • Everyone taking part follows the code of conduct (Contributor Covenant 2.1). Report violations to the address given there.
  • Open issues with the forms in .github/ISSUE_TEMPLATE/: Bug report, Feature request or Channel adapter request (collected in #44). Blank issues are disabled.
  • Security problems never go in a public issue. Follow SECURITY.md (private advisory or email); see also Reporting a vulnerability.
  • Converger is licensed under the MIT License. By opening a pull request you agree that your contribution is licensed under the same terms (inbound = outbound; there is no CLA). Code copied from elsewhere must have a compatible license, and its notice is kept next to it (as for the vendored mekik fixtures in test/protocol/fixtures/mekik/). See ADR-0073.

Development setup​

The full walkthrough is in Getting started. In short:

  • Install the toolchain from .tool-versions (Elixir 1.19.5-otp-28, Erlang 28.5.0.5), for example with asdf or mise, and a PostgreSQL server (CI and compose use 17).
  • mix setup runs deps.get and ecto.setup (create, migrate, seed) and builds the admin/portal assets (assets.setup, assets.build: standalone tailwind and esbuild, no Node.js). The seed creates the first super_admin from ADMIN_EMAIL / ADMIN_PASSWORD, or prints a one-time password.
  • mix phx.server starts the app on http://localhost:4000 (admin panel at /admin); its watchers rebuild the CSS and JavaScript on change. UI conventions: Admin panel and tenant portal.
  • Database settings come from DB_USERNAME, DB_PASSWORD, DB_HOSTNAME, DB_NAME (defaults postgres / postgres / localhost / converger_dev); see the Configuration reference.
  • Optional: cp .env.example .env, fill it in, and docker compose up -d for Prometheus, Grafana, Jaeger and the OpenTelemetry collector (Observability).
  • No C compiler (Windows without the MSVC build tools): set CONVERGER_PASSWORD_HASHER=pbkdf2, see Line endings and Windows.

Dev container​

.devcontainer/ describes a ready-made environment for VS Code Dev Containers, GitHub Codespaces or the devcontainer CLI: the pinned Elixir/OTP image of the Dockerfile with build-essential, cmake (Kafka's crc32cer NIF) and inotify-tools, plus a Postgres 17 service. It works the same on Windows, macOS and Linux and needs only Docker.

  • VS Code: install the Dev Containers extension and run Dev Containers: Reopen in Container. The first start builds the image and runs mix deps.get && mix compile (several minutes).
  • CLI: devcontainer up --workspace-folder ., then devcontainer exec --workspace-folder . mix test.

Inside the container DB_HOSTNAME is db (user and password postgres), so mix setup, mix phx.server and mix test work without further settings. Port 4000 is forwarded to the host. deps/ and _build are kept in Docker volumes (MIX_DEPS_PATH, MIX_BUILD_ROOT), not in the checkout, so they never clash with a native build on the host; docker compose -p converger-devcontainer down -v removes them together with the database. The dev container is independent of the root docker-compose.yml, which runs the release image.

Branches, commits and pull requests​

  • Branch from main, one topic per branch, named <type>/<issue>-<short-description> (for example fix/13-rate-limiting, chore/dependabot-updates).
  • Commit messages and PR titles follow Conventional Commits: type(optional scope): summary, imperative mood, lower case. Types used so far: feat, fix, test, ci, build, chore; use docs for documentation-only changes (for example feat(rate-limit): Hammer 7, cluster-wide limits on hot paths, login lockout, chore(deps): apply pending Dependabot updates). Reference the issue in the title or body ((#13), Closes #56).
  • Keep commits focused; do not mix formatting-only changes into functional commits.
  • PR descriptions explain the problem, the changes, the checks you ran, and, when operators must do something, a Deployment notes or Breaking changes section (new required variables, migrations that need a maintenance window, behavior changes for integrators). Those sections feed Upgrades.
  • Dependency updates arrive from Dependabot (mix, npm for the pnpm workspace and website, GitHub Actions, Docker; weekly).

Changelog and releases​

CHANGELOG.md is updated only when a release is cut, not in feature or fix pull requests: parallel pull requests kept conflicting on it. Describe user-facing changes, Breaking changes and Deployment notes in the pull request description instead; the release notes are compiled from those descriptions.

Cutting a release (maintainers):

  1. List what was merged since the last tag (git describe --tags --abbrev=0; with no tag yet, everything): the merge commits on main and the merged pull requests' descriptions.
  2. Draft the notes in Keep a Changelog groups (Added, Changed, Fixed, Deprecated, Removed, Security), plus Breaking changes and Deployment notes, each line linking its pull request, and pick the SemVer version.
  3. The maintainer reviews and approves the draft and the version.
  4. A chore(release): vX.Y.Z pull request adds the ## [X.Y.Z] - YYYY-MM-DD section to CHANGELOG.md and bumps version in mix.exs. After it is merged, tag the merge commit vX.Y.Z and publish the GitHub release with the same notes.

Checks​

Required check on main​

main requires the CI job chativa reference client compatibility (branch protection, #65): chativa's unmodified @chativa/connector-mekik and parseChatFrame run against a real server (connecting chat clients), so a protocol change that breaks the reference client cannot be merged. The other jobs are not required (the hex.audit step carries a known advisory without a fixed release), but keep them green.

Run mix precommit before pushing​

The precommit alias in mix.exs mirrors the CI lint and test jobs. It runs in the test environment (so it needs Postgres) and fixes formatting and unused lock entries instead of only reporting them:

mix precommit
StepWhat it does
compile --warnings-as-errors --forceFull recompile; any warning fails
deps.unlock --unusedRemoves unused entries from mix.lock
formatFormats the code (.formatter.exs)
credo --strictStatic analysis (.credo.exs)
sobelow --configSecurity analysis (.sobelow-conf)
hex.auditRetired Hex packages
deps.auditDependencies with known vulnerabilities (mix_audit)
test --warnings-as-errorsTest suite (creates and migrates the test database first); warnings in test files fail too

Dialyzer is slow and runs separately:

mix dialyzer

The first run builds the PLTs in priv/plts. Warnings that are accepted go in .dialyzer_ignore.exs with a reason. Accepted Sobelow findings get an inline # sobelow_skip [...] with a justification comment; acknowledged Hex advisories go in mix.exs under hex: [ignore_advisories: ...], also with a reason.

To debug failures: mix test test/path/to/file_test.exs, mix test --failed.

test/acceptance/ holds end-to-end tests of an issue's acceptance criteria, driven only through the public edges (signed inbound webhooks, native sockets via ConvergerWeb.ProtocolClient, provider APIs stubbed with Req.Test, mail captured by Swoosh.Adapters.Test). Deliveries there run in socket, connection and pipeline processes that are not the test's callers, so these tests are async: false and use Req.Test.set_req_test_to_shared/1 and config :swoosh, :shared_test_process.

What CI runs​

.github/workflows/ci.yml runs on every pull request and on pushes to main, with Elixir and OTP from the workflow env (kept in sync with .tool-versions and the Dockerfile):

JobSteps
Format, compile, Credo, Sobelow, audit (lint)mix format --check-formatted, mix deps.unlock --check-unused, mix compile --warnings-as-errors --force, mix credo --strict, mix sobelow --config, mix hex.audit, mix deps.audit
Dialyzermix dialyzer --format github in MIX_ENV=dev, PLTs cached per Elixir/OTP/lockfile
Test suite (coverage)Postgres 17 service; mix coveralls.multiple --type local --type json --type lcov --warnings-as-errors; coverage summary on the run page, cover/excoveralls.json and cover/lcov.info uploaded as an artifact, Codecov upload when CODECOV_TOKEN is configured
Storage integration (MinIO, Azurite)MinIO and Azurite services; mix test test/converger/uploads/storage_integration_test.exs --only minio --only azurite with MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, AZURITE_ENDPOINT

.github/workflows/docker.yml builds the image on every pull request (no push) and scans it with Trivy (report-only); v* tags publish it to GHCR. The documentation workflow is described below.

The reasoning behind these gates is in ADR-0021.

To run the storage integration tests locally, start the same services and export the variables:

docker run -d --name minio -p 9000:9000 -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin bitnamilegacy/minio:latest
docker run -d --name azurite -p 10000:10000 mcr.microsoft.com/azure-storage/azurite
MINIO_ENDPOINT=http://localhost:9000 MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin \
AZURITE_ENDPOINT=http://127.0.0.1:10000/devstoreaccount1 \
mix test test/converger/uploads/storage_integration_test.exs --only minio --only azurite

Dependency security alerts​

Dependabot (configured in .github/dependabot.yml) opens security alerts for the Mix deps, the pnpm workspace (pnpm-lock.yaml) and the documentation site (website/package-lock.json). Prefer upgrading the direct dependency. When the vulnerable package is transitive and no release of the direct dependency fixes it yet (Docusaurus pins several), raise its floor with an override and regenerate the lockfile:

LockfileWhere the override goesCurrent overrides
pnpm-lock.yamloverrides: in pnpm-workspace.yaml, then pnpm installesbuild >=0.28.1
website/package-lock.json"overrides" in website/package.json, then npm installpostcss-selector-parser ^7.1.6, serialize-javascript ^7.0.5, tinypool ^2.1.2, uuid ^11.1.1 (under sockjs only)

Overrides can cross a major version, so check the result before merging: cd website && npm run build (also with DOCUSAURUS_SSG_WORKER_THREAD_COUNT=2, which exercises tinypool) and pnpm -r test. Remove an override once the direct dependency ships the fixed version. An alert with no patched release (for example braces <=3.0.3, which only reaches the docs build tooling) is dismissed in GitHub with the reason stated.

Asynchronous assertions​

test/test_helper.exs raises ExUnit's assert_receive_timeout to 1 s, so assert_receive, assert_reply and assert_push tolerate a busy CI runner; they still return as soon as the message arrives. Don't pass short explicit timeouts to them, and use refute_receive (100 ms default) for negative checks. Tests that need wall-clock windows (rate limits, lockouts) must not depend on landing inside one; see start_in_fresh_window/2 in test/converger_web/controllers/rate_limiting_test.exs.

Line endings and Windows​

.gitattributes sets * text=auto eol=lf: files are stored and checked out with LF on every platform (only *.bat and *.cmd keep CRLF), so mix format --check-formatted and diffs behave the same on Windows as on Linux CI, even with core.autocrlf=true. Release scripts (rel/overlays/bin/*, *.sh) must stay LF because they run in Linux containers. If your clone predates the rule, run git add --renormalize . once.

Notes for developing on Windows:

  • bcrypt_elixir compiles a C NIF and needs the Microsoft C++ Build Tools. Without them, set $env:CONVERGER_PASSWORD_HASHER = "pbkdf2": mix.exs leaves bcrypt_elixir out and passwords are hashed with pure-Elixir PBKDF2 (ADR-0057). The variable is read at compile time, so switching modes recompiles; give the pbkdf2 mode its own build root to avoid that ($env:MIX_BUILD_ROOT = "_build_pbkdf2", ignored by git). Changes that touch password hashing must pass in both modes; CI runs the default (bcrypt). Or use the dev container. With the build tools, run mix from an "x64 Native Tools Command Prompt", or call vcvars64.bat first, for example in a small wrapper script:

    cmd /c '"C:\Program Files\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvars64.bat" >nul && mix deps.compile bcrypt_elixir'
  • Environment variables in PowerShell: $env:DB_PASSWORD = "postgres"; mix test.

  • To run several test suites at once (for example in two worktrees), give each a partition so they use separate databases and upload directories: $env:MIX_TEST_PARTITION = "2"; mix test. No extra port is opened in test (metrics are served on the endpoint, which does not listen in test), so suites do not compete for ports.

  • The two-node cluster suite (test/cluster, see Clustering) is excluded by default; run it with mix test --only cluster. It boots two extra VMs with :peer and uses its own database (<test database>_cluster), so it also works on Windows and next to other partitions.

  • The release overlays are POSIX shell scripts; test release behavior with the Docker image rather than natively.

Documentation with every change​

Documentation lives in docs/ and is published as this site. It is part of the change, not a follow-up:

  • Every PR must update the relevant docs/ pages. New or changed endpoints, configuration keys, environment variables, defaults, error codes, migrations, operational procedures and security behavior are documented in the same PR that introduces them. If a migration needs a maintenance window, add it to Migrations and maintenance windows; if operators must act, add a note to Upgrades.
  • Architectural decisions require a new or updated ADR in docs/adr/: new dependencies or infrastructure, changes to the delivery pipeline, data model, security model, protocol or public API contracts, and any decision a future contributor would otherwise have to rediscover. Follow How we write ADRs and start from the template. The policy itself is ADR-0025.
  • The pull request template (.github/pull_request_template.md) has a documentation checklist: the docs/ pages you updated, the ADR you added or updated (or "no architectural decision involved"), or "no documentation impact" with the reason, and whether npm run build passes. Tick only what is true; reviewers check it.

The "Docs required" check​

The Docs required job in .github/workflows/docs.yml fails a pull request that changes files under lib/, config/ or priv/repo/migrations/ without changing anything under docs/.

Some changes genuinely need no documentation (an internal refactor with no observable change, a log message tweak, a config/ change that only affects the test environment). The escape hatch:

  1. A maintainer adds the label no-docs to the PR.
  2. The PR description states why no documentation change is needed.
  3. Adding or removing the label re-runs the Docs required job automatically (the workflow listens to the labeled and unlabeled events), and it passes with a notice.

Do not use no-docs to postpone documentation that is needed; open the docs change in the same PR instead.

Writing docs​

Pages are Docusaurus 3 documents in plain Markdown (CommonMark, not MDX): no JSX or raw HTML, placeholders such as <token> inside backticks or code blocks. Each file starts with front matter (title, description, sidebar_position); folders get a _category_.json. Link to other pages with relative paths including .md, to code with absolute GitHub URLs, and mark unimplemented features as "Planned" with the issue link.

Preview the site locally​

The site lives in website/ and reads the repository's docs/ folder. It needs Node.js 20 or later:

cd website
npm ci
npm start # dev server with live reload, usually http://localhost:3000
npm run build # production build into website/build; run it before pushing docs changes

The production build fails on broken links and broken Markdown links, so a renamed page or a wrong relative path is caught in the "Build site" job of the PR.

npm start and the Grafana container from docker compose both default to port 3000; stop one or run npm start -- --port 3001.