Skip to content

Commit b95019b

Browse files
authored
feat(java-build): add reusable workflow for Java projects (#155)
1 parent 701e6d6 commit b95019b

5 files changed

Lines changed: 293 additions & 0 deletions

File tree

.github/workflows/java-build.yml

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
name: Build and Test Java Project
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
java-version:
7+
description: "Java version to use"
8+
default: "21"
9+
type: string
10+
refresh-dependencies:
11+
description: "Run Gradle with --refresh-dependencies. Only useful for changing modules (`-SNAPSHOT`) or dynamic versions, whose resolution Gradle otherwise caches for 24h; with pinned versions it just revalidates every module for nothing"
12+
default: false
13+
type: boolean
14+
sonar:
15+
description: "Run the SonarQube analysis (requires the `sonar-token` secret)"
16+
default: false
17+
type: boolean
18+
run-itest:
19+
description: "Run the `itest` Gradle task after the unit tests"
20+
default: false
21+
type: boolean
22+
dockerhub-login:
23+
description: "Log in to Docker Hub before the build. Enable it when the tests pull images, e.g. Testcontainers, to avoid anonymous pull rate limits"
24+
default: false
25+
type: boolean
26+
upload-jar:
27+
description: "Upload the built jar as an artifact, so that a later job can build an OCI image from it"
28+
default: false
29+
type: boolean
30+
artifact-name:
31+
description: "Name of the uploaded jar artifact"
32+
default: "boot-jar"
33+
type: string
34+
artifact-retention-days:
35+
description: "Retention of the uploaded jar artifact, in days"
36+
default: 1
37+
type: number
38+
secrets:
39+
dockerhub-username:
40+
description: "Docker Hub username (used only when `dockerhub-login: true`)"
41+
required: false
42+
dockerhub-password:
43+
description: "Docker Hub token (used only when `dockerhub-login: true`)"
44+
required: false
45+
sonar-token:
46+
description: "SonarCloud token (used only when `sonar: true`)"
47+
required: false
48+
outputs:
49+
jar-path:
50+
description: "Path of the built jar, relative to the workspace. Only set when `upload-jar: true`, empty for pure libraries. Feed it to `docker-build.yml` as `build-args: jar=<path>`"
51+
value: ${{ jobs.build.outputs.jar-path }}
52+
artifact-name:
53+
description: "Name of the uploaded jar artifact, echoed back for convenience"
54+
value: ${{ jobs.build.outputs.artifact-name }}
55+
56+
permissions:
57+
contents: read # actions/checkout
58+
actions: write # actions/upload-artifact
59+
60+
jobs:
61+
build:
62+
runs-on: ubuntu-latest
63+
64+
outputs:
65+
jar-path: ${{ steps.gradle-properties.outputs.jar-path }}
66+
artifact-name: ${{ inputs.artifact-name }}
67+
68+
steps:
69+
- name: Checkout Repository
70+
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
71+
with:
72+
# Sonar needs the full history to attribute lines to authors, and the Gradle
73+
# build derives the project version from the tag pointing at HEAD.
74+
fetch-depth: 0
75+
76+
- name: Set up JDK
77+
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
78+
with:
79+
distribution: temurin
80+
java-version: ${{ inputs.java-version }}
81+
82+
- name: Set up Gradle
83+
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
84+
85+
- name: Login to Docker Hub
86+
if: ${{ inputs.dockerhub-login }}
87+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
88+
with:
89+
username: ${{ secrets.dockerhub-username }}
90+
password: ${{ secrets.dockerhub-password }}
91+
92+
# Inputs are passed through the environment rather than interpolated into the
93+
# script, so that a crafted input cannot inject shell commands.
94+
- name: Build and run unit tests
95+
env:
96+
REFRESH_DEPENDENCIES: ${{ inputs.refresh-dependencies }}
97+
SONAR_TOKEN: ${{ secrets.sonar-token }}
98+
WITH_SONAR: ${{ inputs.sonar }}
99+
GH_REPO_OWNER: ${{ github.repository_owner }}
100+
GH_REPO_NAME: ${{ github.event.repository.name }}
101+
run: |
102+
args=("build")
103+
if [ "$WITH_SONAR" = "true" ]; then
104+
# The Sonar Gradle plugin reads the branch, pull request and repository from
105+
# the GITHUB_* variables, so no branch or pull request property has to be passed.
106+
# sonar.projectKey is derived from SonarQube Gradle plugin and Gradle project properties.
107+
# sonar.organization (mandatory on SonarQube Cloud) is derived from the repository owner lowercased.
108+
sonar_organization="$(echo "$GH_REPO_OWNER" | tr '[:upper:]' '[:lower:]')"
109+
args+=("sonar")
110+
args+=("-Dsonar.organization=$sonar_organization")
111+
fi
112+
if [ "$REFRESH_DEPENDENCIES" = "true" ]; then
113+
args+=("--refresh-dependencies")
114+
fi
115+
./gradlew "${args[@]}" -i
116+
117+
- name: Run integration tests
118+
if: ${{ inputs.run-itest }}
119+
run: ./gradlew itest -i
120+
121+
- name: Upload test and coverage reports
122+
if: ${{ always() }}
123+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
124+
with:
125+
name: test-reports
126+
path: |
127+
**/build/reports/**
128+
if-no-files-found: ignore
129+
retention-days: 7
130+
131+
- name: Resolve project jar path
132+
id: gradle-properties
133+
if: ${{ inputs.upload-jar }}
134+
run: |
135+
# jarPathForOCI is defined by the iExec Java projects that ship an OCI image.
136+
properties=$(./gradlew -q properties)
137+
jar_path=$(echo "$properties" | awk '/^jarPathForOCI:/ {print $2}')
138+
if [ -z "$jar_path" ]; then
139+
echo "❌ Error: jarPathForOCI not defined in Gradle project"
140+
exit 1
141+
fi
142+
echo "jar-path=$jar_path" | tee -a "$GITHUB_OUTPUT"
143+
144+
- name: Upload jar artifact
145+
if: ${{ inputs.upload-jar }}
146+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
147+
with:
148+
name: ${{ inputs.artifact-name }}
149+
path: ${{ steps.gradle-properties.outputs.jar-path }}
150+
if-no-files-found: error
151+
retention-days: ${{ inputs.artifact-retention-days }}

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ Automates the process of publishing NPM packages to the NPM registry with highly
2424

2525
Validates that pull request titles follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for better repository management. Ensures your commit history remains clean and meaningful for improved collaboration.
2626

27+
### [Java Build](./java-build)
28+
29+
Builds, tests and analyses a Gradle-based Java project. Runs the unit tests and a SonarCloud analysis, and optionally hands the built jar to `docker-build` so that the artifact shipped in the image is exactly the one that was tested.
30+
2731
### 🦀 [Rust Build](./rust-build)
2832

2933
Provides a standardized workflow for building, testing, and publishing Rust packages with intelligent caching and comprehensive artifact management. Optimized for Rust projects of all sizes.

java-build/README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# ☕ Java Build Workflow
2+
3+
## 🔍 Overview
4+
5+
This reusable GitHub Actions workflow builds, tests and analyses a Gradle-based Java project.
6+
It is the GitHub Actions counterpart of the `buildJavaProject` Jenkins pipeline used by the iExec middleware
7+
services, and covers the path from a checkout to a jar handed over to an OCI image build.
8+
9+
It serves both services and pure libraries, so anything only a service needs — the Docker Hub login for
10+
Testcontainers, the jar upload — is opt-in and left to the caller.
11+
12+
## ✨ Features
13+
14+
- ☕ Sets up a JDK and Gradle with dependency caching
15+
- 🧪 Runs the unit tests, and optionally an `itest` task
16+
- 🔎 Runs a SonarCloud analysis whose branch and pull request context is auto-detected from the GitHub environment
17+
- 🐳 Optionally logs in to Docker Hub, so Testcontainers-based tests are not hit by anonymous pull rate limits
18+
- 📤 Uploads the built jar as an artifact, ready for `docker-build.yml` to copy into an image
19+
- 📊 Always uploads the Gradle HTML test and coverage reports (`build/reports`), including on failure
20+
21+
## ⚙️ Inputs
22+
23+
| Name | Description | Required | Default |
24+
| ------------------------- | ------------------------------------------------------------------------------------------------ | -------- | ----------------- |
25+
| `artifact-name` | Name of the uploaded jar artifact | No | `"boot-jar"` |
26+
| `artifact-retention-days` | Retention of the uploaded jar artifact, in days | No | `1` |
27+
| `dockerhub-login` | Log in to Docker Hub before the build. Enable it when the tests pull images, e.g. Testcontainers | No | `false` |
28+
| `java-version` | Java version to use | No | `"21"` |
29+
| `refresh-dependencies` | Run Gradle with `--refresh-dependencies` (only useful for `-SNAPSHOT` or dynamic versions) | No | `false` |
30+
| `run-itest` | Run the `itest` Gradle task after the unit tests | No | `false` |
31+
| `sonar` | Run the SonarQube/SonarCloud analysis | No | `false` |
32+
| `upload-jar` | Upload the built jar as an artifact, so that a later job can build an OCI image from it | No | `false` |
33+
34+
## 🔐 Secrets
35+
36+
| Name | Description | Required |
37+
| -------------------- | -------------------------------- | ---------------------------- |
38+
| `dockerhub-username` | Docker Hub username | When `dockerhub-login: true` |
39+
| `dockerhub-password` | Docker Hub token | When `dockerhub-login: true` |
40+
| `sonar-token` | SonarCloud token | When `sonar: true` |
41+
42+
## 📤 Outputs
43+
44+
| Name | Description |
45+
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
46+
| `jar-path` | Path of the Gradle project built jar. Feed it to `docker-build.yml` as `build-args: jar=<path>`. Only set when `upload-jar: true`, empty for pure libraries |
47+
| `artifact-name` | Name of the uploaded jar artifact, echoed back for convenience |
48+
49+
## 💻 Example Usage
50+
51+
Building and testing a service, then building an OCI image from the very same jar. `upload-jar` is opt-in
52+
because the workflow also serves pure libraries, which have no image to build and nothing to hand over:
53+
54+
```yaml
55+
name: CI
56+
57+
on:
58+
pull_request:
59+
push:
60+
branches: [main]
61+
62+
jobs:
63+
build-and-test:
64+
# ⚠️ use tagged version here
65+
uses: iExecBlockchainComputing/github-actions-workflows/.github/workflows/java-build.yml@main
66+
with:
67+
java-version: "21"
68+
sonar: true
69+
dockerhub-login: true
70+
# opt in to the jar upload: only projects that ship an OCI image need it
71+
upload-jar: true
72+
secrets:
73+
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
74+
dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN_PULL_ONLY }}
75+
sonar-token: ${{ secrets.SONAR_TOKEN }}
76+
77+
build-image:
78+
needs: build-and-test
79+
# ⚠️ use tagged version here
80+
uses: iExecBlockchainComputing/github-actions-workflows/.github/workflows/docker-build.yml@main
81+
with:
82+
image-name: docker-regis.iex.ec/my-service
83+
image-tag: dev-${{ github.sha }}
84+
registry: docker-regis.iex.ec
85+
push: true
86+
# the jar built above is downloaded into the build context
87+
artifact-name: ${{ needs.build-and-test.outputs.artifact-name }}
88+
artifact-path: build/libs
89+
build-args: jar=${{ needs.build-and-test.outputs.jar-path }}
90+
secrets:
91+
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
92+
dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN_PULL_ONLY }}
93+
username: ${{ secrets.NEXUS_USERNAME }}
94+
password: ${{ secrets.NEXUS_PASSWORD }}
95+
```
96+
97+
## 📝 Notes
98+
99+
- 🔎 The SonarQube Gradle plugin reads the branch, pull request number and repository from the `GITHUB_*` environment
100+
variables, so none of the `sonar.pullrequest.*` or `sonar.branch.name` properties need to be passed.
101+
The `sonar.projectKey` property is derived from SonarQube Gradle plugin and Gradle project properties.
102+
The `sonar.organization` property (mandatory on SonarQube Cloud) is not auto-detected and derived from the calling repository.
103+
The checkout uses `fetch-depth: 0` because Sonar attributes lines to authors through `git blame`.
104+
- 🏷️ The checkout also needs the full history because the iExec `build.gradle` files derive the project version
105+
from the tag pointing at `HEAD`, appending `-NEXT-SNAPSHOT` when there is none.
106+
- 🔄 `refresh-dependencies` defaults to `false`. `setup-gradle` caches the Gradle home between runs — writing it
107+
from the default branch and restoring it read-only elsewhere — so a build can legitimately resolve against a
108+
cache populated days earlier. That only matters for changing modules (`-SNAPSHOT`) and dynamic versions, which
109+
Gradle re-checks at most every 24 hours; enable the input when a project depends on one. For the pinned versions
110+
the iExec projects resolve from jitpack, it forces Gradle to revalidate the metadata of every module on every
111+
build for no benefit.
112+
- 📤 The jar artifact is uploaded flat, so `artifact-path: build/libs` in `docker-build.yml` restores it at the
113+
path reported by the `jar-path` output.
114+
- 🧩 Pure libraries pass `upload-jar: false`: no artifact is uploaded and the `jar-path` output is never set
115+
(it evaluates to an empty string on the caller side, which is safe to ignore).
116+
- 🔐 The workflow declares `permissions: contents: read` + `actions: write`: the first is needed by `checkout`,
117+
the second by `upload-artifact`. An explicit `permissions` block resets every other scope to `none`, so dropping
118+
`actions: write` silently breaks the report artifact.
119+
- 🌐 The `test-reports` artifact (7-day retention) contains the human-readable HTML reports from `build/reports`
120+
(unit tests, JaCoCo coverage). GitHub does not render HTML inline on a PR: open it from PR → **Checks** → the run
121+
→ **Summary** (bottom *Artifacts* section), download, unzip and open locally. `upload-artifact` also exposes an
122+
`artifact-url` output if you later want to post a direct download link as a PR comment.
123+
124+
## 🛠️ Troubleshooting
125+
126+
- **`Could not resolve com.github.iExecBlockchainComputing...`** — the dependency is not on jitpack yet. jitpack
127+
builds a version on first request, so a freshly pushed tag can take a few minutes to become resolvable.
128+
- **`Cannot perform inline analysis, no branch or pull request found`** — the checkout is shallow. This workflow
129+
sets `fetch-depth: 0`; a caller wrapping it differently has to do the same.
130+
- **`You must define the following mandatory properties ... sonar.organization`** — the SonarQube Cloud organization
131+
could not be resolved. Either the repository owner is not a SonarQube Cloud organization key, or the project
132+
lives under a different one. Update the project configuration on SonarQube Cloud.
133+
- **Testcontainers fails pulling an image** — set `dockerhub-login: true` and pass the Docker Hub secrets.

java-build/workflow-sha256

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0cfcab0eef18565a49557ca272394093e9d8d2a45dd09407b037f1ee0268f701

release-please-config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
"component": "docker-build-cloud",
2222
"extra-label": "docker-build-cloud"
2323
},
24+
"java-build": {
25+
"component": "java-build",
26+
"extra-label": "java-build"
27+
},
2428
"propose-safe-multisig-tx": {
2529
"component": "propose-safe-multisig-tx",
2630
"extra-label": "propose-safe-multisig-tx"

0 commit comments

Comments
 (0)