Java
Download dependencies, test and package a Maven project in containers, with the download cached by the POM.
This tutorial downloads the dependencies and plugins of a small Maven project
once, then runs mvn test and mvn package against them offline, each in a
maven:3.9-eclipse-temurin-21 container and each skipped when its inputs haven't
changed. You need hammerkit and Docker; neither a JDK nor
Maven has to be installed on your machine.
The finished project is
examples/tutorial-java.
The project
tutorial-java/
├── pom.xml
└── src/
├── main/java/dev/hammerkit/greet/Greet.java
└── test/java/dev/hammerkit/greet/GreetTest.java<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.hammerkit</groupId>
<artifactId>greet</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.4</version>
<scope>test</scope>
</dependency>
<!-- Surefire otherwise picks its own launcher version at test time -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- from 3.9.0 on, go-offline also downloads the dependencies of plugins -->
<artifactId>maven-dependency-plugin</artifactId>
<version>3.9.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<dependencies>
<!-- the JUnit provider Surefire would otherwise download at test time -->
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>3.5.4</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
Besides JUnit, the POM pins three things so that everything a build needs can be
downloaded up front. Surefire, Maven's test runner, normally downloads its JUnit
provider and picks a JUnit launcher only when the tests start. Declaring both
moves them into the POM. go-offline downloads dependencies declared on plugins
only from maven-dependency-plugin 3.9.0 on, so the POM pins that version too.
package dev.hammerkit.greet;
public class Greet {
public static String hello(String name) {
return "Hello, " + name + "!";
}
public static void main(String[] args) {
System.out.println(hello(args.length > 0 ? args[0] : "world"));
}
}
package dev.hammerkit.greet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class GreetTest {
@Test
void greetsByName() {
assertEquals("Hello, hammerkit!", Greet.hello("hammerkit"));
}
}
1. Download dependencies
A task names the image it runs in, the files it reads (src) and the files it
produces (generates). The deps task downloads every dependency and plugin the
POM names into a local repository in the project:
envs:
# flags for every mvn call: no progress noise, and the local repository in
# the project, where the deps task keeps it as its output
MAVEN_ARGS: --batch-mode -Dmaven.repo.local=.m2
tasks:
deps:
description: download dependencies and plugins
image: maven:3.9-eclipse-temurin-21
src: [pom.xml]
generates: [.m2]
cmds:
- mvn dependency:go-offlinehammerkit depsRun it again and hammerkit skips it: pom.xml is unchanged. Only a dependency
change downloads again.
MAVEN_ARGS is read by every mvn call. It moves the local repository from
~/.m2/repository into the project, where deps can keep it as a task output.
2. Test and package
Both tasks depend on deps, which mounts its .m2 into their containers:
tasks:
test:
description: compile and run the unit tests
image: maven:3.9-eclipse-temurin-21
deps: [deps]
src: [src]
cmds:
- mvn --offline test
package:
description: build the jar
image: maven:3.9-eclipse-temurin-21
deps: [deps]
src: [src]
generates:
- path: target
export: true
cmds:
- mvn --offline package -DskipTestsNeither lists pom.xml: a task also sees the sources of the tasks it depends on,
and a change to them reruns it, so editing the POM reruns deps, test and
package. --offline keeps Maven from reaching the network, so an artifact
missing from deps' output fails the build instead of being downloaded on the
side.
A container task's outputs stay in a volume hammerkit manages; export: true also
copies target into your project, so the jar is at target/greet-1.0.0.jar.
-DskipTests doesn't skip any checking: test runs the tests, and ci below
requires both.
test and package don't depend on each other, so they run in parallel.
3. One task for CI
A task without cmds groups other tasks. ci names everything a change has to
pass. The complete build file:
envs:
# flags for every mvn call: no progress noise, and the local repository in
# the project, where the deps task keeps it as its output
MAVEN_ARGS: --batch-mode -Dmaven.repo.local=.m2
tasks:
deps:
description: download dependencies and plugins
image: maven:3.9-eclipse-temurin-21
src: [pom.xml]
generates: [.m2]
cmds:
- mvn dependency:go-offline
test:
description: compile and run the unit tests
image: maven:3.9-eclipse-temurin-21
deps: [deps]
src: [src]
cmds:
- mvn --offline test
package:
description: build the jar
image: maven:3.9-eclipse-temurin-21
deps: [deps]
src: [src]
generates:
- path: target
export: true
cmds:
- mvn --offline package -DskipTests
ci:
description: everything CI checks
deps: [test, package]
hammerkit run ciSummary:
ci executed 34.1s
deps executed 31.8s
package executed 33.9s
test executed 34.1s
4 executed, 0 cached (0% cache hit), 34.2s totalRun it again without changing anything:
Summary:
ci cached 17ms
deps skipped 0ms
package cached 0ms
test cached 0ms
0 executed, 3 cached, 1 skipped (100% cache hit), 34ms totaldeps is skipped: every task that needs it was a cache hit, so the repository
isn't needed either. Edit a file in src and test and package run again,
against the artifacts deps already has.
Ignore what hammerkit and Maven write into the project:
.hammerkit
.m2
target
Run it in CI
CI runs the same command: install hammerkit and run
hammerkit run ci. A fresh runner starts with an empty cache; to reuse what your
laptop or the previous run already built, share the cache as described in
caching in CI.
Next steps
- Tests that need a database: start it as a service and let the
task
needit, see services & networking. - Several modules in one repository: monorepos.
- A task reruns and you don't know why:
hammerkit explain.