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

ToolVersionNotes
Java25+Use the Gradle toolchain — no manual install needed if you have a JDK provider
Gradlewrapper (./gradlew)Never install Gradle globally; always use the wrapper
Node.js18+Only needed to run or build the documentation site
pnpm11.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-circuit sequence/traverse, stable virtual threads (Java 21+) for Try.withTimeout, and exhaustive sealed-interface pattern matching (Java 21+). See ADR-001 — Java 25 as the minimum required version.

Clone and build

Terminal window
git clone https://github.com/domix/dmx-fun.git
cd dmx-fun
./gradlew build

./gradlew build compiles all modules, runs all tests, and generates the aggregated Javadoc and coverage report.

Running tests

Terminal window
# 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

Terminal window
cd site
pnpm install # first time only
pnpm 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 build in site/ copies the Javadoc from build/reports/javadoc/ into the site’s public/ directory. Run ./gradlew aggregateJavadoc first 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.Success deliberately accepts null as a valid value when produced by Try.run() (void side-effects — Void has no instances in Java). Result.Ok rejects null. 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 + records
public 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:

@Test
void emptyOption_mapReturnsNone() { ... }

Commit convention

This project uses Conventional Commits:

TypeWhen to use
featNew public API or behaviour
fixBug fix
docsDocumentation only
refactorCode change with no behaviour change
testAdding or updating tests
choreBuild, CI, dependencies
perfPerformance improvement

Always reference the issue number: feat(option): add mapBoth combinator (close #42).

Branch naming

<type>/<short-description>
feat/option-map-both
fix/try-recover-npe
docs/guide/adding-validated-examples

Opening a pull request

  1. Fork the repository and create a branch from main.
  2. Make your changes — one concern per PR.
  3. Ensure ./gradlew build passes locally.
  4. Open the PR against main with a title following the commit convention.
  5. 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.

FileTriggerPurpose
gradle.ymlpush to main, any PRFull build, tests, coverage
publish.ymlpush to main (stable version)Publish to Maven Central + create GH tag
publish-snapshot.ymlpush to main (SNAPSHOT version)Publish SNAPSHOT to Maven Central
pages.ymlpush to main, manual dispatchBuild Javadoc + Astro site → GitHub Pages
assertj-compatibility.yamlPR touching assertj/**Matrix: AssertJ 3.21 – 3.27
jackson-compatibility.yamlPR touching jackson/**Matrix: Jackson 2.13 – 2.21
spring-compatibility.yamlPR 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 build

This 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:

SecretUsed for
MAVEN_CENTRAL_USERNAMEMaven Central auth
MAVEN_CENTRAL_PASSWORDMaven Central auth
SIGNING_KEY_IDIn-memory PGP key ID
SIGNING_KEYIn-memory PGP key (armored)
SIGNING_KEY_PASSWORDPGP 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:

  1. ./gradlew aggregateJavadoc — builds multi-module Javadoc into build/reports/javadoc/
  2. pnpm run build (in site/) — copies the Javadoc into site/public/ and produces site/dist/
  3. Uploads site/dist/ as a Pages artifact and deploys via actions/deploy-pages

To preview the site locally without triggering CI:

Terminal window
./gradlew aggregateJavadoc
cd site && pnpm run dev

Compatibility 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).

WorkflowGradle propertyTested range
assertj-compatibility.yaml-PassertjVersion3.21.03.27.7
jackson-compatibility.yaml-PjacksonVersion2.13.52.21.2
spring-compatibility.yaml-PspringVersion6.0.237.0.6

To run a specific version locally:

Terminal window
./gradlew :assertj:test -PassertjVersion=3.25.3
./gradlew :jackson:test -PjacksonVersion=2.17.3
./gradlew :spring:test -PspringVersion=6.1.21

When 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.