Chapter 01Lesson 05~120 minutes

Checkpoint Lab — Build Automation Foundations, Reproducibility, Build Graphs, and the JVM Toolchain Ecosystem

Complete an evidence-driven checkpoint: build the same tiny JVM behavior with Maven and Gradle, record graph/tool/input identities, compare clean and repeat runs, perturb declared and undeclared inputs, and document what the experiment does—and does not—prove.

Checkpoint LabEvidenceChecksumsReproducibilityVerification

Learning objectives

  • Execute controlled Maven and Gradle verification paths in disposable, isolated local state.
  • Record JDK, wrapper/build-tool, source, dependency/model, test, artifact, and checksum evidence before comparing outcomes.
  • Predict and verify at least two model, graph, cache, or artifact changes before performing them.
  • Distinguish a declared source input change from an unrelated environment change and explain their expected effects.
  • Produce a concise reproducibility report with assumptions, evidence, limitations, and cleanup rather than declaring success from a green build alone.
Version baseline — verified 2026-08-23. The examples use JDK 21, Apache Maven 3.9.16, Apache Maven Wrapper 3.3.4, and Gradle 9.7.1. Maven 3.9.16 is the current recommended Maven 3 release; Maven 4.0.0-rc-6 is still a preview and is intentionally not the production baseline here. Gradle 9 requires JVM 17 or newer to run, so JDK 21 gives both tools a common supported runtime. The versions are teaching pins, not timeless “latest” claims.

1. Checkpoint scenario and acceptance contract

You are handing a tiny JVM component to a CI platform team. They do not care which laptop created the first JAR. They need an explainable build contract: exact tool identities, declared inputs, test evidence, package identity, what is reusable, what is generated, and which assumptions remain outside the build model.

REQUIRED EVIDENCE
[ ] JDK/JVM identity recorded
[ ] Maven wrapper reports 3.9.16
[ ] Gradle wrapper reports 9.7.1
[ ] source SHA-256 values recorded
[ ] Maven effective model/dependency tree inspected
[ ] Gradle projects/tasks/dry-run inspected
[ ] two tests pass in each project
[ ] JAR contents inspected
[ ] artifact SHA-256 recorded for clean and repeat runs
[ ] repeat-run behavior interpreted, not merely observed
[ ] one declared source input changed and consequences predicted/verified
[ ] one unrelated environment variable changed and consequences predicted/verified
[ ] limitations of the reproducibility claim documented
[ ] disposable lab state cleaned without touching normal user caches

A checkpoint passes when the evidence is internally consistent and the learner can explain causality. Matching artifact digests are useful, but they do not replace test evidence or prove that every external input has been controlled.

2. Setup and preflight

Reuse the two disposable projects from Lesson 2, or recreate them with the exact POM, Gradle files, Greeting.java, and GreetingTest.java shown there. Do not run this lab in a valuable repository. Keep the structure below.

build-foundations-lab/
├── .lab-state/
├── maven-app/
│   ├── .mvn/wrapper/...
│   ├── mvnw
│   ├── mvnw.cmd
│   ├── pom.xml
│   └── src/{main,test}/java/com/example/greeting/...
└── gradle-app/
    ├── gradle/wrapper/...
    ├── gradlew
    ├── gradlew.bat
    ├── settings.gradle.kts
    ├── build.gradle.kts
    └── src/{main,test}/java/com/example/greeting/...
cd build-foundations-lab
test -f maven-app/mvnw
test -f gradle-app/gradlew
java -version
maven-app/mvnw -v
gradle-app/gradlew --version

If a wrapper downloads a distribution on first use, treat that as a resolution event. Confirm the configured distribution URL and expected version before trusting the downloaded executable. Do not replace a wrapper property with an unreviewed mirror simply to bypass a network failure.

3. Record the input and version inventory

Create a small evidence directory outside both generated-output directories. The report should survive clean but remain inside the disposable lab.

mkdir -p evidence

{
  echo "=== Java ==="
  java -version 2>&1
  echo
  echo "=== Maven wrapper ==="
  (cd maven-app && ./mvnw -v)
  echo
  echo "=== Gradle wrapper ==="
  (cd gradle-app && ./gradlew --version)
} > evidence/tool-versions.txt

find maven-app/src gradle-app/src -type f -print0 \
  | sort -z \
  | xargs -0 sha256sum > evidence/source-before.sha256

