Thanks for your interest. The README describes how to build the project and
format the sources; this file describes what happens around a change. AGENTS.md
summarises the layout and conventions of the code for coding agents (and is a fine start for
humans too); the modules with rules of their own have one as well.
Every pull request refers to a GitHub issue that
describes the problem or the feature, so that the discussion of what to do and why happens before
the code is written and stays findable afterwards. Open the issue first if there is none, and name it
in the description of the pull request (Closes #123). The pull requests Dependabot opens are the
exception.
main is protected by a ruleset (Settings > Rules): every change
arrives through a pull request whose status check of the CI is green, force pushes and deletion are
blocked, and the head branch is deleted on merge. No review is required while the project has one
maintainer. Repository admins may bypass the ruleset; releases used that for their version commits
until 0.2.0, since 0.3.0 they go through a pull request too, so the bypass is an emergency exit only. The JSON file in the
repository is the documentation of the ruleset, update it when the rules change (the Rules page can
import it).
Every commit carries a Developer Certificate of Origin sign-off,
the Signed-off-by: Name <email> trailer that git commit -s adds: it states that you have the right
to submit the change under the project's license. The CI checks it for every commit of a pull request
(merge commits and Dependabot's commits excepted) and tells you how to add missing ones
(git rebase --signoff).
- Published POMs are flattened (
flatten-maven-plugin, modeoss): the POM that goes into a jar's artifact has no parent, no imported BOM and every version resolved, so consumers do not see the build structure of this repository. The generated.flattened-pom.xmlfiles are ignored by git and removed by./mvnw clean. The parent POM itself is not published. - Java modules:
easyssf-core,easyssf-test,easyssf-receiverandeasyssf-receiver-jdbcare explicit modules (module-info.java, namesorg.easyssf.core,org.easyssf.test,org.easyssf.receiver,org.easyssf.receiver.jdbc); Micrometer isrequires static. The Spring Boot modules and the conformance harness are automatic modules, named by theAutomatic-Module-Namemanifest entry, which every module sets through theautomatic.module.nameproperty. A new package in an explicit module needs anexportsline. Surefire runs the tests of the explicit modules on the module path;easyssf-receiver-jdbcaddsjava.namingfor test compilation because H2's data source implements ajavax.naminginterface. Surefire keeps JUnit on the class path and opens the test packages to it; IntelliJ IDEA puts JUnit on the module path instead, so the profileideof the root POM, active only in the IDE (it activates on theidea.versionproperty the IDE sets), hands surefire anargLinethat opens every test package of an explicit module toorg.junit.platform.commons, which the IDE copies into its JUnit run configurations. The packages are listed in the propertyide.test.add-opensof each explicit module; a new test package there needs a line, or the IDE fails with "does not opens ... to module org.junit.platform.commons" while Maven passes. - Every module ships an SBOM,
target/bom.jsonin CycloneDX format, attached to the artifact with the classifiercyclonedx. It lists the compile and runtime dependencies. .mvn/jvm.configsilences the schema validator inside the CycloneDX plugin, which warns about keywords of the CycloneDX schema it does not know (meta:enum,deprecated) on every build. The file takes JVM arguments only, so this is where its one line is explained.- Maven 3.9.x: the wrapper (
.mvn/wrapper/maven-wrapper.properties) stays on Maven 3.9 for now. Maven 3.10 uses Resolver 2, which writes_remote.repositoriesmarker files (and their checksums) into the staging directory thecentral-publishing-maven-pluginbundles, and Central rejects the bundle with "Bundle has content that does NOT have a .pom file"; the first attempt to release 0.2.0 failed that way. Dependabot ignores minor and major Maven updates for that reason. Try 3.10 again once the plugin (0.11.0 as of 2026-10-04) excludes those files: build the bundle locally with./mvnw -Prelease -Dgpg.skip -DskipTests deployagainst dummy credentials and checktarget/central-publishing/central-bundle.zipfor_remote.repositories. The settings step of the workflows that scopes the Central credentials (repositoryOrigins, needed by 3.10) stays: Maven 3.9 only warns about the unknown tag. - Database tests: the JDBC stores are tested on H2 by the normal build and on PostgreSQL and
MySQL in Testcontainers containers by the tests tagged
database, which the normal build excludes:./mvnw -pl easyssf-receiver-jdbc -Pdatabase-tests verifyruns them (Docker required). CI runs them in thedatabasejob. - Test logging:
easyssf-receiver/src/test/resources/logback-test.xmlkeeps the tests of the plain receiver at INFO; the expected failures of the negative tests are logged with their stack traces at DEBUG. The Spring Boot modules use Spring Boot's defaults. - Mockito is attached to the test JVM of the autoconfigure module as a Java agent (surefire
argLine, path from the dependency plugin), so it does not attach itself at runtime, which the JDK warns about. Tests started from an IDE do not get the agent and show that warning; it is harmless.
GitHub Actions, see .github/workflows:
ci.ymlruns for every push tomainand every pull request, the cheap checks first so that a failure costs little:changesclassifies the files the change touches;sign-offchecks the DCO trailers of a pull request;docschecks the links (lychee,.lychee.toml) and the spelling (typos,_typos.toml) of the Markdown files when any changed.build, only when something but documentation changed: the formatting check, then the framework-free modules with their tests on Java 21. Its jars go to the jobs behind as an artifact, so they do not build them again.- Behind
build:spring(the Spring Boot modules on Java 21),jdk(every module on Java 25, and on the newest JDK as a job that does not fail the build; it passes-Dbytebuddy.experimental=truein case Mockito's Byte Buddy does not know the class file version yet), anddatabase(the stores ofeasyssf-receiver-jdbcon PostgreSQL and MySQL). Thejdkjob is also where a javadoc comment attached to nothing fails the build: the profiledangling-doc-commentsof the root POM turns on javac's lint of that name with-Werrorwherever the build JDK is 23 or newer, which knows the lint; Java 21 does not. To run the check locally, build with such a JDK, for exampleJAVA_HOME=~/.sdkman/candidates/java/25.0.2-tem ./mvnw -o -DskipTests test-compile. statusis green when every job that had to run succeeded; skipped jobs count as green. It is the one check to require in the branch protection ofmain, the others come and go with the change. A pull request from a fork runs with a read-only token and without secrets. Who may trigger a run is decided under Settings > Actions > General ("Fork pull request workflows", "Approval for running workflows from outside collaborators"). The conformance tests are not part of it.
release.ymlpublishes a release, see below.conformance.ymlruns the four conformance plans against the suite, one job per plan, every night and on demand. The suite and nginx images are the onesConformanceSettingsofeasyssf-test-conformancenames, the last suite release pinned by digest, the single place to change them; a manual run can name other images, for example the registry'slatesttag to try the suite's master ahead of a release.
Releases go to Maven Central under the namespace org.easyssf. The
release profile of the root POM builds what Central requires: sources and javadoc jars, GPG
signatures for every file, and deploy uploads the whole build as one deployment through the
central-publishing-maven-plugin,
which waits until Central has validated and published it. The parent POM, the examples and the
conformance tests (maven.deploy.skip) are built but not published.
The workflow needs four repository secrets: CENTRAL_USERNAME and CENTRAL_PASSWORD, a user token
generated on the portal (account menu, "Generate User Token"), and GPG_PRIVATE_KEY and
GPG_PASSPHRASE, the ASCII-armored private key of oss@easyssf.org and its
passphrase. The public key is on keys.openpgp.org and keyserver.ubuntu.com.
A release goes through a pull request like every other change to main; only the tag is pushed
directly, and tags are not covered by the ruleset. etc/release.sh runs the steps:
etc/release.sh prepare X.Y.Z, on an up-to-datemain, creates the branchrelease/X.Y.Zand sets the version in every POM and in the dependency snippets of the READMEs, setsproject.build.outputTimestampto now, and turns theUnreleasedsection ofCHANGELOG.mdinto the section ofX.Y.Zwith today's date (the workflow publishes that section as the release notes and fails without it). Nothing is committed: review the diff, write the lead paragraph of the section, and check the dependency snippets of other projects the script lists, such as the Quarkus extension's.etc/release.sh propose [X.Y+1.0]commitsRelease X.Y.Z, sets the next development version (the next minor by default) and commitsStart X.Y+1.0, pushes the branch and opens the pull request with the release notes in its description. The release pull request is, like Dependabot's, the exception to the rule that a pull request refers to an issue. Merge it with a merge commit or a rebase, not a squash: the commitRelease X.Y.Zhas to reachmainas it is, because the tag goes on it. With--dry-runthe step stops after the two commits; running it again pushes and opens the pull request, and it does the same after a push orghcall that failed. The two commits travel in one pull request so thatmainis at a snapshot version before and after the merge, with the released version on exactly one commit in between.etc/release.sh release, onmainafter the merge, finds the commitRelease X.Y.Z, checks its POM version and changelog section, creates the signed tagvX.Y.Zon it and pushes the tag, then watches the release workflow, which checks that the tag matches the POM version, runs the tests, signs, publishes, and creates the GitHub release with the changelog section and the SBOMs attached. The artifacts are on Central a few minutes after the workflow finishes; search indexes them within hours. If the pull request was squashed, the script says so:mainthen has no commit with the release version, and the two commits have to be brought back with a new pull request.
The script refuses to run on the wrong branch, on a main that differs from origin/main, on a dirty
tree, or for a tag that exists. Between the merge and the next snapshot commit, main carries the
release version for one commit, which the snapshot publishing job recognizes and skips. The admin
bypass of the ruleset is not needed for releases.
Snapshots of main are published by ci.yml to the
Central snapshot repository while the
repository variable PUBLISH_SNAPSHOTS is true. Consumers add that repository with
<snapshots><enabled>true</enabled></snapshots>.
Both workflows let setup-java write the settings.xml with the Central token as the server
central, and then add <repositoryOrigins>https://central.sonatype.com</repositoryOrigins> to that
server. Maven 3.10 offers the credentials of a settings server only to repositories whose origin it
can associate with the server id, and central is also the id of Maven Central, so without the
origin the uploads to central.sonatype.com get no credentials and fail with 401. (The alternative,
-Dmaven.repository.credentialScope=id, restores the id-only matching of Maven 3.9 for every
server.)
To try the release build locally without signing or uploading:
./mvnw -Prelease -Dgpg.skip verify. With the key in the local keyring, set MAVEN_GPG_PASSPHRASE
to sign as well; the passphrase never goes on the command line.