Skip to content

Maintain your build

B. K. Oxley (binkley) edited this page Sep 24, 2024 · 21 revisions
Maintain build

Maintain your build

TODO page needs content

  • 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

Know what your build does

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.

Quiet local builds, noisly CI builds

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

  • 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

  • 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.

Keep your build clean

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).

Gradle

The Kordamp plugin used for Gradle does not fail the build when jdeps errors, and only generates a report text file. See this issue.

Maven

Try Maven with dependency:tree -Dverbose. This will show conflicting versions of dependencies.

Keep local builds quiet

It is frustrating for local devs when something horrible happened during the build (say a production with "ERROR" output during a test), but:

  1. The build is GREEN, and developers should trust that.
  2. 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.

Keep CI builds noisy

In CI, this is different, and there you want as much output as possible to diagnose the unexpected.

Keep your build current

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").

Keep plugins and dependencies up-to-date

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

Gradle

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.

Maven

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.

Automated PRs for dependency updates

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.

More on Gradle version numbers

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"

Note on toolVersion property

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 all toolVersion properties, and update gradle.properties as accordingly

NB — Maven handles this differently, and does not have this concern.

Keep your build fast

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:

See an example build scan from May 1, 2023.

NBBuild Scan supports Maven as well when using the paid enterprise version.

Keep your developers fast

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.

Tips

  • 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 Gradle dependencyUpdates 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.

Going further

TODO: Placeholder section

Clone this wiki locally