Chapter 29Lesson 05~380 minutes

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.

CheckpointGradle WrapperShard manifestSHA-256Promotion

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.

7. Independently verify shard completeness and uniqueness

Do not infer completeness from task success. Validate the manifest:

sort -u ../template/shard-manifest/expected.txt > .ci/expected.txt
cat ../template/shard-manifest/shard-a.txt ../template/shard-manifest/shard-b.txt | sed '/^$/d' | sort > .ci/assigned-all.txt
sort -u .ci/assigned-all.txt > .ci/assigned-unique.txt

diff -u .ci/expected.txt .ci/assigned-unique.txt
if test "$(wc -l < .ci/assigned-all.txt)" -ne "$(wc -l < .ci/assigned-unique.txt)"; then
  echo "Duplicate shard assignment detected" >&2
  exit 1
fi

Now break it deliberately by removing ShardBTest from the copied shard-B manifest and rerun the comparison. Preserve the diff, then restore the line. The failure proves the gate can detect a false-green partition even when shard A itself passes.

cp ../template/shard-manifest/shard-b.txt .ci/shard-b.saved
: > ../template/shard-manifest/shard-b.txt
if cat ../template/shard-manifest/shard-a.txt ../template/shard-manifest/shard-b.txt | sed '/^$/d' | sort -u | diff -u .ci/expected.txt -; then
  echo "Expected the broken shard plan to fail" >&2
  exit 1
else
  echo "Broken shard plan detected as expected"
fi
cp .ci/shard-b.saved ../template/shard-manifest/shard-b.txt

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?

What does the shard manifest failure simulation prove?

Why is the fake poisoned JAR kept in a separate cache class?

Which hashes must match at the end?

What is Chapter 30’s next question?

Official references and version notes

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.

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