Checkpoint Lab — Maven Packaging, install, deploy, Distribution Management, Signing, and Repository Publishing
Publish a harmless Maven artifact to disposable repository state, prove exact coordinates and content identity, capture checksum/signature evidence, consume from a fresh local repository, and reproduce one authentication/ID failure safely.
This checkpoint is complete only when you can explain the identity of the source, build, artifact, repository coordinate, and consumer independently. You will publish one harmless final artifact, verify its bytes and signature evidence, consume it from a clean repository, and then reproduce an authentication/server-ID failure without using any real credential.
Learning objectives
- Record source revision, Maven/JDK identity, project version, and artifact checksum.
- Compare package, install, and deploy side effects with separate isolated repositories.
- Publish main/source/Javadoc artifacts to a disposable repository and verify layout/metadata.
- Generate and verify detached signature evidence with a disposable key.
- Consume from clean state and diagnose a loopback repository/server-ID authentication failure.
./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. Checkpoint acceptance contract
| Invariant | Required evidence |
|---|---|
| Source identity is recorded | Local Git commit SHA plus Maven/JDK version evidence. |
| Project identity is exact |
dev.academy.publish:hello-publisher:1.0.0
captured from Maven model.
|
| Package does not install this project | Main target JAR exists; project coordinate absent from package-only repository. |
| Install adds local project coordinate | POM/main/source/Javadoc artifacts visible under isolated local repository. |
| Deploy publishes same main bytes | File-repository JAR hash equals build-output JAR hash. |
| Signature evidence verifies | Synthetic Ed25519 detached signature verifies against published bytes. |
| Clean consumer resolves publication | Fresh consumer repository obtains coordinate from disposable release repository. |
| Wrong server ID fails safely | Loopback authenticated target returns a Maven deployment failure until server ID is corrected. |
| Cleanup is scoped | Only checkpoint directories and loopback process are removed. |
2. Setup, preflight, and source commit
Run this from a trusted wrapper-enabled disposable Maven project whose Wrapper configuration was already reviewed in Chapter 04. The checkpoint copies that wrapper unchanged so the build-tool identity is part of the evidence instead of an unreviewed download.
set -euo pipefail
mkdir -p ../maven-publish-checkpoint/src/main/java/dev/academy/publish ../maven-publish-checkpoint/{evidence,tools}
cp mvnw mvnw.cmd ../maven-publish-checkpoint/
cp -R .mvn ../maven-publish-checkpoint/
cd ../maven-publish-checkpoint
./mvnw -v | tee evidence/00-maven-version.txt
java -version 2> evidence/00-java-version.txt
javac -version 2> evidence/00-javac-version.txt || javac -version | tee evidence/00-javac-version.txt
git init -q
git config user.name "DevOps Academy Lab"
git config user.email "lab@example.invalid"
# Save pom.xml and Greeting.java from this lesson before committing.
git add pom.xml src
git commit -qm "checkpoint source"
git rev-parse HEAD | tee evidence/01-source-commit.txt
./mvnw -Dmaven.repo.local=.model-m2 help:evaluate -Dexpression=project.version -q -DforceStdout | tee evidence/02-project-version.txt
The Git identity is local to the disposable repository and uses a reserved invalid email domain. It does not touch global Git configuration. The Wrapper must still report Maven 3.9.16; stop if the copied wrapper has drifted.
3. Project, consumer, and verification tools
<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";
}
}
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");
}
}
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Base64;
public final class RepoServer {
public static void main(String[] args) throws Exception {
Path root = Path.of(args.length > 0 ? args[0] : "repo-store").toAbsolutePath().normalize();
int port = args.length > 1 ? Integer.parseInt(args[1]) : 8765;
String user = System.getenv().getOrDefault("MAVEN_LAB_REPO_USER", "lab-user");
String pass = System.getenv().getOrDefault("MAVEN_LAB_REPO_PASS", "lab-pass");
String wanted = "Basic " + Base64.getEncoder().encodeToString((user + ":" + pass).getBytes(StandardCharsets.UTF_8));
Files.createDirectories(root);
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0);
server.createContext("/repository/releases", ex -> handle(ex, root, wanted));
server.start();
System.out.println("READY http://127.0.0.1:" + port + "/repository/releases");
}
private static void handle(HttpExchange ex, Path root, String wanted) throws IOException {
String auth = ex.getRequestHeaders().getFirst("Authorization");
if (!wanted.equals(auth)) {
ex.getResponseHeaders().add("WWW-Authenticate", "Basic realm=lab");
ex.sendResponseHeaders(401, -1);
ex.close();
return;
}
String suffix = ex.getRequestURI().getPath().substring("/repository/releases".length());
while (suffix.startsWith("/")) suffix = suffix.substring(1);
Path target = root.resolve(suffix).normalize();
if (!target.startsWith(root)) {
ex.sendResponseHeaders(400, -1); ex.close(); return;
}
String method = ex.getRequestMethod();
if ("PUT".equals(method)) {
Files.createDirectories(target.getParent());
Files.copy(ex.getRequestBody(), target, StandardCopyOption.REPLACE_EXISTING);
ex.sendResponseHeaders(201, -1);
} else if ("GET".equals(method) || "HEAD".equals(method)) {
if (!Files.isRegularFile(target)) { ex.sendResponseHeaders(404, -1); }
else {
long size = Files.size(target);
ex.sendResponseHeaders(200, "HEAD".equals(method) ? -1 : size);
if ("GET".equals(method)) Files.copy(target, ex.getResponseBody());
}
} else if ("OPTIONS".equals(method)) {
ex.sendResponseHeaders(200, -1);
} else {
ex.sendResponseHeaders(405, -1);
}
ex.close();
}
}
4. Predict before execution
Write these predictions into
evidence/predictions.txt before running Maven:
-
packagewill create the main JAR but the project coordinate will be absent from.package-m2/dev/academy/publish/.... -
installwill add the project coordinate to.install-m2, including attached sources/Javadoc because their executions occur atverify. -
deploywill add repository-layout files under.lab-remote/releasesand the published main JAR hash will equal the build-output JAR hash from that run. -
A fresh consumer repository will resolve the project from
.lab-remote/releases; a stale repository is not acceptable evidence.
5. Compare package and install side effects
set -euo pipefail
rm -rf .package-m2 target
./mvnw -Dmaven.repo.local=.package-m2 clean package | tee evidence/03-package.log
sha256sum target/hello-publisher-1.0.0.jar | tee evidence/04-package-sha256.txt
if test -e .package-m2/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar; then
echo 'FAIL: package unexpectedly installed project coordinate' >&2; exit 3
fi
set -euo pipefail
rm -rf .install-m2
./mvnw -Dmaven.repo.local=.install-m2 clean install | tee evidence/05-install.log
find .install-m2/dev/academy/publish/hello-publisher/1.0.0 -maxdepth 1 -type f -print | sort | tee evidence/06-installed-files.txt
Do not compare total repository sizes: plugin/dependency resolution differs between runs. Compare the specific project-coordinate path and attached classifiers.
6. Deploy to the disposable release repository and prove byte identity
set -euo pipefail
rm -rf .deploy-m2 .lab-remote target
./mvnw -Dmaven.repo.local=.deploy-m2 clean deploy | tee evidence/07-deploy.log
published=.lab-remote/releases/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar
sha256sum target/hello-publisher-1.0.0.jar | tee evidence/08-built-sha256.txt
sha256sum "$published" | tee evidence/09-published-sha256.txt
cmp --silent target/hello-publisher-1.0.0.jar "$published"
find .lab-remote/releases/dev/academy/publish/hello-publisher -type f -print | sort | tee evidence/10-publication-layout.txt
Also inspect the POM plus -sources and
-javadoc artifacts in the release directory. Maven may
create checksum/metadata sidecars according to repository transport
behavior; your independently recorded SHA-256 remains the
checkpoint’s explicit byte-identity evidence.
7. Generate detached signature evidence without persisting a private key
set -euo pipefail
javac --release 17 -d tools tools/DetachedSigner.java
published=.lab-remote/releases/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar
java -cp tools DetachedSigner sign "$published" evidence/public-key.b64 evidence/published.sig.b64 | tee evidence/11-sign.txt
java -cp tools DetachedSigner verify "$published" evidence/public-key.b64 evidence/published.sig.b64 | tee evidence/12-signature-verification.txt
grep -qx 'SIGNATURE_OK' evidence/12-signature-verification.txt
The generated private key is process-local and discarded. The checkpoint therefore demonstrates cryptographic verification without creating a reusable release identity. Production signing would use Maven GPG Plugin 3.2.8 or an organization-approved signing service/agent with controlled key custody.
8. Consume from a clean isolated repository
<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 consumer POM/source above.
rm -rf .consumer-m2
./mvnw -Dmaven.repo.local=.consumer-m2 -f consumer/pom.xml clean package | tee evidence/13-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/14-consumer-run.txt
grep -qx 'hello-from-published-artifact' evidence/14-consumer-run.txt
9. Simulate and repair one authentication/repository-ID failure
This failure uses only loopback networking and lab-only values.
First replace the project’s release
distributionManagement with the loopback fragment below
and save the intentionally wrong settings file.
<distributionManagement>
<repository>
<id>lab-auth-repo</id>
<url>http://127.0.0.1:8765/repository/releases</url>
</repository>
</distributionManagement>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
<servers>
<server>
<id>wrong-server-id</id>
<username>${env.MAVEN_LAB_REPO_USER}</username>
<password>${env.MAVEN_LAB_REPO_PASS}</password>
</server>
</servers>
</settings>
set -euo pipefail
javac --release 17 -d tools tools/RepoServer.java
export MAVEN_LAB_REPO_USER=lab-user
export MAVEN_LAB_REPO_PASS=lab-pass
java -cp tools RepoServer .auth-repo 8765 > evidence/15-auth-server.log 2>&1 &
server_pid=$!
trap 'kill "$server_pid" 2>/dev/null || true' EXIT
sleep 1
set +e
./mvnw -s .lab-settings.xml -Dmaven.repo.local=.auth-fail-m2 clean deploy > evidence/16-auth-failure.log 2>&1
status=$?
set -e
printf 'wrong-id exit=%s
' "$status" | tee evidence/17-auth-failure-exit.txt
test "$status" -ne 0
# Change ONLY wrong-server-id -> lab-auth-repo in .lab-settings.xml.
rm -rf .auth-fixed-m2
./mvnw -s .lab-settings.xml -Dmaven.repo.local=.auth-fixed-m2 clean deploy | tee evidence/18-auth-fixed.log
find .auth-repo -type f -print | sort | tee evidence/19-auth-repo-layout.txt
lab-user/lab-pass exist only
inside the disposable loopback exercise. They are not examples of
production secrets. Do not copy real repository credentials into the
POM, URL, evidence directory, or Git history.
10. Verification checklist
- Source commit, Maven/JDK identity, and project version are recorded.
- Package produced a main JAR and did not install this project coordinate.
- Install wrote the project POM/main/source/Javadoc artifacts under the isolated coordinate path.
- Deploy wrote the same main JAR bytes to the disposable release repository.
- SHA-256 evidence for build and publication matches.
-
Detached Ed25519 signature verification prints
SIGNATURE_OK. - A fresh consumer repository resolves and runs the published artifact.
- The intentionally wrong settings server ID produces a non-zero deployment failure.
- Changing only the server ID restores authenticated loopback publication.
- No normal Maven cache, global settings, production repository, real credential, or durable signing key is touched.
11. Cleanup and rollback
kill "$server_pid" 2>/dev/null || true
trap - EXIT
cd ..
rm -rf maven-publish-checkpoint
If evidence must be retained for a training review, copy only the
evidence/ directory after checking that it contains no
credential value. Delete all disposable publication repositories and
synthetic key/signature tools afterward.
Knowledge check
Why does the checkpoint record both source commit and artifact SHA-256?
The commit identifies source state; the digest identifies the actual published bytes. One does not substitute for the other.
What observable side effect distinguishes install from package?
The current project coordinate appears under the selected local Maven repository path, including its POM/artifacts.
Why is a fresh consumer repository mandatory evidence?
It prevents a prior local install from satisfying the coordinate and hiding a broken/missing publication.
Why does the wrong server ID produce an authentication failure even though credentials exist in settings?
The credentials are stored under a different identity key, so Maven does not select them for the deployment repository ID.
What is missing from the synthetic Ed25519 signature before it can represent organizational release trust?
A governed long-lived signing identity, protected private-key custody, trusted public-key distribution, and release-policy/provenance linkage.
What production capability does Chapter 12 add?
An auditable artifact-promotion boundary: exact coordinates, attached artifacts, local vs published state, credential indirection, byte identity, signing evidence, clean consumption, and immutable-release reasoning.
12. Production operating model and Chapter 13 bridge
Chapter 12 adds controlled artifact publication to the build-engineering operating model. A team can now prove which source and toolchain produced which bytes, where those bytes were installed or deployed, and which credential/signing boundary authorized publication. Chapter 13 builds on that evidence to study Maven performance, parallel builds, daemon options, reproducible builds, and systematic troubleshooting without sacrificing artifact identity.
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.
The checkpoint intentionally uses local file and loopback HTTP repositories. Repository-manager staging, retention, access administration, and enterprise promotion workflows belong to the dedicated artifact-repository/platform courses.
- 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.