This document covers development setup, testing, and guidelines for contributors.
Ensure you have the correct tool versions installed (see .tool-versions):
- Erlang: 27.2.1
- Gleam: 1.14.0
- just: 1.38.0
This project uses just as a task runner. Run just to see all available commands.
just deps # Download dependencies
just build # Build for Erlang target (alias: b)
just build-js # Build for JavaScript target
just build-all # Build for both targets
just test # Run tests on both targets (alias: t)
just test-erlang # Run tests on Erlang only
just test-js # Run tests on JavaScript only
just format # Format source code (alias: f)
just format-check # Check formatting without modifying
just check # Run format-check + tests on both targets (alias: c)
just check-quick # Run format-check + Erlang tests only
just docs # Generate documentation (alias: d)
just watch # Watch and rebuild on changes (requires watchexec)
just watch-test # Watch and run tests on changesNote
You can also use gleam commands directly (e.g., gleam build, gleam test --target javascript).
This project has code coverage for both Erlang and JavaScript targets.
just coverage # Run coverage on both Erlang and JavaScript
just coverage-js # JavaScript only (uses c8/V8 native coverage)
just coverage-erlang # Erlang only (uses Erlang's cover tool)
just coverage-js-report # Generate HTML report (after coverage-js)JavaScript (c8): Uses V8's native code coverage via c8. Runs gleeunit tests and integration tests, outputs to coverage/lcov.info.
Erlang (cover): Uses a custom escript (scripts/gleam_cover.escript) that:
- Compiles BEAM files with Erlang's cover tool
- Runs tests via EUnit (not gleeunit's
main()which callshalt()) - Collects per-module line coverage
- Optionally exports to LCOV format (
--lcovflag)
Note
Erlang coverage line numbers refer to the generated Erlang code in build/dev/erlang/birch/_gleam_artefacts/*.erl, not the original Gleam source files.
Hosted coverage services like Codecov and Coveralls don't work well with Gleam because:
- Path mismatch: Coverage reports contain paths to compiled artifacts (
build/dev/javascript/*.mjs,build/dev/erlang/*.erl), not Gleam source files - Files not in repo: The compiled files don't exist in the repository
- Line number mismatch: Line numbers in compiled Erlang/JS code don't correspond to Gleam source lines
This is a fundamental limitation of transpiled languages. Elixir works better with these tools because it has tighter BEAM runtime integration, while Gleam compiles via intermediate Erlang source.
Use local coverage instead - just coverage provides accurate module-level percentages.
The scripts/gleam_cover.escript is generic and can be copied to any Gleam project:
- Copy
scripts/gleam_cover.escriptto your project - Add to your justfile:
coverage-erlang: build escript scripts/gleam_cover.escript coverage-erlang-lcov: build escript scripts/gleam_cover.escript --lcov
- The script auto-detects the project name from
gleam.tomland finds all*_testmodules
src/
├── birch.gleam # Main public API
├── birch/
│ ├── level.gleam # LogLevel type
│ ├── record.gleam # LogRecord type and Metadata
│ ├── logger.gleam # Logger type with handlers and context
│ ├── handler.gleam # Handler interface
│ ├── formatter.gleam # Format functions
│ ├── config.gleam # Global configuration
│ ├── sampling.gleam # Sampling and rate limiting
│ ├── scope.gleam # Scoped context
│ ├── handler/
│ │ ├── console.gleam # Console output with colors
│ │ ├── json.gleam # JSON-formatted output
│ │ ├── file.gleam # File output with rotation
│ │ └── async.gleam # Async (non-blocking) handler
│ └── internal/ # Internal modules (not public API)
├── birch_ffi.erl # Erlang FFI implementation
└── birch_ffi.mjs # JavaScript FFI implementation
test/
├── birch_test.gleam # Unit tests
└── property_test.gleam # Property-based tests (qcheck)
When modifying platform-specific code:
- Update both
birch_ffi.erlANDbirch_ffi.mjs - Ensure behavior is consistent across platforms
- Test on both Erlang and JavaScript targets
- Create a new file in
src/birch/handler/ - Implement formatting using
formatter.Formattertype - Use
handler.new()to create the handler - Add tests in
test/birch_test.gleam - Document with module-level and function doc comments
- Follow Gleam's built-in formatter (
just format) - Use doc comments (
///) for all public functions and types - Use
Errinstead ofErrorfor the error log level (avoids Result conflict)
This project uses conventional commits.
| Type | Description | In Changelog? |
|---|---|---|
feat |
A new feature | Yes (Features) |
fix |
A bug fix | Yes (Bug Fixes) |
perf |
Performance improvement | Yes (Performance) |
refactor |
Code change (not fix or feature) | Yes (Code Refactoring) |
docs |
Documentation only | No |
style |
Code style (no logic change) | No |
test |
Adding/correcting tests | No |
build |
Build system or dependencies | No |
ci |
CI configuration | No |
chore |
Other changes | No |
revert |
Reverts a previous commit | No |
Commits with scopes ci or deps are excluded from the changelog regardless of type.
birch uses changie for changelog management and a fully automated GitHub Actions pipeline for releases and publishing.
PR with change fragments → merge to main → release workflow creates release PR
→ merge release PR → auto-tag → publish to Hex.pm
Every PR that includes user-facing changes should include a changie fragment. The PR validation workflow checks for this and comments on the PR if an entry is missing.
just change # Create a new changelog entry (interactive)
just changelog-preview # Preview what the next version will look likeChangie will prompt you to select a kind which determines the version bump:
| Kind | Version Bump |
|---|---|
| Added | minor |
| Changed, Deprecated, Fixed, Performance, Removed, Reverted, Dependencies, Security | patch |
Fragments are stored in .changes/unreleased/ and committed with your PR.
The PR validation workflow (.github/workflows/pr.yml) runs on every PR and:
- Validates the PR title against conventional commit format using commitlint
- Checks for changelog entries and posts a preview comment if fragments are found, or a reminder if they're missing
The release workflow (.github/workflows/release.yml) runs on every push to main and can also be triggered manually. It:
- Checks for unreleased changie fragments in
.changes/unreleased/ - If fragments exist, batches them into a versioned changelog entry (version auto-determined from changie kinds)
- Updates the version in
gleam.toml - Creates or updates a release PR with the changelog changes
If no unreleased fragments are found, the workflow skips silently.
When the release PR is merged to main, the auto-tag workflow (.github/workflows/auto-tag.yml):
- Detects the new version from the merged PR
- Creates a Git tag (
v{version}) - Creates a GitHub Release from the tag
When a v* tag is pushed, the publish workflow (.github/workflows/publish.yml):
- Runs the full CI test suite (both Erlang and JavaScript targets)
- If tests pass, publishes the package to Hex.pm
| Workflow | Trigger | Purpose |
|---|---|---|
ci.yml |
Push/PR to main | Tests, formatting, docs build |
pr.yml |
PR opened/updated | Title validation, changelog check |
release.yml |
Push to main | Creates release PR from changie fragments |
auto-tag.yml |
Release PR merged | Creates Git tag and GitHub Release |
publish.yml |
v* tag pushed |
Publishes to Hex.pm |
| Secret | Purpose |
|---|---|
RELEASE_PAT |
GitHub PAT with contents:write and pull-requests:write (needed so release PRs trigger other workflows) |
HEXPM_API_KEY |
API key for publishing to Hex.pm |