Contributing
Contributions are welcome — bug reports, documentation improvements, and new features alike. This page covers everything you need to go from a fresh clone to a passing build and an open PR.
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Java | 25+ | Use the Gradle toolchain — no manual install needed if you have a JDK provider |
| Gradle | wrapper (./gradlew) | Never install Gradle globally; always use the wrapper |
| Node.js | 18+ | Only needed to run or build the documentation site |
| pnpm | 11.1.3+ | Run from the site/ directory — enable with corepack enable |
No other global tools are required.
Why Java 25? The Java 25 minimum is an explicit architecture decision: it enables
Gatherer(finalized in Java 24) for true short-circuitsequence/traverse, stable virtual threads (Java 21+) forTry.withTimeout, and exhaustive sealed-interface pattern matching (Java 21+). See ADR-001 — Java 25 as the minimum required version.
Clone and build
git clone https://github.com/domix/dmx-fun.gitcd dmx-fun./gradlew build./gradlew build compiles all modules, runs all tests, and generates the aggregated Javadoc and coverage report.
Running tests
# Run all tests across all modules./gradlew test
# Run tests for a specific module./gradlew :lib:test./gradlew :jackson:test./gradlew :assertj:test
# Run a single test class./gradlew :lib:test --tests "dmx.fun.OptionTest"To see the full test report after a run, open build/reports/tests/testAggregateTestReport/index.html.
Running the documentation site locally
cd sitepnpm install # first time onlypnpm run dev # starts dev server at http://localhost:4321/dmx-fun/The dev server watches for changes in site/src/ and reloads automatically.
The site is served under the /dmx-fun/ base path to match GitHub Pages.
Note:
pnpm run buildinsite/copies the Javadoc frombuild/reports/javadoc/into the site’spublic/directory. Run./gradlew aggregateJavadocfirst if you need the full build.
Code conventions
No null in the public API
All public methods must be annotated with @NullMarked (via the package-info or class-level annotation).
Return Option<T> instead of a nullable type; accept @Nullable parameters only when the absence is
meaningful to the implementation.
The choice of jspecify as the null-safety annotation library is documented in ADR-008 — jspecify (@NullMarked, @Nullable) for null safety.
Exception:
Try.Successdeliberately acceptsnullas a valid value when produced byTry.run()(void side-effects —Voidhas no instances in Java).Result.Okrejectsnull. This asymmetry is documented in ADR-004 — Try allows Success(null); Result.Ok rejects null.
Sealed interfaces and records
Every sum type in the library is a sealed interface with record implementations —
a deliberate design decision documented in ADR-003 — Sealed interfaces + records as ADT representation.
This enables exhaustive pattern matching for callers and removes the need for instanceof chains.
// Correct — sealed + recordspublic sealed interface Option<T> permits Option.Some, Option.None { record Some<T>(T value) implements Option<T> {} record None<T>() implements Option<T> {}}No checked exceptions in the public API
Checked exceptions must not appear in public method signatures.
Wrap third-party code that throws in Try.of(...) and surface failures through Result or Try.
Test naming
Test method names follow the pattern <condition>_<expected outcome>, written in camelCase:
@Testvoid emptyOption_mapReturnsNone() { ... }Commit convention
This project uses Conventional Commits:
| Type | When to use |
|---|---|
feat | New public API or behaviour |
fix | Bug fix |
docs | Documentation only |
refactor | Code change with no behaviour change |
test | Adding or updating tests |
chore | Build, CI, dependencies |
perf | Performance improvement |
Always reference the issue number: feat(option): add mapBoth combinator (close #42).
Branch naming
<type>/<short-description>feat/option-map-bothfix/try-recover-npedocs/guide/adding-validated-examplesOpening a pull request
- Fork the repository and create a branch from
main. - Make your changes — one concern per PR.
- Ensure
./gradlew buildpasses locally. - Open the PR against
mainwith a title following the commit convention. - Link the related issue in the PR description (
close #NNN).
All PRs run the full CI pipeline (gradle.yml) automatically.
CI pipelines
All pipelines live in .github/workflows/. The table below is a quick reference; the sections
that follow explain each one in detail.
| File | Trigger | Purpose |
|---|---|---|
gradle.yml | push to main, any PR | Full build, tests, coverage |
publish.yml | push to main (stable version) | Publish to Maven Central + create GH tag |
publish-snapshot.yml | push to main (SNAPSHOT version) | Publish SNAPSHOT to Maven Central |
pages.yml | push to main, manual dispatch | Build Javadoc + Astro site → GitHub Pages |
assertj-compatibility.yaml | PR touching assertj/** | Matrix: AssertJ 3.21 – 3.27 |
jackson-compatibility.yaml | PR touching jackson/** | Matrix: Jackson 2.13 – 2.21 |
spring-compatibility.yaml | PR touching spring/** | Matrix: Spring 6.0 – 7.0 |
gradle.yml — Main CI
Runs on every push to main and on every pull request against main.
For pushes, a dorny/paths-filter step skips the build job when none of the source directories
have changed (avoids redundant runs for docs-only commits). For PRs, the build always runs.
./gradlew buildThis compiles all modules, runs all tests, generates the aggregated test report and JaCoCo
coverage report. A madrapps/jacoco-report step then posts coverage deltas as a PR comment.
A separate dependency-submission job (push-only) submits the dependency graph to GitHub so
Dependabot can raise alerts for vulnerable transitive dependencies.
publish.yml — Stable release
Triggered on push to main when the changed paths include source or build files. The workflow
reads version from gradle.properties and skips entirely if:
- the version ends with
-SNAPSHOT, or - the tag
v{version}already exists in the remote.
When both checks pass it runs the full test suite, publishes all modules to Maven Central, pushes the release tag, and creates a GitHub Release with auto-generated release notes.
The required repository secrets are:
| Secret | Used for |
|---|---|
MAVEN_CENTRAL_USERNAME | Maven Central auth |
MAVEN_CENTRAL_PASSWORD | Maven Central auth |
SIGNING_KEY_ID | In-memory PGP key ID |
SIGNING_KEY | In-memory PGP key (armored) |
SIGNING_KEY_PASSWORD | PGP key passphrase |
To trigger a stable release: set version=X.Y.Z (no -SNAPSHOT) in gradle.properties and
push to main.
publish-snapshot.yml — SNAPSHOT publish
Mirrors publish.yml but fires when version ends with -SNAPSHOT. It runs tests then
publishes to Maven Central’s snapshot repository. No tag is created.
To trigger: ensure version=X.Y.Z-SNAPSHOT in gradle.properties and push to main.
pages.yml — GitHub Pages deploy
Triggered on push to main when site/**, any build.gradle, or source files under
lib/src/main/, assertj/src/main/, or jackson/src/main/ change. Also supports
workflow_dispatch for manual deploys.
Steps:
./gradlew aggregateJavadoc— builds multi-module Javadoc intobuild/reports/javadoc/pnpm run build(insite/) — copies the Javadoc intosite/public/and producessite/dist/- Uploads
site/dist/as a Pages artifact and deploys viaactions/deploy-pages
To preview the site locally without triggering CI:
./gradlew aggregateJavadoccd site && pnpm run devCompatibility matrix workflows
Each optional-dependency module has its own matrix workflow that runs on PRs touching that module’s directory. The matrix version is passed as a Gradle property; the build falls back to the version catalog entry when the property is absent (safe for local development).
| Workflow | Gradle property | Tested range |
|---|---|---|
assertj-compatibility.yaml | -PassertjVersion | 3.21.0 – 3.27.7 |
jackson-compatibility.yaml | -PjacksonVersion | 2.13.5 – 2.21.2 |
spring-compatibility.yaml | -PspringVersion | 6.0.23 – 7.0.6 |
To run a specific version locally:
./gradlew :assertj:test -PassertjVersion=3.25.3./gradlew :jackson:test -PjacksonVersion=2.17.3./gradlew :spring:test -PspringVersion=6.1.21When you add a new module with an optional peer dependency, create
.github/workflows/{module}-compatibility.yaml following this same pattern.
See step 6 of the Adding a New Module guide for details.
Note on lib/src/test/resources/
This directory is intentionally untracked (it appears in git status as untracked).
It is generated at test time and should not be committed.