cat evidence/tool-versions.txt
cat evidence/source-before.sha256

The source hashes provide exact identity for the lab’s authored Java files. They do not hash the POM, Gradle scripts, wrappers, dependencies, or JDK; those need their own evidence. This is why a reproducibility report should list its scope rather than saying “everything is reproducible.”

4. Prediction 1 — what should a clean build change?

Before executing anything, write down your prediction:

  • Maven clean verify should recreate target/, run tests, and produce a Maven JAR.
  • Gradle clean build should recreate build/, execute the required Java/test/package task graph, and produce a Gradle JAR.
  • The isolated dependency/cache directories may populate with downloaded artifacts.
  • Neither build should modify the Java source files.

That is your first required model/graph prediction. The next steps test it.

5. Maven controlled build and evidence

cd maven-app
./mvnw -Dmaven.repo.local="$PWD/../.lab-state/m2" clean verify \
  | tee ../evidence/maven-clean.log

jar tf target/greeting-maven-1.0.0.jar \
  > ../evidence/maven-jar-contents.txt
sha256sum target/greeting-maven-1.0.0.jar \
  > ../evidence/maven-clean.sha256
cp target/surefire-reports/*.txt ../evidence/
cd ..

Verify that the log shows the expected lifecycle reaching verify, the test report records two tests with zero failures/errors, and the JAR contains Greeting.class. Do not accept the existence of the JAR as a substitute for the test result.

6. Gradle controlled build and evidence

cd gradle-app
GRADLE_USER_HOME="$PWD/../.lab-state/gradle" \
  ./gradlew clean build --console=plain \
  | tee ../evidence/gradle-clean.log

jar tf build/libs/greeting-gradle-1.0.0.jar \
  > ../evidence/gradle-jar-contents.txt
sha256sum build/libs/greeting-gradle-1.0.0.jar \
  > ../evidence/gradle-clean.sha256
cp build/test-results/test/TEST-*.xml ../evidence/
cd ..

Verify that test succeeds, the XML report contains the expected tests, and the JAR contains the application class. The Gradle console should show which tasks executed. This is execution-graph evidence; it is not interchangeable with Maven lifecycle logging.

7. Prediction 2 — what should a repeat build do?

Before repeating the build, predict the difference:

  • Maven should reuse already downloaded dependencies from the isolated local repository but normally execute the selected lifecycle/plugin work again.
  • Gradle should recognize unchanged declared task inputs/outputs and mark eligible work UP-TO-DATE (or another non-executed outcome such as NO-SOURCE).
  • For each tool, if archive inputs and controlled metadata are stable, its repeated JAR checksum should remain the same.

Do not predict that the Maven JAR digest must equal the Gradle JAR digest. They are different build implementations and can create different archive metadata while packaging equivalent class behavior.

8. Run again without clean and compare

cd maven-app
./mvnw -Dmaven.repo.local="$PWD/../.lab-state/m2" verify \
  | tee ../evidence/maven-repeat.log
sha256sum target/greeting-maven-1.0.0.jar \
  > ../evidence/maven-repeat.sha256
cd ../gradle-app
GRADLE_USER_HOME="$PWD/../.lab-state/gradle" \
  ./gradlew build --console=plain \
  | tee ../evidence/gradle-repeat.log
sha256sum build/libs/greeting-gradle-1.0.0.jar \
  > ../evidence/gradle-repeat.sha256
cd ..

diff -u evidence/maven-clean.sha256 evidence/maven-repeat.sha256 || true
diff -u evidence/gradle-clean.sha256 evidence/gradle-repeat.sha256 || true
grep -E "UP-TO-DATE|FROM-CACHE|NO-SOURCE" evidence/gradle-repeat.log || true

If a checksum changes unexpectedly, inspect the archive and build configuration rather than declaring the tool non-reproducible. If Gradle executes a task you predicted would be up-to-date, inspect which input or output changed. The value of the exercise is the explanation.

9. Declared-input perturbation: change the Java source

Now change a declared input in both projects. Replace the greeting prefix from Hello to Welcome, and update the corresponding test expectation. Before running, predict:

  • source hashes will change;
  • compilation and tests must no longer be reusable as unchanged work;
  • the packaged artifact identity should change;
  • dependency versions should not change merely because application source changed.
return "Welcome, " + name.trim() + "!";
assertEquals("Welcome, DevOps!", Greeting.message(" DevOps "));
find maven-app/src gradle-app/src -type f -print0 \
  | sort -z \
  | xargs -0 sha256sum > evidence/source-after-declared-change.sha256

(cd maven-app && ./mvnw -Dmaven.repo.local="$PWD/../.lab-state/m2" verify)
(cd gradle-app && GRADLE_USER_HOME="$PWD/../.lab-state/gradle" ./gradlew build --console=plain)

sha256sum maven-app/target/greeting-maven-1.0.0.jar \
  > evidence/maven-declared-change.sha256
sha256sum gradle-app/build/libs/greeting-gradle-1.0.0.jar \
  > evidence/gradle-declared-change.sha256

Compare the new source and artifact hashes with the earlier evidence. This is the simplest causal chain in the chapter: a declared source/test input changed, the relevant execution work changed, and the package identity changed.

10. Unrelated environmental perturbation: change a value the build does not read

Set an environment variable named ACADEMY_NOISE. Neither POM nor Gradle build reads this variable. Predict that it should not change source hashes, test behavior, or artifact bytes.

export ACADEMY_NOISE="first"
(cd maven-app && ./mvnw -Dmaven.repo.local="$PWD/../.lab-state/m2" verify)
(cd gradle-app && GRADLE_USER_HOME="$PWD/../.lab-state/gradle" ./gradlew build --console=plain)

sha256sum maven-app/target/greeting-maven-1.0.0.jar
sha256sum gradle-app/build/libs/greeting-gradle-1.0.0.jar

export ACADEMY_NOISE="second"
(cd maven-app && ./mvnw -Dmaven.repo.local="$PWD/../.lab-state/m2" verify)
(cd gradle-app && GRADLE_USER_HOME="$PWD/../.lab-state/gradle" ./gradlew build --console=plain)

sha256sum maven-app/target/greeting-maven-1.0.0.jar
sha256sum gradle-app/build/libs/greeting-gradle-1.0.0.jar

The variable is environmental state, but it is not an input to these builds because nothing reads it. This distinction matters: “environment variable exists” is not equivalent to “environment variable affects output.” Lesson 4 showed the dangerous opposite case—a task that reads an environment value but fails to declare it.

11. Record model and graph evidence

A checksum-only report cannot explain why the build ran. Add read-only model evidence:

cd maven-app
./mvnw -Dmaven.repo.local="$PWD/../.lab-state/m2" \
  help:effective-pom > ../evidence/maven-effective-pom.xml
./mvnw -Dmaven.repo.local="$PWD/../.lab-state/m2" \
  dependency:tree > ../evidence/maven-dependency-tree.txt
cd ..
cd gradle-app
GRADLE_USER_HOME="$PWD/../.lab-state/gradle" \
  ./gradlew projects > ../evidence/gradle-projects.txt
GRADLE_USER_HOME="$PWD/../.lab-state/gradle" \
  ./gradlew tasks > ../evidence/gradle-tasks.txt
GRADLE_USER_HOME="$PWD/../.lab-state/gradle" \
  ./gradlew build --dry-run > ../evidence/gradle-build-dry-run.txt
cd ..

The effective POM reveals inherited/defaulted Maven model state. The dependency tree records the resolved dependency relationship. Gradle project/task/dry-run views document the model and selected task path. None of these files should contain real credentials; review before sharing evidence from a non-lab project.

12. Write a reproducibility report with explicit limits

# Chapter 01 reproducibility checkpoint

## Controlled
- source/test files and their SHA-256 identities
- project build files
- Maven wrapper: 3.9.16
- Gradle wrapper: 9.7.1
- lab JVM/JDK: 21
- pinned JUnit/plugin versions in the example
- Maven local repository isolated to .lab-state/m2
- Gradle User Home isolated to .lab-state/gradle
- fixed archive timestamp intent in Maven; reproducible archive settings in Gradle

## Verified
- Maven tests pass and report files exist
- Gradle tests pass and report files exist
- both JARs contain Greeting.class
- clean/repeat artifact identities recorded
- declared source change changes expected work/artifact identity
- unrelated ACADEMY_NOISE variable does not affect this build

## Not proven by this lab
- rebuild on a second operating system or CPU architecture
- long-term immutability of every remote dependency/plugin repository
- organization repository-manager policy
- CI runner hardening or secret management
- publication/signing/provenance
- equivalence of Maven-produced and Gradle-produced JAR bytes

## Production next steps
- commit and verify wrapper integrity metadata
- enforce approved repositories and dependency governance
- pin/lock/verify dependencies according to tool policy
- execute the same wrapper entry point on ephemeral CI
- record exact artifact identity and promote the same bytes

A good report makes the negative space visible. “Not proven” is not failure; it prevents a small local experiment from being misrepresented as full supply-chain assurance.

13. Hash the evidence package

Hashing evidence does not make its claims true. It makes the reviewed evidence set identifiable so later changes are detectable.

find evidence -type f ! -name evidence-manifest.sha256 -print0 \
  | sort -z \
  | xargs -0 sha256sum > evidence/evidence-manifest.sha256

cat evidence/evidence-manifest.sha256

14. Windows notes for the checkpoint

On PowerShell use mvnw.cmd, gradlew.bat, Get-FileHash, and a process-scoped GRADLE_USER_HOME. Do not translate POSIX environment syntax literally.

$env:GRADLE_USER_HOME = "$PWD\.lab-state\gradle"

Set-Location maven-app
.\mvnw.cmd "-Dmaven.repo.local=$PWD\..\.lab-state\m2" clean verify
Get-FileHash .\target\greeting-maven-1.0.0.jar -Algorithm SHA256
Set-Location ..

Set-Location gradle-app
.\gradlew.bat clean build --console=plain
Get-FileHash .\build\libs\greeting-gradle-1.0.0.jar -Algorithm SHA256
Set-Location ..

15. Verification checklist and cleanup

Check Pass condition
Tool identity Wrapper Maven 3.9.16; wrapper Gradle 9.7.1; expected JDK 21 baseline
Model evidence Maven effective POM/dependency tree and Gradle project/task/dry-run evidence retained
Tests Two tests pass in each tool’s report
Artifacts Each JAR inspected and SHA-256 recorded
Repeat run Observed Maven/Gradle reuse behavior explained
Declared perturbation Source/test hashes and relevant work/artifact identity change as predicted
Environmental perturbation ACADEMY_NOISE does not change output because the build does not read it
Safety No real secrets/private endpoints; only disposable local state
Limitations Report distinguishes verified claims from untested production boundaries
pwd
find . -maxdepth 2 -type d | sort

# Keep evidence if you want to review it.
# When finished, move one directory up and verify the target:
# cd ..
# test "$(basename "$PWD")" != "build-foundations-lab"
# rm -rf build-foundations-lab
Destructive boundary: cleanup must never target your normal ~/.m2, ~/.gradle, a production repository checkout, shared artifact repository, or CI cache. Remove only the verified disposable lab directory.

Knowledge check

The Maven and Gradle JARs have different SHA-256 values. Did the checkpoint fail?

The Gradle repeat build executes compileJava even though you did not edit Java source. What should you do first?

You changed ACADEMY_NOISE and the artifact changed. What does that imply?

Why does a matching artifact checksum after two warm runs not prove full reproducibility?

A teammate proposes deleting ~/.m2 and ~/.gradle to make the checkpoint cleaner. What is the safer method?

What is the strongest Chapter 01 handoff statement?

Summary

  • You treated the build as evidence-producing automation, not a single success message.
  • You recorded JDK, Maven wrapper, Gradle wrapper, source, model, graph, test, JAR-content, and SHA-256 evidence.
  • You predicted and verified the effects of a clean build, a repeat build, a declared source change, and an unrelated environmental change.
  • You kept dependency/cache state isolated and avoided destructive “delete all caches” troubleshooting.
  • You documented the boundaries the local experiment did not prove, which is essential for honest production readiness.

This completes Chapter 01’s build-engineering mental model. The key operating principle is now explicit: before optimizing, publishing, or automating a build, be able to name its inputs, execution model, outputs, versions, trust boundaries, and evidence.

Chapter 02

Java and JVM Project Structure, Source Sets, Compilation, Testing, Packaging, and Toolchains

Chapter 02 goes deeper into the Java/JVM structures that Chapter 01 treated as controlled inputs: source sets, compiler behavior, test layouts, package structure, runtime versus compile targets, and toolchain selection across Maven and Gradle.

Primary sources and version notes

This checkpoint was authored against the official Maven/Gradle documentation available on 2026-08-23. Re-check current versions and compatibility before using these pins in a real project.

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.