Chapter 12Lesson 04~175 minutes

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.

DiagnosticsRepository IDCredential SafetyImmutabilityStale Install

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.
Current baseline — verified 2026-08-24. Labs use Maven 3.9.16 via Maven Wrapper 3.3.4, JDK 21, Java 17 target, Compiler 3.15.0, JAR 3.5.1, Install/Deploy 3.1.4, Source 3.4.0, Javadoc 3.12.0, Help 3.5.2, and GPG 3.2.8. Publication targets and local Maven repositories are disposable project-relative directories. No real repository, signing key, global settings file, production CI secret, or normal user cache is modified.

1. Diagnostic sequence: preserve evidence before mutation

  1. Capture the concise failing command, exit status, repository ID/URL, and relevant log lines.
  2. Confirm wrapper, Maven, and JDK identity.
  3. Inspect declared and effective distribution management plus active settings.
  4. Inspect the project coordinate and attached-artifact set.
  5. Inspect the selected local repository and publication filesystem separately.
  6. Inspect deploy/signing/plugin failures.
  7. Apply the least destructive correction.
  8. Repeat in controlled fresh state and compare hashes/metadata.
Do not respond to a publishing problem by deleting the normal ~/.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.

Incident response: if a real credential has appeared in a log or Git history, revoke/rotate it first. Editing the visible log later does not make the credential trustworthy again.

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?

What is the first response after a real token appears in logs?

What does “same release coordinate, different SHA-256” prove?

Why use a fresh isolated local repository to diagnose stale install masking?

Should a private signing key be uploaded as a CI artifact for later stages?

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.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.