Checkpoint Lab — CI/CD Build Patterns, Caching, Test Sharding, Artifact Promotion, and Ephemeral Build Agents
Design and exercise a small Gradle CI build from a clean agent, record tool and artifact identity, validate complete shard coverage, simulate a poisoned cache and a failed shard plan, then promote exactly the artifact that passed verification.
Learning objectives
- Design a complete local simulation of a clean Gradle CI build using a verified Wrapper.
- Record source, Wrapper/Gradle, JDK, test reports, and artifact checksum as build evidence.
- Validate shard coverage independently and fail on a missing shard assignment.
- Simulate poisoned/stale cache content and prove the restore policy ignores project build outputs.
- Promote the exact previously built JAR and verify byte identity without rebuilding.
Checkpoint assumptions. Gradle 9.7.1 Wrapper already verified, JDK 21, Java release 17, JUnit 6.1.3. If Gradle dependencies are not already cached, the first build needs ordinary access to Maven Central. No hosted CI/repository or paid service is required.
1. Scenario and acceptance contract
You are building dev.academy.ci:ci-demo:1.0.0. A
disposable “agent” must start from a clean project copy, run two
deterministic shards, preserve XML reports, package one reproducible
JAR, and export its checksum. A later “promotion agent” must copy
the exact artifact without invoking Gradle. The exercise also
injects a stale output into a fake cache and a missing shard
assignment; both must be detected without deleting normal user
state.
| Prediction | Expected observable result |
|---|---|
| A stale JAR placed in the fake cache should not become release input. |
Restore policy ignores build/; only the current
build JAR is exported.
|
Removing ShardBTest from shard manifest should
fail validation.
|
Manifest comparison exits nonzero before release gate. |
| Promotion should not change artifact SHA-256. | Build handoff hash equals release-repository hash. |
| A fresh agent may reuse approved dependency state but not workspace outputs. |
New build/ starts absent while selected Gradle
User Home state may be restored.
|
2. Set up the project and verified Wrapper
Create ch29-checkpoint/template. Copy the already
verified Gradle 9.7.1 Wrapper files into that template, then add the
same project model used in Lesson 2:
plugins {
java
}
group = "dev.academy.ci"
version = "1.0.0"
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
tasks.withType<JavaCompile>().configureEach {
options.release.set(17)
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:6.1.3")
}
tasks.test {
useJUnitPlatform()
}
val testSourceSet = sourceSets.named("test")
fun registerShard(taskName: String, pattern: String, reportName: String) =
tasks.register<Test>(taskName) {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Runs deterministic CI test shard $reportName"
testClassesDirs = testSourceSet.get().output.classesDirs
classpath = testSourceSet.get().runtimeClasspath
useJUnitPlatform()
filter {
includeTestsMatching(pattern)
isFailOnNoMatchingTests = true
}
reports.junitXml.outputLocation.set(layout.buildDirectory.dir("test-results/$reportName"))
reports.html.outputLocation.set(layout.buildDirectory.dir("reports/tests/$reportName"))
}
val shardA = registerShard("testShardA", "*ShardATest", "shard-a")
val shardB = registerShard("testShardB", "*ShardBTest", "shard-b")
tasks.register("ciTest") {
group = LifecycleBasePlugin.VERIFICATION_GROUP
description = "Runs every declared CI shard"
dependsOn(shardA, shardB)
}
tasks.jar {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
package dev.academy.ci;
public final class Calculator {
private Calculator() {}
public static int add(int a, int b) { return a + b; }
public static int multiply(int a, int b) { return a * b; }
}
package dev.academy.ci;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ShardATest {
@Test void adds() { assertEquals(7, Calculator.add(3, 4)); }
}
package dev.academy.ci;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class ShardBTest {
@Test void multiplies() { assertEquals(12, Calculator.multiply(3, 4)); }
}
# shard-manifest/expected.txt
ShardATest
ShardBTest
# shard-manifest/shard-a.txt
ShardATest
# shard-manifest/shard-b.txt
ShardBTest
3. Preflight: do not proceed with an unknown toolchain
From the template directory:
set -euo pipefail
test -x ./gradlew || {
echo "Verified Gradle Wrapper is required. Copy the Chapter 15 wrapper fixture first." >&2
exit 2
}
java -version
./gradlew --version
test -f gradle/wrapper/gradle-wrapper.properties
grep -F 'gradle-9.7.1-bin.zip' gradle/wrapper/gradle-wrapper.properties
On Windows, use gradlew.bat and PowerShell equivalents.
Do not generate a new Wrapper from an untrusted tool just to satisfy
the checkpoint.
4. Create clean build agent and explicit reusable-cache roots
The template is source state. The build agent receives a fresh copy; reusable cache state lives outside its workspace. This mirrors a provider artifact/cache service without depending on one.
cd ..
mkdir -p approved-cache/gradle-wrapper approved-cache/gradle-modules pipeline-artifacts release-repo
rm -rf agent-build agent-promote
cp -R template agent-build
mkdir -p agent-build/.ci/gradle-home
test ! -d agent-build/build
printf 'source_copy=clean\n' > agent-build/.ci/source-evidence.txt
5. Inject a harmless poisoned/stale cache candidate
Create an entry that would be dangerous if the restore policy treated project outputs as dependencies:
mkdir -p approved-cache/WRONG-build-output/build/libs
printf 'poisoned-stale-bytes\n' > approved-cache/WRONG-build-output/build/libs/ci-demo-1.0.0.jar
sha256sum approved-cache/WRONG-build-output/build/libs/ci-demo-1.0.0.jar
Prediction: this file must never be copied into
agent-build/build. The approved restore policy only
covers Wrapper/dependency cache state. A real remote Build Cache
would be configured separately with trusted task-output keys.
6. Build agent: record identity, run shards, package once
Run from agent-build:
cd agent-build
export GRADLE_USER_HOME="$PWD/.ci/gradle-home"
mkdir -p .ci/evidence
git rev-parse HEAD 2>/dev/null | tee .ci/evidence/commit.txt || printf 'disposable-copy\n' | tee .ci/evidence/commit.txt
java -version 2>&1 | tee .ci/evidence/java.txt
./gradlew --version | tee .ci/evidence/gradle.txt
./gradlew clean testShardA testShardB jar
find build/test-results -type f -name 'TEST-*.xml' -print | sort | tee .ci/evidence/test-xml.txt
artifact=$(find build/libs -maxdepth 1 -type f -name '*.jar' | head -n 1)
sha256sum "$artifact" | tee .ci/evidence/artifact.sha256
If your find implementation lacks
-maxdepth, list build/libs/*.jar directly.
The artifact is built exactly once in this checkpoint.
8. Export the build handoff
Copy only deliberate evidence and the exact built artifact to the pipeline-artifact directory:
mkdir -p ../pipeline-artifacts/build-001/reports
artifact=$(find build/libs -maxdepth 1 -type f -name '*.jar' | head -n 1)
cp "$artifact" ../pipeline-artifacts/build-001/ci-demo-1.0.0.jar
cp .ci/evidence/artifact.sha256 ../pipeline-artifacts/build-001/
cp .ci/evidence/java.txt .ci/evidence/gradle.txt .ci/evidence/commit.txt ../pipeline-artifacts/build-001/
cp -R build/test-results/shard-a ../pipeline-artifacts/build-001/reports/
cp -R build/test-results/shard-b ../pipeline-artifacts/build-001/reports/
cd ../pipeline-artifacts/build-001
actual=$(sha256sum ci-demo-1.0.0.jar | awk '{print $1}')
expected=$(awk '{print $1}' artifact.sha256)
test "$actual" = "$expected"
printf 'handoff_sha256=%s\n' "$actual"
9. Promotion agent: no source build, no Gradle invocation
The promotion “agent” receives only pipeline artifacts. That separation makes accidental rebuild impossible.
cd ../../
mkdir -p agent-promote/input
cp -R pipeline-artifacts/build-001/. agent-promote/input/
cd agent-promote/input
expected=$(awk '{print $1}' artifact.sha256)
actual=$(sha256sum ci-demo-1.0.0.jar | awk '{print $1}')
test "$actual" = "$expected"
mkdir -p ../../release-repo/dev/academy/ci/ci-demo/1.0.0
cp ci-demo-1.0.0.jar ../../release-repo/dev/academy/ci/ci-demo/1.0.0/
release=$(sha256sum ../../release-repo/dev/academy/ci/ci-demo/1.0.0/ci-demo-1.0.0.jar | awk '{print $1}')
test "$release" = "$expected"
printf 'promoted_without_rebuild=%s\n' "$release"
Critical evidence: the promotion directory contains no Wrapper, source files, or build script. It literally cannot recompile the candidate.
10. Prove the poisoned build-output cache was irrelevant
Compare the stale injected cache hash with the promoted artifact hash. They must differ, and the promoted artifact must equal the build handoff:
poison=$(sha256sum ../../approved-cache/WRONG-build-output/build/libs/ci-demo-1.0.0.jar | awk '{print $1}')
release=$(sha256sum ../../release-repo/dev/academy/ci/ci-demo/1.0.0/ci-demo-1.0.0.jar | awk '{print $1}')
test "$poison" != "$release"
printf 'ignored_poison=%s\nrelease=%s\n' "$poison" "$release"
11. Record the Maven/Gradle operating matrix
The checkpoint uses Gradle, but the production model should document both build tools when the organization runs a mixed estate:
| Invariant | Gradle implementation | Maven implementation |
|---|---|---|
| Build-tool bootstrap |
Committed verified gradlew / Wrapper 9.7.1.
|
Committed verified mvnw / Wrapper 3.3.4 + Maven
3.9.16.
|
| Isolated dependency state |
Explicit GRADLE_USER_HOME; dependency/build
cache separated.
|
-Dmaven.repo.local=...; avoid confusing
installed project artifacts with downloaded dependencies.
|
| Test sharding |
Separate Test tasks/tags/classes with distinct
XML reports.
|
Pinned Surefire/Failsafe executions or
-Dtest selection with distinct CI report
collection.
|
| Artifact handoff |
build/libs artifact copied to CI artifact store
with checksum.
|
target package copied to CI artifact store with
checksum.
|
| Promotion |
Publish/copy exact exported artifact; no
jar/build.
|
Publish/copy exact exported artifact; no
package rebuild.
|
12. Verification checklist
| Evidence | Pass condition |
|---|---|
| Source/tool identity | Commit/ref, Gradle 9.7.1 Wrapper, JDK 21, Java 17 target recorded. |
| Clean workspace | Agent starts without build/ output. |
| Shard evidence | A and B XML report paths exist; manifest union exactly matches expected tests. |
| Poison simulation | Stale fake JAR exists but is never restored/exported/promoted. |
| Artifact checksum | Build handoff SHA-256 equals release-repository SHA-256. |
| No rebuild | Promotion agent has no source/Wrapper and invokes no build tool. |
| Secrets | No real repository credentials/tokens were used. |
| Cleanup | Only disposable checkpoint directories are removed. |
13. Cleanup and rollback
After retaining any evidence files you want for study, delete only the checkpoint directory:
cd ../../
rm -rf ch29-checkpoint
14. What Chapter 29 adds—and the bridge to Chapter 30
You now have a CI operating model that preserves build-tool identity, explicit cache trust, complete test evidence, and immutable artifact promotion across ephemeral agents. Chapter 30 compares Maven and Gradle as whole build systems: lifecycle versus task graph, metadata and dependency semantics, migration strategies, mixed estates, and tool-selection tradeoffs.
Knowledge check
Why is the promotion agent intentionally source-free?
It proves promotion consumes the existing candidate and cannot silently rebuild a different one.
What does the shard manifest failure simulation prove?
That the pipeline detects incomplete test assignment independently of individual shard task success.
Why is the fake poisoned JAR kept in a separate cache class?
To demonstrate that correct restore policy ignores project build outputs rather than trusting arbitrary cached files.
Which hashes must match at the end?
The build-stage exported artifact SHA-256 and the promoted release-repository artifact SHA-256.
What is Chapter 30’s next question?
How Maven and Gradle differ as build models and how to migrate or operate them together deliberately.
Official references and version notes
- Gradle 9.7.1 release notes — pinned Gradle baseline.
- Gradle dependency caching — dependency-cache state and guidance for ephemeral builds.
- Gradle Build Cache — local/remote task-output cache and CI push/read trust model.
- Build Cache use cases — CI-produced cache entries and cross-machine reuse.
-
Gradle task outcomes
—
UP-TO-DATEversusFROM-CACHE. - Gradle on GitLab CI — current Wrapper-first CI guidance and cache considerations.
- Maven release history — Maven 3.9.16 GA baseline.
- Maven Wrapper 3.3.4 — current stable Wrapper baseline.
- Maven Surefire test goal — explicit test selection and failure behavior.
Version-sensitive statements were rechecked against primary documentation on 2026-08-24. Mandatory labs remain local/free; hosted CI, remote caches, artifact repositories, and secret stores are represented as optional production mappings only.
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.