Maven Packaging, install, deploy, Distribution Management, Signing, and Repository Publishing: Diagnostics, Failure Modes, Security, and Performance
Diagnose publishing failures from preserved evidence: repository/server ID mismatches, leaked credentials, mutable release coordinates, unsafe signing-key custody, and stale local installs that hide published artifacts.
Publishing failures are dangerous when teams “fix” them by broadening credentials, retrying against a different repository, deleting caches, or overwriting the same release. This lesson uses an evidence-first sequence and disposable state to keep the original cause visible.
Learning objectives
- Diagnose repository/server ID mismatches without exposing real credentials.
- Recognize command-line and log credential leakage patterns.
- Prove why mutable release coordinates destroy content identity.
- Keep private signing material outside source and build artifacts.
- Detect stale local-install masking with a clean isolated consumer repository.
1. Diagnostic sequence: preserve evidence before mutation
- Capture the concise failing command, exit status, repository ID/URL, and relevant log lines.
- Confirm wrapper, Maven, and JDK identity.
- Inspect declared and effective distribution management plus active settings.
- Inspect the project coordinate and attached-artifact set.
- Inspect the selected local repository and publication filesystem separately.
- Inspect deploy/signing/plugin failures.
- Apply the least destructive correction.
- Repeat in controlled fresh state and compare hashes/metadata.
~/.m2 tree or changing global settings. Use a new
disposable -Dmaven.repo.local=... path so the
comparison itself becomes evidence.
2. Failure: repository ID and server credential ID do not match
The local fixture below requires HTTP Basic authentication and
supports the small PUT/GET surface needed for a deployment
simulation. The POM repository ID is lab-auth-repo,
while the intentionally broken settings file stores credentials
under wrong-server-id. Maven therefore has no matching
credentials for the deployment repository.
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();
}
}
<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>
<distributionManagement>
<repository>
<id>lab-auth-repo</id>
<url>http://127.0.0.1:8765/repository/releases</url>
</repository>
</distributionManagement>
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/repo-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=.diag-auth-m2 clean deploy > evidence/auth-failure.log 2>&1
status=$?
set -e
printf 'deploy exit=%s
' "$status" | tee evidence/auth-failure-exit.txt
Expected evidence is an authorization/deployment failure, not a
compiler failure. Repair only the settings ID so it becomes
lab-auth-repo, then rerun with a fresh
.diag-auth-fixed-m2. Do not move the lab password into
the POM or URL.
<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>lab-auth-repo</id>
<username>${env.MAVEN_LAB_REPO_USER}</username>
<password>${env.MAVEN_LAB_REPO_PASS}</password>
</server>
</servers>
</settings>
3. Failure: credentials leak through command line or logs
Commands that embed a token in a repository URL,
-Dpassword=..., or shell-expanded secret are visible to
shell history, process inspection, and CI logs. A redaction filter
is not a license to print secrets first. Keep credentials in the
secret/settings boundary and make diagnostic commands show IDs and
repository URLs without values.
4. Failure: same release coordinate, different bytes
A local file repository is intentionally weak enough to demonstrate
this. Publish 1.0.0, record its SHA-256, modify the
source, rebuild, and deploy 1.0.0 again. If the
repository accepts the overwrite, the coordinate stayed constant
while content identity changed.
set -euo pipefail
first=.diag-release-before.sha256
second=.diag-release-after.sha256
artifact=.diag-remote/dev/academy/publish/hello-publisher/1.0.0/hello-publisher-1.0.0.jar
./mvnw -Dmaven.repo.local=.diag-release-m2 clean deploy -DaltDeploymentRepository="diag::file://$PWD/.diag-remote"
sha256sum "$artifact" | tee "$first"
# Change Greeting.java only inside this disposable lab, then redeploy the SAME 1.0.0 coordinate.
./mvnw -Dmaven.repo.local=.diag-release-m2 clean deploy -DaltDeploymentRepository="diag::file://$PWD/.diag-remote"
sha256sum "$artifact" | tee "$second"
diff -u "$first" "$second" || true
The correct repair is not “accept the new hash.” Restore the intended source or increment the version, then enforce immutability in the real repository platform. The file target is a diagnostic fixture, not a release-policy model.
5. Failure: signing key becomes source or CI artifact
A private signing key copied into src/,
target/, an uploaded CI artifact, container layer, or
repository is no longer a trustworthy exclusive identity. Remove it
from the workflow and rotate/revoke according to your signing
infrastructure. A training key may be disposable, but production
keys require controlled storage and auditable access.
6. Failure: stale local install hides the publication
A consumer first checks its local repository. If an earlier local
install already satisfies
dev.academy.publish:hello-publisher:1.0.0, the build
can succeed even when the remote-style repository is missing or
wrong. Diagnose by comparing a fresh repository, not by deleting the
normal cache.
set -euo pipefail
# Stale path intentionally contains a prior install from the lab.
./mvnw -Dmaven.repo.local=.stale-m2 -f consumer/pom.xml package | tee evidence/stale-consumer.log
# New isolated path must fetch the project coordinate from the configured file repository.
rm -rf .fresh-consumer-m2
./mvnw -Dmaven.repo.local=.fresh-consumer-m2 -f consumer/pom.xml package | tee evidence/fresh-consumer.log
If stale succeeds and fresh fails, the first success was not
publication evidence. Inspect
.stale-m2/dev/academy/publish/... and the repository
target separately.
7. Failure interpretation for snapshots and metadata
Snapshot deployments add another layer: metadata maps the base
-SNAPSHOT request to timestamp/build-number instances.
A stale metadata file or consumer update policy can therefore
produce “I deployed it but did not receive it” symptoms. Preserve
maven-metadata.xml, compare repository timestamps, and
test with a new isolated consumer before changing global update
policies.
8. Performance: separate build work from publication work
Measure resolution, compilation/tests, Javadoc/source generation, signing, and upload separately. A warm local repository can make dependency resolution faster without making publication more correct. A remote retry can hide flaky transport while increasing release duration. Preserve timing around the stage that is actually slow rather than disabling quality/signing controls globally.
Knowledge check
Why does changing the settings server ID fix the authentication fixture?
Maven selects server credentials by matching the deployment repository ID to a settings server ID. Wrong ID means the intended credential entry is not selected.
What is the first response after a real token appears in logs?
Revoke or rotate the credential; do not assume log redaction retroactively restores secrecy.
What does “same release coordinate, different SHA-256” prove?
The publication target allowed mutable release identity. The fix is new version/controlled immutability, not accepting the overwritten hash.
Why use a fresh isolated local repository to diagnose stale install masking?
It removes only the lab’s prior project-coordinate state and proves whether the consumer can resolve from the publication target.
Should a private signing key be uploaded as a CI artifact for later stages?
No. That expands key custody and can expose release authority. Use a controlled signing boundary/agent or secret/key service.
9. Bridge to the checkpoint
The checkpoint combines all of these controls: exact coordinate, source revision, package/install/deploy side effects, attached artifacts, SHA-256 and detached signature evidence, clean consumer resolution, then a repository-ID authentication failure against the loopback fixture and verified cleanup.
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.