Maven Packaging, install, deploy, Distribution Management, Signing, and Repository Publishing: Guided Hands-On Workflow and Core Operations
Build and publish a harmless Maven JAR through package, verify, install, and deploy using isolated local state, a file-backed repository, attached sources/Javadocs, checksums, and a synthetic detached-signature path.
This workflow turns the model into evidence. Every mutation stays
under one disposable directory, and every important command is paired
with a prediction about target/, the isolated local
repository, or the file-backed publication repository.
Learning objectives
- Author a minimal publishable Maven project with pinned publishing-related plugins.
- Inspect the main JAR and attached sources/Javadoc artifacts.
- Compare package, install, verify, and deploy side effects with isolated repositories.
- Publish to a local file repository and inspect coordinates, metadata, and checksums.
- Create and verify a disposable detached signature without storing a long-lived private key.
./mvnw, find, sha256sum, and
rm -rf only for the named disposable lab directory. On
Windows use mvnw.cmd, Get-ChildItem,
Get-FileHash -Algorithm SHA256, and
Remove-Item -Recurse on that same disposable directory.
For a Windows file-repository target, convert the directory to an
absolute file:///C:/...-style URI (for example with
PowerShell's [uri]$path/AbsoluteUri) and
pass it through the lab repository property instead of copying the
POSIX file:// form literally. Never substitute a normal
Maven cache, home directory, or shared repository path.
1. Create the disposable workspace
Start from the root of a
trusted wrapper-enabled disposable Maven project
created in Chapter 04. Copy the already-reviewed Wrapper scripts and
.mvn/ configuration into this new lab; do not download
or regenerate a different wrapper just for this exercise.
set -euo pipefail
mkdir -p ../maven-publish-lab/src/main/java/dev/academy/publish ../maven-publish-lab/{evidence,tools}
cp mvnw mvnw.cmd ../maven-publish-lab/
cp -R .mvn ../maven-publish-lab/
cd ../maven-publish-lab
./mvnw -v | tee evidence/maven-version.txt
java -version 2> evidence/java-version.txt
printf '%s
' 'Prediction A: package creates target JAR but does not install this project coordinate.' 'Prediction B: install adds the coordinate to the selected local repository.' 'Prediction C: deploy adds repository-layout files under .lab-remote/releases.' > evidence/predictions.txt
The copied wrapper must still identify Maven 3.9.16 and the previously reviewed Wrapper 3.3.4 configuration. If it does not, stop and repair the wrapper using Chapter 04 rather than accepting drift.
2. Author the project and pin the publication toolchain
The POM uses a final coordinate
dev.academy.publish:hello-publisher:1.0.0. Source and
Javadoc attachments are bound at verify, before
install/deploy. The file-backed release and snapshot repositories
are deliberately local and disposable.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy.publish</groupId>
<artifactId>hello-publisher</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputTimestamp>2026-08-24T00:00:00Z</project.build.outputTimestamp>
<lab.release.repo>file://${project.basedir}/.lab-remote/releases</lab.release.repo>
<lab.snapshot.repo>file://${project.basedir}/.lab-remote/snapshots</lab.snapshot.repo>
</properties>
<distributionManagement>
<repository>
<id>lab-releases</id>
<url>${lab.release.repo}</url>
</repository>
<snapshotRepository>
<id>lab-snapshots</id>
<url>${lab.snapshot.repo}</url>
</snapshotRepository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals><goal>jar-no-fork</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.12.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<phase>verify</phase>
<goals><goal>jar</goal></goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>3.1.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>3.1.4</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>release-sign</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.8</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals><goal>sign</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
package dev.academy.publish;
/** Small deterministic class used only by the publishing lab. */
public final class Greeting {
private Greeting() {}
/** Returns a stable greeting for artifact inspection and consumption. */
public static String message() {
return "hello-from-published-artifact";
}
}
3. Inspect the effective model before mutation
set -euo pipefail
./mvnw -Dmaven.repo.local=.model-m2 help:effective-pom -Dverbose > evidence/effective-pom.xml
./mvnw -Dmaven.repo.local=.model-m2 help:evaluate -Dexpression=project.version -q -DforceStdout | tee evidence/project-version.txt
grep -nE "maven-deploy-plugin|maven-install-plugin|distributionManagement|lab-releases" evidence/effective-pom.xml
The .model-m2 repository may receive plugin
metadata/artifacts needed for inspection. That is resolution state,
not proof that hello-publisher:1.0.0 has been
installed.
4. package: inspect the main JAR and prove what did not happen
set -euo pipefail
./mvnw -Dmaven.repo.local=.package-m2 clean package | tee evidence/01-package.log
jar tf target/hello-publisher-1.0.0.jar | tee evidence/02-jar-contents.txt
sha256sum target/hello-publisher-1.0.0.jar | tee evidence/03-package-sha256.txt
test -f target/hello-publisher-1.0.0.jar
if test -e .package-m2/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar; then
echo 'UNEXPECTED: project coordinate was installed during package' >&2
exit 2
fi
The local repository is not empty because Maven resolves its own build inputs there. The important negative proof is narrower: the current project’s coordinate is absent.
5. verify: attach sources and Javadocs
The project deliberately binds source:jar-no-fork and
javadoc:jar at verify. Running
verify therefore extends the artifact set without yet
requiring a remote publication.
set -euo pipefail
./mvnw -Dmaven.repo.local=.verify-m2 clean verify | tee evidence/04-verify.log
ls -1 target/*.jar | tee evidence/05-attached-artifacts.txt
sha256sum target/*.jar | tee evidence/06-attached-sha256.txt
Expected artifact set: hello-publisher-1.0.0.jar,
hello-publisher-1.0.0-sources.jar, and
hello-publisher-1.0.0-javadoc.jar. Their classifiers
let Maven publish them under the same project coordinate.
6. install: add the project coordinate to an isolated local repository
set -euo pipefail
./mvnw -Dmaven.repo.local=.install-m2 clean install | tee evidence/07-install.log
find .install-m2/dev/academy/publish/hello-publisher/1.0.0 -maxdepth 1 -type f -print | sort | tee evidence/08-installed-files.txt
The installed path should include the POM and each attached
artifact. This is local consumption state only; nothing has yet
crossed into .lab-remote.
7. deploy: publish to a disposable file repository
set -euo pipefail
rm -rf .deploy-m2 .lab-remote
./mvnw -Dmaven.repo.local=.deploy-m2 clean deploy | tee evidence/09-deploy.log
find .lab-remote/releases -type f -print | sort | tee evidence/10-published-files.txt
sha256sum .lab-remote/releases/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar | tee evidence/11-published-main-sha256.txt
sha256sum target/hello-publisher-1.0.0.jar | tee evidence/12-built-main-sha256.txt
cmp --silent target/hello-publisher-1.0.0.jar .lab-remote/releases/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar
deploy passes through install, so
.deploy-m2 contains the project coordinate as well. The
new trust transition is the repository-layout copy under
.lab-remote/releases.
8. Create real cryptographic signature evidence with a disposable JDK key
The mandatory synthetic path below uses JDK Ed25519. The private key exists only in the signing process and is not written to disk; the public key and detached signature are evidence files. This teaches signature verification without pretending it is the PGP trust model used by Maven Central-style release processes.
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public final class DetachedSigner {
public static void main(String[] args) throws Exception {
if (args.length != 4) throw new IllegalArgumentException("sign|verify artifact public-key signature");
Path artifact = Path.of(args[1]);
Path publicFile = Path.of(args[2]);
Path signatureFile = Path.of(args[3]);
byte[] data = Files.readAllBytes(artifact);
if ("sign".equals(args[0])) {
KeyPairGenerator generator = KeyPairGenerator.getInstance("Ed25519");
KeyPair pair = generator.generateKeyPair();
Signature signer = Signature.getInstance("Ed25519");
signer.initSign(pair.getPrivate());
signer.update(data);
Files.writeString(publicFile, Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()));
Files.writeString(signatureFile, Base64.getEncoder().encodeToString(signer.sign()));
System.out.println("SIGNATURE_CREATED");
} else if ("verify".equals(args[0])) {
byte[] publicBytes = Base64.getDecoder().decode(Files.readString(publicFile).trim());
PublicKey publicKey = KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(publicBytes));
Signature verifier = Signature.getInstance("Ed25519");
verifier.initVerify(publicKey);
verifier.update(data);
boolean ok = verifier.verify(Base64.getDecoder().decode(Files.readString(signatureFile).trim()));
System.out.println(ok ? "SIGNATURE_OK" : "SIGNATURE_BAD");
if (!ok) System.exit(2);
} else throw new IllegalArgumentException("first argument must be sign or verify");
}
}
set -euo pipefail
javac --release 17 -d tools tools/DetachedSigner.java
artifact=.lab-remote/releases/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar
java -cp tools DetachedSigner sign "$artifact" evidence/lab-public-key.b64 evidence/hello-publisher.sig.b64 | tee evidence/13-sign.txt
java -cp tools DetachedSigner verify "$artifact" evidence/lab-public-key.b64 evidence/hello-publisher.sig.b64 | tee evidence/14-signature-verify.txt
release-sign profile in this lesson is a configuration
example; do not activate it with a real organizational key in a
training workspace.
9. Consume from a clean isolated repository
A strong publication check uses a consumer whose local repository does not already contain the project coordinate. The repository in this consumer POM points only at the disposable file publication for project artifacts.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.academy.consumer</groupId>
<artifactId>clean-consumer</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>training-releases</id>
<url>file://${project.basedir}/../.lab-remote/releases</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.academy.publish</groupId>
<artifactId>hello-publisher</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build><plugins><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.15.0</version>
</plugin></plugins></build>
</project>
package dev.academy.consumer;
import dev.academy.publish.Greeting;
public final class Consumer {
public static void main(String[] args) {
System.out.println(Greeting.message());
}
}
set -euo pipefail
mkdir -p consumer/src/main/java/dev/academy/consumer
# Save the two files above, then:
rm -rf .consumer-m2
./mvnw -Dmaven.repo.local=.consumer-m2 -f consumer/pom.xml clean package | tee evidence/15-consumer-build.log
java -cp "consumer/target/classes:.consumer-m2/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar" dev.academy.consumer.Consumer | tee evidence/16-consumer-run.txt
On Windows use ; instead of : as the Java
classpath separator. The expected application output is
hello-from-published-artifact.
10. Challenge: choose the control, not the memorized command
Your CI built and tested 1.0.0. A later release job
receives the already-built JAR and needs to publish exactly those
bytes. Should it run mvn deploy and rebuild the
project, or should it promote/deploy the captured artifact with
preserved identity? Explain the risk before revealing the answer.
11. Cleanup only disposable state
cd ..
rm -rf maven-publish-lab
Knowledge check
Why does the package lab inspect a coordinate path instead of asserting the whole local repository is empty?
Because package still resolves Maven plugins and dependencies into the local repository. The precise claim is that the current project coordinate has not been installed.
Why bind source/Javadoc attachments before install?
Install and deploy should see the complete project artifact set so the attached classifiers travel with the main POM/artifact.
What does cmp prove in the file-repository
lab?
It proves the published main JAR bytes match the build output bytes for that run. It does not prove repository immutability or publisher identity.
Why is the Ed25519 lab signature called synthetic?
It teaches a real detached-signature mechanism with a disposable key, but it is not claiming to be the Maven GPG/PGP trust and key-distribution process used by a production release repository.
Why must the consumer use a fresh local repository?
Otherwise an earlier install could satisfy the coordinate and hide a broken or missing publication.
12. Bridge to configuration tradeoffs
You now have observable publication state. Lesson 3 asks which of these mechanisms should be used in a real team: reactor vs install, snapshot vs final release, POM-owned distribution management vs CI-injected destinations, and developer signing vs a controlled release job.
Official references and version notes
Version-sensitive statements were checked against Apache Maven primary documentation on 2026-08-24. The mandatory Maven path pins Maven 3.9.16 via Maven Wrapper 3.3.4, JDK 21 to run Maven, Java 17 as the compiler release target, Compiler Plugin 3.15.0, JAR Plugin 3.5.1, Install Plugin 3.1.4, Deploy Plugin 3.1.4, Source Plugin 3.4.0, Javadoc Plugin 3.12.0, Help Plugin 3.5.2, and GPG Plugin 3.2.8.
Maven 4 remains preview-stage in the current Apache download page, so this chapter does not silently switch publishing semantics to Maven 4.
- Apache Maven 3.9.16 — Download / current release
- Apache Maven Wrapper 3.3.4
- Maven Install Plugin 3.1.4
- Maven Deploy Plugin 3.1.4
- deploy:deploy parameters and alternative repository syntax
- Maven JAR Plugin 3.5.1
- Maven Source Plugin 3.4.0
- Maven Javadoc Plugin 3.12.0
- Maven GPG Plugin 3.2.8
- Maven Settings reference — servers and credential indirection
- Maven repository layout
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.