Publishing
A build ends at artifacts under target/. Publishing is what makes them available to somebody else: the
artifacts laid out as a repository, carrying the metadata a repository demands, and finally uploaded and
signed. Publishing to Maven Central is really two jobs - produce a correct, complete bundle and upload
it - and Jenesis owns the first while deliberately leaving the signed upload to a dedicated release tool.
Publishing locally with export
Another project on the same machine can require a module only once it has been exported: a build writes
under its own target/, which no other project reads. export builds the project, stages the release tree
that the next section describes, and copies it into the local repositories:
java build/jenesis/Make.java export
- The
mavenlayout exports only the Maven artifacts, into the local Maven repository (~/), under. m2/ repository <group>/. Maven, and Gradle through<artifact>/ <version>/ mavenLocal(), consume them from there. - The
modularlayout exports only the modules, into the local module repository (~/), under. jenesis <module>/. Every export also refreshes<version>/ <module>/, which holds the latest one.<module>. jar modular_, the default, exports both, so one export is consumed everywhere: by a POM-based project of any build tool through the local Maven repository, and by a Jenesis project that requires the module by name through the local module repository.to_ maven
A project that sets no version exports an unversioned module, whose POM carries 0-SNAPSHOT. Another project
requires the module by name, and the local repositories are read before any remote: without a pin it takes
the latest export, with a pinned version that version's build. A consumer does not notice a new export on its
own; it takes one when it is rebuilt with -Djenesis..
Staging the release tree
The stage target materialises the full release tree in Maven repository layout under
target/: the main jar, the POM, and - when you ask for them - the -sources. and
-javadoc. that Central demands. You enable those and set the version from the command line:
java -Djenesis.project.version=1.0.0 \
-Djenesis.project.sources=true \
-Djenesis.project.documentation=true \
build/jenesis/Make.java stage
Central requires the -javadoc. but not that it documents anything, and rendered documentation can make
up most of a release's size. With -Djenesis., the jar is still staged but holds
nothing but a file named INTENTIONALLY_, and no documentation tool runs.
Central also requires the POM to carry name, description, url, <licenses>, <developers>, and
<scm>. Jenesis folds two channels into each POM. Everything it can derive from the source comes first: the
coordinate from the module name, and the name and description from its Javadoc, or all three from the source
pom.. The first sentence of the module's documentation comment is its name, and the comment's second
paragraph its description, each reduced to plain text: HTML, <!-- --> comments, {@code} and {@link}
markup give way to the words they mark, and a third paragraph is left for the reader of the source.
///) keeps its inline markup: `code` and **emphasis**
reach the name and the description as written, and only a link is reduced to its label. The JDK renders Markdown
in its javadoc tool alone, so a build has no Markdown to plain text conversion to call.
A project. file at the project root carries only what a module declaration cannot express. Jenesis
reads it when it is there; -Djenesis. names other files instead, and an empty value reads none:
# project.properties
url=https://github.com/jenesis/jenesis
license.apache-2_0.name=Apache-2.0
license.apache-2_0.url=https://www.apache.org/licenses/LICENSE-2.0.txt
developer.raphw.name=Rafael Winterhalter
developer.raphw.email=rafael.wth@gmail.com
scm.connection=scm:git:https://github.com/jenesis/jenesis.git
scm.url=https://github.com/jenesis/jenesis
organization.name=Example Ltd
organization.url=https://example.com
copyright=Copyright 2026 Example Ltd
manufacturer.name=Example Ltd
manufacturer.url=https://example.com
publisher=Example Ltd
organization. and organization. become the POM's <organization>, and a source pom. supplies
them from its own <organization>. copyright has no place in a POM; it is taken as written into the SBOM and
an installer, and no year is added to it. manufacturer., manufacturer. and publisher have no place
in a POM either and are recorded in the SBOM alone. Every one of these keys is optional, and nothing is recorded
for a key the project does not declare.
The jar carries the POM it is published with as well, at META-INF/, beside a
pom. that holds its groupId, artifactId and version - where Maven places them in every jar it
builds. Tools that find a jar inside an image or an archive identify it by these files: a scanner such as Syft,
or GraalVM's own SBOM of a native image. The POM is generated before the jar is packed so that both carry the
same one, and a module under the modular layout, which has no Maven coordinate, carries neither.
-Djenesis. leaves both out of the jar.
Created-By: Jenesis rather than the JDK that ran the build, and Javadoc is generated
with -notimestamp, so two independent builds of the same sources hash bit-for-bit identically -
a consumer can verify the bytes on Central were built from the published sources. Declare your own
Created-By and it is kept.
Pointing a release at its sources
The repository URL says where a project lives, not which revision a release was built from. Three properties
say that: jenesis. names the revision, for Git the commit id; jenesis. names the
tag the release carries, if it has one; and jenesis. names the Git tree of that revision, which
identifies the content of the whole repository independently of its history. Pass them on the command that
stages the release:
java -Djenesis.project.version=1.0.0 \
-Djenesis.project.tag=v1.0.0 \
-Djenesis.project.revision=$(git rev-parse HEAD) \
-Djenesis.project.tree=$(git rev-parse HEAD^{tree}) \
build/jenesis/Make.java stage
A GitHub Actions workflow has the first two at hand: ${{ github. is the commit the run builds, and
${{ github. is the tag when a pushed tag started the run. The tree comes from the checkout.
- run: >
java -Djenesis.project.tag=${{ github.ref_name }}
-Djenesis.project.revision=${{ github.sha }}
-Djenesis.project.tree=$(git rev-parse HEAD^{tree})
build/jenesis/Make.java stage
Neither value is derived: projects name their tags differently, so the tag is not guessed from the version.
A value that does not change from release to release can also be declared in project., as
scm., scm. or scm., and a source pom. contributes the <tag> of its <scm>. A property takes
precedence over a declared value, and an empty property, such as -Djenesis., records nothing even
when a value is declared.
The tag becomes the <tag> of the POM's <scm>; the POM has no element for a revision or a tree. The SBOM
records the tag and the revision as the jenesis:scm:tag and jenesis:scm:revision properties of the project's
component, and the tree in the component's swhid field, as the SWHID swh:1:dir:<tree>; a tree
that is not a 40-character Git tree id fails the build. When
scm. names a URL, such as scm:git:https:/, the SBOM also carries a
vcs reference that locates the sources at the revision, or at the tag when no revision is given, in the
notation SPDX uses for a download location: git+https:/. A tag of
HEAD, which a pom. declares for the root of its repository, counts as no tag there.
Publishing a bill of materials
A module can also publish the pin set of its own resolved closure, so a downstream project imports the versions this one was built and tested against instead of curating its own. It is the emitting counterpart of the bills of materials you already know how to consume.
A bom. file in the configuration folder switches it on - the file may be empty, presence is the
switch. The modular layouts then render the module's closure as a properties file that export publishes
into the local module repository beside the module jar:
~/.jenesis/demo.bom/1.0.0/demo.bom.properties
Another project consumes it with @jenesis., exactly the way it consumes a hand-written file. The
BOM travels through the module layout only; the Maven export never carries it.
Signing the jar itself
There are two signatures around a published jar, and they answer different questions. The detached one the
next section covers travels beside the artifact and says who published it. The other lives inside the
archive: jarsigner, the JDK's own signer, writes a manifest of digests and a signature block into the jar,
which a JVM can check as it loads the classes.
Naming a key store turns it on:
jenesis.jarsigner.keystore the key store to sign with
jenesis.jarsigner.alias the key within that store
jenesis.jarsigner.storepass where the store's password is read from
jenesis.jarsigner.keypass the same, where the key has a password of its own
jenesis.jarsigner.storetype the JDK's own default otherwise, normally PKCS12
jenesis.jarsigner.tsa a timestamp authority, so a signature outlives the certificate
jenesis.jarsigner.arguments anything else jarsigner accepts
These are properties rather than a configuration file because where the key is and what unlocks it differ
between a laptop, a release machine and a CI runner, and a developer without the key still has to be able to
build. They belong to the machine that signs: a project's jenesis. or profile that sets any of
them is refused, since a file travelling with the sources would decide which key a build reaches for and
which file a password is read from. A machine holds them once, for everything built on it:
# ~/.jenesis/jenesis.properties
jenesis.jarsigner.keystore=/home/me/.keys/signing.p12
jenesis.jarsigner.alias=me
jenesis.jarsigner.storepass=file /home/me/.keys/signing.pass
and a release runner names them for one run, where a -D wins over that file:
java -Djenesis.jarsigner.keystore=/etc/jenesis/release.p12 \
-Djenesis.jarsigner.alias=release \
-Djenesis.jarsigner.storepass=env JENESIS_KEYSTORE_PASSWORD \
build/jenesis/Make.java
Naming any of those keys says the project signs its jar. Saying that and then leaving the key store, the alias or the password location unnamed stops the build rather than producing an unsigned jar - a release that shipped unsigned because a runner forgot a flag is the one outcome worth refusing. A project that names none of them does not sign.
A password is never a value, not even a property value. storepass and keypass take a location in
jarsigner's own grammar - env <variable> or file <path> - and anything else fails the build naming the two
forms; a password passed as -D would otherwise stand in the process list of the machine that runs the build.
The key store itself is referenced by path and never copied into target/, so no private key reaches the
build tree or a shared build cache.
Signing is part of producing the artifact rather than a step after it: the signer reads the jar the archiver
wrote and writes the signed jar in its place, so the unsigned jar never leaves the toolchain. The module's
inventory, both staged repositories, an export and a publication all see only the signed jar - and a
detached signature made afterwards covers the signed bytes.
target/ after a key rotation.
Releasing into a Jenesis module repository
One destination the tool publishes to by itself: a Jenesis module repository, the layout a modular build
resolves module names from. export reaches one machine; once jenesis. names a repository,
release puts each staged module there as its release/ step, for every machine that resolves
from it. A jenesis repository of a Jenesis Repository takes that release and serves the layout, at
the same address a build names in jenesis.:
java -Djenesis.project.version=1.0.0 \
-Djenesis.release.uri=https://repo.example.com/repository/releases/<repo>/ \
-Djenesis.release.token=<key> \
build/jenesis/Make.java release
Each module is put once: its jar, under its version, at module/ below that
address. What is staged beside the jar, such as the -sources. or the POM, stays in the staged tree. A project
whose jenesis. names the same address then resolves the module by its name at that version.
release does this in the modular and modular_ layouts, and
refuses to run in the maven layout, which stages no modular tree.
A release needs a version: a module staged without one, as when jenesis. is not set, fails
the release before anything is put into the repository, and so does a module staged at more than one
version.
The key is sent as the Authorization header, exactly as given. Where the settings are not given, a release
reads the JENESIS_ and JENESIS_ environment variables - never the variables naming
the repositories a build resolves from, so a CI job keeps its release key apart from the key it reads with.
The key is a credential: only the command line, ~/ or the environment may name it,
and it is never sent to a repository that a file of the project named, so a project that names its own release
address in jenesis. releases without a key. A plaintext http: address is refused unless
-Djenesis. allows it.
A project that publishes to Maven does not need this step to reach module consumers: a java repository of a
Jenesis Repository takes Maven publishes only, and makes a module available from one. A modular jar deployed to its
Maven layout, by JReleaser, mvn deploy or any other tool, is served by its module name as well.
The last mile: signing and uploading
For Maven Central, and for every other publication beyond a Jenesis module repository, the remote upload and
GPG signing are not Jenesis's job. Point JReleaser at
target/ and it signs every artifact and uploads the bundle to Central. Jenesis stops at
the unsigned, validated bundle, so credentials and signing keys never enter the build.
This split is deliberate. Most people building a project never release it: releasing is a rare, tightly controlled job for CI or a hardened environment that holds the keys. And the way you release evolves independently of how the build produces artifacts.
Driving the release tool from the build
You can still reach that tool through the same selector vocabulary as everything else. release is a target on
every layout, and it depends on stage, so what a release tool uploads is always the tree this build just
produced:
java build/jenesis/Make.java release
The target exists whether or not anything is configured; the tool is what a file activates. A jreleaser.
(or ., ., .) at the project root adds a release/ step - a project-root lookup
rather than the usual per-module configuration folder, because JReleaser resolves every path in its
configuration against one base directory. -Djenesis. names a different file.
It contributes two steps. The first writes a jreleaser. holding JRELEASER_, the
version this build stamped, so the version is stated once rather than passed to two tools that can then
disagree. Point a configuration at it with
environment: { variables: target/. The second runs
the jreleaser executable found in the environment, forwarding the process environment unchanged. Every
JRELEASER_ credential is therefore read by JReleaser itself and never touched, logged, or stored by the
build.
release is a dry run by default: every local phase runs and every remote one is
skipped. A real release needs -Djenesis.jreleaser.dry=false, the single switch that separates
a rehearsal from a publication - so no combination of selectors alone can publish.
native-image is. Every tool that shapes an artifact - a compiler, a linter - is pinned,
because reproducing a build means reproducing it exactly. A release tool shapes nothing: it transmits a
finished, already-reproducible tree. What it needs instead is credentials, network access, and a git identity,
which belong to the release environment.
release target is for what they do not cover: rehearsing a release
locally, and releasing from a pipeline that has no such integration.