-
Notifications
You must be signed in to change notification settings - Fork 70
Maintain your build
- After talking about the local/CI split on logging, move details to the monitoring page.
- Structure between this page and dependency management.
- Clean up after card #533 completes.
Treat your build as you would your codebase: Maintain it, refactor as needed, run performance checks, et al.
The key mindset for maintaining your build is measuring and metrics. This means you take advantage of tools that:
- Show you what your build does at every step
- Show you how long steps take
- Give you data relevant to each step
- Automate most busy work, and only want attention when needed
What does your build do exactly, and in what sequence or order? To find out you can ask the Gradle or Maven tools, sometimes with plugins.
You want to know what your build is actually doing. When you are a local developer, you usually do not want much output beyond progress, and any errors/warnings that pop up. But when looking at CI logs (a remote build), you'd like to see a lot of detail: you do not scan CI build logs often unless a problem crops up, and every log line helps, but noisy "download progress" messages are still in the way of your goal.
From this we conclude:
- For local builds: Keep build output to a minimum.
- For CI builds: Show as much as sensible.
- Gradle dry run has the
--dry-run
(or-m
) flag for a high-level view -
Gradle Task Tree plugin
with
./gradlew some...tasks taskTree
These are high-level, and it is hard for you to change task sequences through configuration. You may need to fork plugins to alter how they work in your build. However, most times the build order defaults are good enough, and until you have specific build needs, default orderings are satisfactory.
-
Maven Buildplan plugin
with
./mvnw buildplan:list
(see plugin documentation for other goals and output format)
This is not builtin to your build tool: Maven does not have a "dry run" flag. However in most cases Maven provides more information and flexibility. With all build plugins you can alter when a tool runs in the build by giving a "phase" to your declaration.
An example from
pom.xml
that sets when to run code style checks in the build:
<plugin>
<!-- Ignoring details and configuration for the plugin -->
<artifactId>maven-checkstyle-plugin</artifactId>
<executions>
<execution>
<id>checkstyle-validate</id> <!-- arbirary: helpful in logging -->
<!-- We want to pin checkstyle:validate to Maven validate phase -->
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
This is more verbose than Gradle, but also more obvious. With the above configuration, you would see in the build output (modulo your project, and your checkstyle plugin version):
[INFO] --- checkstyle:3.4.0:check (validate-checkstyle) @ modern-java-practices
Note
You have a lot of control over logging from Maven when building.
Above you can see that <id>SOME NAME</id>
appears in the logging which is
easily searchable locally or in remote logging systems.
Let tools tell you when you have dodgy dependencies, or an inconsistent setup.
For example, leverage jdeps
which
comes with the JDK.
Jdeps spots, for example, if you have a multi-version jar as a dependency that
does not include your JDK version (an example of this may be is JUnit), or if
your code depends on internal (non-public) classes of the JDK
(important especially when using the JDK module system).
The Kordamp plugin used for Gradle does not fail the build when jdeps errors, and only generates a report text file. See this issue.
Try Maven with dependency:tree -Dverbose
.
This will show conflicting versions of dependencies.
It is frustrating for local devs when something horrible happened during the build (say a production with "ERROR" output during a test), but:
- The build is GREEN, and developers should trust that.
- There is too much output in the local build, so developers don't spot telltale signs of trouble.
There are many approaches to this problem. This project uses JDK logging as an
example,
and keeps the build quiet in
config/logging.properties
.
In CI, this is different, and there you want as much output as possible to diagnose the unexpected.
An important part of build hygiene is keeping your build system, plugins, and dependencies up to date. This might be simply to address bug fixes (including bugs you weren't aware of), or might be critical security fixes. The best policy is: Stay current. Others will have found—reported problems—, and 3rd-parties may have addressed them. Leverage the power of Linus' Law ("given enough eyeballs, all bugs are shallow").
TODO: Move discussion to the "Dependency management" page.
TODO: Reorganize so the Gradle and Maven material separates more.
- Gradle — Benjamin Manes is kind enough in his plugin project to list alternatives. If you are moving towards Gradle version catalogs, you should consider the Version catalog update plugin used in this project.
- Maven
- Team agreement on release updates only, or if non-release plugins and dependencies make sense for your situation.
- Each of these plugins for Gradle or Maven have their quirks. Do not treat them as sources of truth but as recommendations. Use your judgment. In parallel, take advantage of CI tooling such as Dependabot (GitHub) or Dependabot (GitLab).
An example use which shows most outdated plugins and dependencies (note that one
Maven example modifies your pom.xml
, a fact you can choose or avoid):
$ ./gradlew dependencyUpdates
# output ommitted
$ ./mvnw versions:update-properties # Updates pom.xml in place
$ ./mvnw versions:display-property-updates # Just lists proposed updates
# output ommitted
This project keeps version numbers for dependencies in
gradle/libs.versions.toml
following Gradle
recommendations.
Since Gradle build files (including gradle/libs.versions.toml
) should be in
Git, versionCatalogApplyUpdates
is safe as you
can always revert changes, but do consider looking before committing, and
always run a "clean build" before commit.
TODO: Remove this after completing transistion to Gradle version catalog.
While the project transistions to the version catalog, some versions are
tracked in
gradle.properties
.
The POM tracks dependency and plugin versions.
Since your pom.xml
is in Git, versions:update-properties
is safe as you
can always revert changes, but do consider looking before committing, and
always run a "clean verify" before commit.
Dependabot
may prove speedier for you than updating dependency versions locally, and runs
in CI (GitHub) on a schedule you pick.
The bot submits PRs to your repository when it finds out of date dependencies,
and is smart about new git pushes and rebases.
See
dependabot.yml
for an example using a daily schedule for Gradle and Maven.
Another choice may be Renovate.
Your simplest approach to Gradle is to keep everything in build.gradle
. Even
this unfortunately still requires a settings.gradle
to define a project
artifact name, and leaves duplicate version numbers for related dependencies
scattered through build.gradle
.
Another approach is to rely on a Gradle plugin such as that from Spring Boot to manage dependencies for you. This unfortunately does not help with plugins at all, nor with dependencies that Spring Boot does not know about.
This project uses a 3-file solution for Gradle versioning, and you should consider doing the same:
-
gradle.properties
is the sole source of truth for version numbers, both plugins and dependencies. -
settings.gradle
configures plugin versions using the properties. -
build.gradle
uses plugins without needing version numbers, and dependencies refer to their property versions.
The benefits of this approach grow for Gradle multi-project projects, where you
may have plugin and dependency versions scattered across each build.gradle
file for you project and subprojects.
So to adjust a version, edit gradle.properties
. To see this approach in action
for dependencies, try:
$ grep junitVersion gradle.properties setttings.gradle build.gradle
gradle.properties:junitVersion=5.7.0
build.gradle: testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion"
build.gradle: testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
If you use the toolVersion
property for a plugin to update the called tool
separately from the plugin itself, this is a convention, not something the
Gradle API provides to plugins. As a consequence, the Versions plugin is unable
to know if your tool version is out of date. An example is the JaCoCo plugin
distributed with Gradle.
Two options:
- Do not use the
toolVersion
property unless needed to address a discovered build issue, and remove it once the plugin catches up to provide the tool version you need - Continue using the
toolVersion
property, and as part of running./gradlew dependencyUpdates
, manually check alltoolVersion
properties, and updategradle.properties
as accordingly
NB — Maven handles this differently, and does not have this concern.
A fast local build is one of the best things you can do for your team. There are variants of profiling your build for Gradle and Maven:
-
Gradle build scan with the
--scan
flag -
Maven profiler extension with
the
-Dprofile
flag
See an example build scan from May 1, 2023.
NB — Build Scan supports Maven as well when using the paid enterprise version.
Some shortcuts to speed up the red-green-refactor cycle:
- Just validate code coverage; do not run other parts of the build:
- Gradle —
./gradlew clean jacocoTestReport jacocoTestCoverageVerification
- Maven —
./mvnw clean test jacoco:report jacoco:check
.
- Gradle —
- Gradle and Maven provide default versions of bundled plugins. In both built tools, the version update plugins need you to be explicit in stating versions for bundled plugins, so those versions are visible for update.
- Enable HTML reports for local use; enable XML reports for CI use in integrating with report tooling.
- To open the report for Jdeps, build locally and use the
<project root>/build/reports/jdeps/
(Gradle) path. The path shown in a Docker build is relative to the interior of the container. - Both dependency vulnerability
checks and mutation
testing can take a while, depending on your project.
If you find they slow your team local build too much, these are good
candidates for moving to
CI-only steps, such as a
-PCI
flag for Maven (see "Tips" section of Use Gradle or Maven for Gradle for an equivalent). This project keeps them as part of the local build, as the demonstration code is short. - See the bottom of
build.gradle
for an example of customizing "new" versions reported by the GradledependencyUpdates
task. - The equivalent Maven approach for controlling the definition of "new" is to use Version number rules. And see Ignore a specific version suffix in version updates.
- With the Gradle plugin, you can program your build to fail if dependencies are outdated. Read at Configuration option to fail build if stuff is out of date for details.
TODO: Placeholder section
See the code repo for working examples.
This work is dedicated/deeded to the public following laws in jurisdictions
for such purpose.
The principal authors are:
You can always use the "Pages" UI control above this sidebar ☝ to navigate around all pages alphabetically (even pages not in this sidebar), to navigate the outline for each page, or for a search box.
Here is the suggested reading order: