Maintainer runbook. Publishing and pushing are never automated: every step in section 4 requires explicit maintainer action, and agents must stop at the end of section 3.
The reactor version is the revision property in the root pom.xml
(flatten-maven-plugin oss mode resolves it into published POMs). All
nine published coordinates - the repofyr-parent POM, the seven
repofyr-*_2.13 server artifacts, and repofyr-embedded-mongo_2.13 -
release together at that one version. repofyr-dev-server is excluded
deliberately; see the excludeArtifacts note in the release profile.
The server version line is independent of onfhir.libs.version, the
io.onfhir reusable-library line released from srdc/onfhir-libs. Both
lines start at 4.0.0 and will diverge. Repofyr is GPL-3.0; the libraries it
consumes are Apache-2.0. Never change an io.onfhir coordinate, package,
or license from this repository.
- Patch (
4.0.x): fixes only, no server API or configuration change. - Minor (
4.x.0): additive only - new operations, new extension points, new optional configuration keys. An existing deployment upgrades without edits. - Major (
5.0.0, ...): breaking server API or configuration changes, shipped with a migration guide entry underdocs/migration/.
Configuration keys, persistence identifiers, and stored-data conventions are part of the compatibility contract. Changing one is a major even when no Scala signature moves, because a running deployment cannot absorb it silently. Deprecate in a minor, remove in the following major. Majors are event-driven: ship one when breaking changes have accumulated, not on a calendar. Published releases are immutable on Maven Central; fixes always roll forward as a new version, never in place.
There is deliberately no binary-compatibility gate here. Repofyr is
consumed as a deployable server and as an extension point, not as a broad
library API surface, so the migration guide is the contract that
srdc/onfhir-libs enforces with a MiMa baseline.
git log/git status: confirm the tree is the state you intend to release (this repository is sometimes worked on by parallel sessions).CHANGELOG.md: the entry for this version is complete; stamp the release date.- If any server API, package, coordinate, or configuration change shipped
since the last release, confirm the migration guide under
docs/migration/covers it. - Library version check.
onfhir.libs.versionin the rootpom.xmlmust name a released, non-SNAPSHOTio.onfhirversion that is resolvable from Maven Central. A server release whose libraries resolve only from a local install or a staging directory is not reproducible for anyone else, and Central will reject a POM whose dependencies it cannot see. The property is4.0.0today, andio.onfhir4.0.0 is published - all fourteen artifacts the reactor depends on resolve from Central - so this no longer blocks the first server release. Re-check it wheneveronfhir.libs.versionmoves: confirm the version resolves from Central and not merely from~/.m2, which a localmvn installin the sibling repository will happily satisfy. The fresh-cache build in the next item is what actually proves it. - Full verification suite is green (the
verifyskill):mvn -B test- full reactor, zero failures (334 tests today);powershell -File scripts/check-server-boundary.ps1- expectcheck-server-boundary: PASS - server modules stay in io.repofyr.*;- the same reactor against a throwaway Maven cache, which is what
distinguishes a published dependency from a locally installed one:
mvn -B -Dmaven.repo.local=<throwaway> test.Dockerfile-buildJarexercises the same property from inside a container, so a successfuldocker build -f docker/Dockerfile-buildJarcounts as a second independent check of it.
- Fresh-checkout rehearsal: clone into a temporary directory and run the
reactor tests there. This catches working-copy-only state - an untracked
configuration file, a stale
target/, or a locally installed artifact that masks a missing dependency declaration.
-
The SRDC release GPG key must be importable on the build machine. Headless signing works through loopback pinentry, which the
releaseprofile already passes tomaven-gpg-plugin, so signing does not prompt. -
Deploy the full reactor to a file-based staging repository:
mvn -B -Prelease deploy -DskipPublishing=true -DaltDeploymentRepository=staging::file:///<absolute-staging-path>
Both flags are required, and
-DskipPublishing=trueis the one that keeps this local. Through 4.0.0 this step was documented with-DaltDeploymentRepositoryalone and described as "not a publish". It was: thereleaseprofile runscentral-publishing-maven-pluginwith<extensions>true</extensions>, which injects its ownpublishgoal into the deploy lifecycle, and that goal contacts the Central portal and ignoresaltDeploymentRepositoryentirely. Running the old command on 2026-08-18 created real portal deployment31bedb80-2625-4031-babd-2bcc7c198ce5. Nothing became public -autoPublish=falseandwaitUntil=validatedstop at validation - but the staging directory stayed empty, because nothing was ever routed to it.skipPublishingis a parameter of the plugin'spublishgoal: boolean, defaultfalse, exposed as${skipPublishing}. Verified against the 0.8.0 plugin descriptor rather than inferred from documentation..claude/hooks/block-remote-publish.shenforces this, denying any Maven deploy that lacks either flag, so the command that caused the accident cannot be run from Claude Code again.The staged artifacts do not land at the path you name. With
<extensions>true</extensions>the publishing plugin takes over thedeployphase entirely, somaven-deploy-pluginnever runs andaltDeploymentRepositoryroutes nothing. The artifacts are written to the plugin's ownstagingDirectory, which defaults totarget/central-staging, and the bundle it would upload totarget/central-publishing. Verify the former, not the path you passed - on 2026-08-18 the documented path was searched, found absent, and mistaken for a failed run.The flag is kept in the command anyway, for two reasons: the hook requires both as an explicit signal of intent, and it would start routing again if
<extensions>true</extensions>were ever removed.The
releaseprofile also attaches the sources and javadoc JARs that Maven Central requires. -
Verify the staging repository:
powershell -File scripts/check-staged-release.ps1 -RepositoryPath target/central-staging -Version <version>
-RepositoryPathis mandatory and istarget/central-staging- see above, it is not whateveraltDeploymentRepositorynamed.-Versiondefaults to4.0.0. The script walks the nine coordinates and asserts, for each: the POM exists, declares the GNU General Public License, does NOT declare an Apache License, and carries no unresolved${revision}(which would mean flatten did not run); the binary,-sourcesand-javadocJARs are present; the binary JAR packagesMETA-INF/LICENSE; and every file has a good detached.ascsignature.Expect:
check-staged-release: PASS - 9 <version> artifacts verified. -
Resolve the staged artifacts the way a consumer will. The gate above checks what each POM says; this checks what Maven does with it:
mvn -B -Dmaven.repo.local=<throwaway> dependency:resolve
from a one-dependency throwaway project naming a
repofyr-server-*artifact, withtarget/central-stagingadded as afile:///repository. Expect the whole server stack - on the order of 100 artifacts, includingrepofyr-coreand theio.onfhirlibraries. A handful means the POM is not declaring its dependencies.This is not optional, and it belongs here rather than after publishing. 4.0.0 shipped with all three server POMs dependency-reduced by shade, declaring no compile dependencies at all: the coordinate resolved to one jar instead of a hundred and failed at runtime. Section 5 listed this check as a post-publish item, which is too late - a published release is immutable, and 4.0.0 could only be superseded by 4.0.1. The gate now asserts the POM contents too, but run this as well: the gate encodes what went wrong last time, and resolution is what actually matters.
The license assertion runs in both directions on purpose. Repofyr is
GPL-3.0 and the io.onfhir libraries are Apache-2.0, so an accidental
flip either way is a release-blocking defect that no test would catch.
Signing must not be disabled to get a green run. An unsigned rehearsal is
useful only when the maintainer explicitly asks for one, via
-Dgpg.skip=true on the deploy plus -SkipSignatures on the check; a
repository verified that way is not a release candidate.
The script's artifact table is hand-maintained. Adding a module to the reactor means adding a row, or the new artifact ships unverified.
Not required for a Repofyr server release. Decided 2026-08-18, after 4.0.0 shipped without one - itself a major, with renamed coordinates and packages. Consumers pin their versions and migrate on their own schedule, so a release cannot break them retroactively; the gates in section 1 and the staging checks in section 2 are the acceptance bar.
The rehearsal for the reusable libraries belongs to srdc/onfhir-libs, whose
artifacts a far wider set of consumers compile against.
If you do want one - a release that restructures the POM or the release profile is the case that would justify it - the mechanics are below. The trap is worth reading even if you skip the rest.
Purge io/repofyr from the rehearsal's local Maven repository first - and
io/onfhir too when the libraries are being released from staging in the
same cycle - so the consumer builds can only resolve from the staged
repository. A stale cached artifact otherwise produces a false pass.
- spark-on-fhir: build it against the staged artifacts, applying the
Migration Table in the migration guide. Server-owned types moved from
io.onfhir.*toio.repofyr.*and the coordinates changed with them, so this is a source migration and not a version bump. Run its complete build, not a subset. - CRT: point it at the released spark-on-fhir and
io.onfhirlibrary versions, remove any SNAPSHOT or local-Maven workaround, and run its launch verification. - Other internal and public consumers: they migrate on their own release schedules and do not gate this release once 1 and 2 pass.
- Record every omission the rehearsal discovers back into the migration guide and the release notes. The rehearsal exists to find gaps in that guide, so a discovery is a successful rehearsal, not a failed one.
Agents stop here. Everything below is maintainer-only and explicitly authorized per release.
-
Upload to the Central portal:
mvn -B -Prelease deploy
The
releaseprofile configurescentral-publishing-maven-pluginwithautoPublish=falseandwaitUntil=validated: this uploads the bundle, waits for Central to validate it, and stops. Nothing is public yet. Credentials come from thecentralserver entry in the maintainer'ssettings.xml. -
Promote the validated deployment in the Central portal under the SRDC account. This is the irreversible step - a published release is immutable and a mistake can only be rolled forward - so review the deployment's artifact list once more before publishing it.
-
Tag
v<version>and push the repository and the tag. -
Build and push the Docker images. From the repository root, after the reactor has been built (
Dockerfile-addJarcopies the already-builtrepofyr-server-standalone.jar;docker/build.shrecords the same commands):docker build -f docker/Dockerfile-addJar --build-arg FHIR_VERSION=r4 -t srdc/repofyr:r4 . docker build -f docker/Dockerfile-addJar --build-arg FHIR_VERSION=r5 -t srdc/repofyr:r5 .
Tag each image with the release version as well as the floating
r4/r5tag, then push both tags, so a deployment can pin an exact build.Dockerfile-buildJaris the alternative that builds inside the image; it skips tests via-Pxtest, which makes it unsuitable as the release build. -
Create the GitHub release: changelog excerpt plus a link to the migration guide.
- Convert each entry in
docs/release/known-limitations.mdinto a GitHub issue and link the issue numbers back into that file. - Bump
revisionin the rootpom.xmlto the next development version. - Verify the README Maven Central badge renders. It reports "not found" until the first publish, so this check only becomes meaningful now.
- Re-confirm resolution against Central once propagation completes, repeating the section 2 step 4 check without the staging repository. This is a confirmation, not the gate: the gate is step 4 itself, run before promotion, because after promotion a defect can only be superseded by a new version.
- Announce as appropriate (release notes, downstream consumers).