Checkpoint Lab — Version Catalogs, Platforms, Constraints, Dependency Locking, and Centralized Version Governance
Govern a small multi-project dependency set end to end, capture lock evidence, perform one minimal reviewed upgrade, inject an out-of-policy version, and prove the gate catches it.
Learning objectives
- Build a three-project Gradle governance fixture with one version catalog, one Java platform, and lock state in two consumers.
- Predict which files represent declaration intent, resolution policy, and resolved state before running Gradle.
- Capture initial dependencyInsight and lockfile evidence for a clean baseline.
- Perform a reviewed Commons Lang 3.19.0 → 3.20.0 platform upgrade with a targeted minimal lock update.
- Inject a direct out-of-policy strict version and prove graph resolution rejects it.
- Verify rollback/cleanup and articulate the production operating model carried into Chapter 21.
1. Checkpoint scenario and success criteria
Your team owns a small multi-project JVM product. Build authors should write stable aliases, compatibility policy should live in one platform, and CI should reproduce previously reviewed selections. You must prove each layer independently and show that an incompatible direct request cannot bypass the platform.
Success means: the catalog contains no duplicate platform version, both consumers resolve through the platform, lockfiles are committed candidates, a single reviewed policy upgrade produces an explainable lock diff, and an out-of-policy strict request fails resolution.
2. Preflight and exact assumptions
Wrapper rule: start from a disposable copy of a
project whose Gradle 9.7.1 Wrapper was already reviewed in Chapter
15. Do not generate a new wrapper from an unknown system Gradle
merely for this lab. Keep GRADLE_USER_HOME inside the
disposable lab and remove only that lab state during cleanup.
| Assumption | Checkpoint value |
|---|---|
| Gradle | 9.7.1 through committed Wrapper |
| Gradle runtime JVM | JDK 21 in the course environment; Gradle 9 requires JVM 17+ |
| Project target | Java 17 toolchain |
| Repository | Maven Central only for public dependencies |
| Initial policy | Commons Lang 3.19.0 strict; SLF4J API 2.0.17 required |
| Reviewed upgrade | Commons Lang 3.20.0 |
| Cache |
Project-local disposable GRADLE_USER_HOME
|
| Paid/hosted services | None |
cd gradle-governance-checkpoint
export GRADLE_USER_HOME="$PWD/.gradle-user-home"
./gradlew --version
java -version
3. Create the governance files
// settings.gradle.kts
dependencyResolutionManagement {
repositories { mavenCentral() }
}
rootProject.name = "gradle-governance-checkpoint"
include("platform", "lib", "app")
# gradle/libs.versions.toml
[libraries]
commons-lang3.module = "org.apache.commons:commons-lang3"
slf4j-api.module = "org.slf4j:slf4j-api"
// platform/build.gradle.kts
plugins { `java-platform` }
dependencies {
constraints {
api("org.apache.commons:commons-lang3") {
version { strictly("3.19.0") }
because("approved checkpoint baseline")
}
api("org.slf4j:slf4j-api:2.0.17")
}
}
// lib/build.gradle.kts
plugins { `java-library` }
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
dependencies {
api(platform(project(":platform")))
api(libs.commons.lang3)
}
dependencyLocking { lockAllConfigurations() }
// app/build.gradle.kts
plugins { application }
java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }
dependencies {
implementation(platform(project(":platform")))
implementation(project(":lib"))
implementation(libs.slf4j.api)
}
application { mainClass = "dev.academy.app.App" }
dependencyLocking { lockAllConfigurations() }
Add the same small Java sources from Lesson 2 (Words
using StringUtils, and App calling it).
The exact source behavior is not the checkpoint; the dependency
graph is.
4. Predict before resolution
| Prediction | Expected reason |
|---|---|
libs.versions.toml contains no Lang version
|
Platform owns graph policy; catalog owns coordinate alias. |
:lib selects Lang 3.19.0 |
Its versionless alias is constrained by the strict platform. |
:app also sees Lang 3.19.0 transitively through
:lib
|
The app consumes the same platform and project dependency. |
No gradle.lockfile exists before
--write-locks in a fresh fixture
|
Lock activation does not itself persist state. |
| Changing platform strict 3.19.0 → 3.20.0 without updating locks will fail | Existing lock state acts like a strict opinion at 3.19.0. |
Write these predictions into
evidence/predictions.txt before running resolution. The
checkpoint requires prediction/evidence pairs, not retrospective
explanations.
5. Capture the initial graph and lock state
mkdir -p evidence
./gradlew :lib:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath | tee evidence/lib-lang.initial.txt
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath | tee evidence/app-lang.initial.txt
./gradlew :lib:dependencies :app:dependencies --write-locks | tee evidence/locks.initial.log
cp lib/gradle.lockfile evidence/lib.initial.lock
cp app/gradle.lockfile evidence/app.initial.lock
./gradlew :app:build
Verify the platform is cited in selection reasons, Lang is 3.19.0, SLF4J is 2.0.17, and the lockfiles contain the external modules for the configurations resolved by the dependency reports.
6. Reviewed upgrade with minimal lock intent
Edit one policy line in platform/build.gradle.kts:
strictly("3.20.0").
set +e
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath > evidence/stale-lock.expected-failure.txt 2>&1
stale=$?
set -e
printf 'stale lock exit=%s\n' "$stale"
./gradlew :lib:dependencies :app:dependencies --update-locks org.apache.commons:commons-lang3 | tee evidence/locks.targeted-update.log
diff -u evidence/lib.initial.lock lib/gradle.lockfile > evidence/lib.lock.diff || true
diff -u evidence/app.initial.lock app/gradle.lockfile > evidence/app.lock.diff || true
cat evidence/lib.lock.diff
cat evidence/app.lock.diff
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath | tee evidence/app-lang.upgraded.txt
./gradlew :app:build
Verification: the resolved Lang version is now 3.20.0, the platform is the policy source, and the lock diff is limited to changes causally required by that upgrade. If another module moves, explain why using insight before accepting it.
7. Inject an out-of-policy version and prove the gate catches it
Temporarily add this dependency to
app/build.gradle.kts:
implementation("org.apache.commons:commons-lang3") {
version { strictly("3.18.0") }
because("intentional out-of-policy checkpoint injection")
}
set +e
./gradlew :app:dependencies --configuration runtimeClasspath > evidence/out-of-policy.txt 2>&1
policy_status=$?
set -e
printf 'out-of-policy exit=%s\n' "$policy_status"
sed -n '1,220p' evidence/out-of-policy.txt
# Repair: remove the temporary direct strict dependency, then verify again.
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath
./gradlew :app:build
Expected: the platform strictly requires 3.20.0 while the injected direct dependency strictly requires 3.18.0. The accepted-version sets do not intersect, so resolution fails. That failure is the governance gate working. Do not “fix” it with an exclusion or force.
8. Explain declaration versus resolution in one sentence per file
| File | What it controls |
|---|---|
gradle/libs.versions.toml |
Stable aliases/coordinates used by build authors; no final version is encoded for this checkpoint. |
platform/build.gradle.kts |
Compatibility/version policy that participates in graph resolution. |
lib/build.gradle.kts /
app/build.gradle.kts
|
Which platform and dependency edges each project actually declares, plus locking activation. |
lib/gradle.lockfile /
app/gradle.lockfile
|
Resolved external module versions captured for locked, resolved configurations. |
evidence/*.txt |
Human-review proof of selection reasons/failures; useful lab/CI artifact, not input to Gradle resolution. |
9. Verification checklist
- Wrapper reports Gradle 9.7.1 and the expected JDK.
-
Catalog aliases are consumed in
:lib/:appand do not duplicate the Lang policy version. - Both consumers depend on the local platform.
- Initial insight selects Lang 3.19.0; upgraded insight selects 3.20.0.
- Initial lockfiles are saved before mutation.
-
Targeted update uses
--update-locks org.apache.commons:commons-lang3. - Every lock diff line is explained.
- The out-of-policy strict 3.18.0 injection fails resolution.
- After repair,
:app:buildsucceeds again. - No credentials, normal user cache, or hosted service was modified.
10. Cleanup and rollback
# Confirm the temporary 3.18.0 injection has been removed.
grep -R '3.18.0' app platform gradle || true
# The final reviewed state should retain the 3.20.0 policy and its matching locks.
./gradlew :app:dependencyInsight --dependency commons-lang3 --configuration runtimeClasspath
cd ..
rm -rf gradle-governance-checkpoint
For a real repository, rollback means reverting the platform and lockfile commit together—not deleting normal caches. This disposable cleanup removes only the checkpoint directory.
11. Production operating model and bridge to Chapter 21
Chapter 20 adds a three-layer dependency-governance model: catalogs make declarations consistent, platforms/constraints make compatibility policy explicit, and lockfiles make selected state reviewable. The next chapter expands the structural boundary: multi-project and composite builds determine where these policies live, how included builds interact, and how build ownership scales without turning one root script into a global mutable namespace.
Knowledge check
Which file in the checkpoint controls the approved Lang version?
platform/build.gradle.kts; the catalog is
deliberately versionless for Lang.
Why is the stale-lock failure before the targeted update required evidence?
It proves the existing captured state prevented an unreviewed graph transition after policy changed.
Why use
--update-locks org.apache.commons:commons-lang3
instead of deleting lockfiles?
It keeps unrelated lock state as resolution input and narrows the intended upgrade while preserving review history.
What should happen when app adds strict Lang 3.18.0 while the platform strictly requires 3.20.0?
Resolution should fail because the strict accepted-version sets are incompatible.
Does a successful final build prove artifact authenticity?
No. It proves the governed graph builds; dependency integrity/signature verification is a separate supply-chain control.
What is the Chapter 21 bridge?
Now that dependency governance is explicit, Chapter 21 decides how that policy is owned and shared across multi-project, composite, and included-build structures.
Official references and version notes
- Gradle Version Catalogs — alias/accessor semantics, TOML format, rich versions, publishing/sharing.
- Using Catalogs with Platforms — explains why catalogs do not enforce graph versions and how platforms differ.
- Gradle Platforms and Java Platform Plugin — constraints, BOM imports, regular versus enforced platforms.
-
Declaring Versions and Ranges
—
require,strictly,prefer, andreject. -
Dependency Locking
— activation,
--write-locks,--update-locks, lockfile location, and lock modes. - Viewing and Debugging Dependencies — dependency reports and selection reasons.
- Gradle 9.7.1 Release Notes — pinned chapter baseline.
Version-sensitive statements were rechecked for Gradle 9.7.1 on 2026-08-24. This chapter does not depend on Maven 4, a third-party Gradle plugin, hosted CI, or a commercial repository.
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.