Contributing
Setup
Running tests
Differential tests against Alertmanager
amlint simulates Alertmanager in Python, so its routing, inhibition and the config errors
it shares with amtool check-config are tested against the real thing. One command:
It downloads an Alertmanager release (0.34.1 by default) into .cache/alertmanager/,
checks the tarball's sha256 against the release's sha256sums.txt, and runs
tests/differential/ with that release's amtool and alertmanager binaries:
test_routing.py: generated configs and label sets, amlint's receivers againstamtool config routes testtest_validation.py:amtool check-configrejects a config exactly when amlint reports an error-level finding of a code amtool also catches (ALSO_CAUGHT_BY_AMTOOL)test_inhibition.py: alerts POSTed to a runningalertmanager; each one'sstatus.inhibitedByand receivers against amlint's simulation
AM_VERSION=0.32.2 scripts/differential.sh # another release
AMLINT_DIFF_SEED=7 AMLINT_DIFF_EXAMPLES=2000 scripts/differential.sh -x # explore more
Cases come from Hypothesis with a fixed seed (AMLINT_DIFF_SEED, default 0), so a run is
reproducible. CI prints the seed in the pytest header. On a mismatch the test reduces the
case to a minimal config and label set and prints both results, plus the amtool command
that reproduces it. Plain pytest skips these tests when amtool isn't on PATH.
CI runs them in .github/workflows/differential.yml against the last two minor releases
and the newest, on pull requests that touch the routing engine, and weekly with a fresh
seed. A mismatch is either a bug in amlint, which you fix with a regression test in
test_linter.py, or a deliberate difference or an Alertmanager bug, which you document in
the fidelity page and teach the test to recognize, narrowly.
Linting
The playground
The playground runs amlint in the browser with
Pyodide. The page's files are in docs/playground/: app.js (the UI),
worker.js (loads Pyodide and amlint off the main thread) and amlint_playground.py, which
calls amlint.cli.main([... "--format", "json"]) for check, route and diff. New checks show
up there without any change; a change to the JSON output of those commands may need one.
The template is overrides/playground.html.
The amlint wheel, the CodeMirror bundle and the examples are generated into
docs/playground/dist/ (not committed). To work on the page locally (needs npm):
pip install mkdocs-material
python playground/build.py # again after changing amlint/ or playground/
mkdocs serve # http://127.0.0.1:8000/amlint/playground/
The docs workflow runs a headless smoke test of the built site. To run it yourself:
mkdocs build
python -m http.server 8000 --directory site &
cd playground && npx playwright install chromium && node smoke-test.mjs http://127.0.0.1:8000/playground/
Examples are listed in playground/build.py; most come from testdata/real-world/.
How to add a new check
Every check is a standalone function in amlint/linter.py.
Adding one takes about 10 lines of code and a test.
1. Write the function:
def check_your_rule(cfg: dict) -> List[Finding]:
out: List[Finding] = []
route = cfg.get("route")
if not route:
return out
for node, path in _walk_routes(route):
if something_is_wrong(node):
out.append(Finding(
WARN, # ERROR / WARN / INFO
"your-rule-code", # unique kebab-case identifier
"Clear message explaining what's wrong and why it matters.",
path,
))
return out
2. Register it in ALL_CHECKS:
3. Add a test:
def test_your_rule():
cfg = {
"route": {"receiver": "a"},
"receivers": [{"name": "a"}],
# config that triggers the rule
}
assert "your-rule-code" in codes(cfg)
4. Add docs:
Add a section to the relevant page under docs/checks/.
Severity guide
| Level | Use when |
|---|---|
ERROR |
Alertmanager will drop alerts or refuse to start |
WARN |
Almost certainly not what the author intended |
INFO |
Suspicious, worth reviewing, but may be intentional |
Opening a PR
- One check per PR is easiest to review
- Tests must pass:
pytest test_linter.py -v - No ruff errors:
ruff check amlint/ - Update the checks table in
docs/checks/index.md - Update
CHANGELOG.md