Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .github/workflows/java-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Build and Test Java Project

on:
workflow_call:
inputs:
java-version:
description: "Java version to use"
default: "21"
type: string
refresh-dependencies:
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"
default: false
type: boolean
sonar:
description: "Run the SonarQube analysis (requires the `sonar-token` secret)"
default: false
type: boolean
run-itest:
description: "Run the `itest` Gradle task after the unit tests"
default: false
type: boolean
dockerhub-login:
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"
default: false
type: boolean
upload-jar:
description: "Upload the built jar as an artifact, so that a later job can build an OCI image from it"
default: false
type: boolean
artifact-name:
description: "Name of the uploaded jar artifact"
default: "boot-jar"
type: string
artifact-retention-days:
description: "Retention of the uploaded jar artifact, in days"
default: 1
type: number
secrets:
dockerhub-username:
description: "Docker Hub username (used only when `dockerhub-login: true`)"
required: false
dockerhub-password:
description: "Docker Hub token (used only when `dockerhub-login: true`)"
required: false
sonar-token:
description: "SonarCloud token (used only when `sonar: true`)"
required: false
outputs:
jar-path:
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>`"
value: ${{ jobs.build.outputs.jar-path }}
artifact-name:
description: "Name of the uploaded jar artifact, echoed back for convenience"
value: ${{ jobs.build.outputs.artifact-name }}

permissions:
contents: read # actions/checkout
actions: write # actions/upload-artifact

jobs:
build:
runs-on: ubuntu-latest

outputs:
jar-path: ${{ steps.gradle-properties.outputs.jar-path }}
artifact-name: ${{ inputs.artifact-name }}

steps:
- name: Checkout Repository
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
# Sonar needs the full history to attribute lines to authors, and the Gradle
# build derives the project version from the tag pointing at HEAD.
fetch-depth: 0

- name: Set up JDK
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: ${{ inputs.java-version }}

- name: Set up Gradle
uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0

- name: Login to Docker Hub
if: ${{ inputs.dockerhub-login }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.dockerhub-username }}
password: ${{ secrets.dockerhub-password }}

# Inputs are passed through the environment rather than interpolated into the
# script, so that a crafted input cannot inject shell commands.
- name: Build and run unit tests
env:
REFRESH_DEPENDENCIES: ${{ inputs.refresh-dependencies }}
SONAR_TOKEN: ${{ secrets.sonar-token }}
WITH_SONAR: ${{ inputs.sonar }}
GH_REPO_OWNER: ${{ github.repository_owner }}
GH_REPO_NAME: ${{ github.event.repository.name }}
run: |
args=("build")
if [ "$WITH_SONAR" = "true" ]; then
# The Sonar Gradle plugin reads the branch, pull request and repository from
# the GITHUB_* variables, so no branch or pull request property has to be passed.
# sonar.projectKey is derived from SonarQube Gradle plugin and Gradle project properties.
# sonar.organization (mandatory on SonarQube Cloud) is derived from the repository owner lowercased.
sonar_organization="$(echo "$GH_REPO_OWNER" | tr '[:upper:]' '[:lower:]')"
args+=("sonar")
args+=("-Dsonar.organization=$sonar_organization")
fi
if [ "$REFRESH_DEPENDENCIES" = "true" ]; then
args+=("--refresh-dependencies")
fi
./gradlew "${args[@]}" -i

- name: Run integration tests
if: ${{ inputs.run-itest }}
run: ./gradlew itest -i

- name: Upload test and coverage reports
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: test-reports
path: |
**/build/reports/**
if-no-files-found: ignore
retention-days: 7

- name: Resolve project jar path
id: gradle-properties
if: ${{ inputs.upload-jar }}
run: |
# jarPathForOCI is defined by the iExec Java projects that ship an OCI image.
properties=$(./gradlew -q properties)
jar_path=$(echo "$properties" | awk '/^jarPathForOCI:/ {print $2}')
if [ -z "$jar_path" ]; then
echo "❌ Error: jarPathForOCI not defined in Gradle project"
exit 1
fi
echo "jar-path=$jar_path" | tee -a "$GITHUB_OUTPUT"

- name: Upload jar artifact
if: ${{ inputs.upload-jar }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.artifact-name }}
path: ${{ steps.gradle-properties.outputs.jar-path }}
if-no-files-found: error
retention-days: ${{ inputs.artifact-retention-days }}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Automates the process of publishing NPM packages to the NPM registry with highly

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.

### ☕ [Java Build](./java-build)

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.

### 🦀 [Rust Build](./rust-build)

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.
Expand Down
133 changes: 133 additions & 0 deletions java-build/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# ☕ Java Build Workflow

## 🔍 Overview

This reusable GitHub Actions workflow builds, tests and analyses a Gradle-based Java project.
It is the GitHub Actions counterpart of the `buildJavaProject` Jenkins pipeline used by the iExec middleware
services, and covers the path from a checkout to a jar handed over to an OCI image build.

It serves both services and pure libraries, so anything only a service needs — the Docker Hub login for
Testcontainers, the jar upload — is opt-in and left to the caller.

## ✨ Features

- ☕ Sets up a JDK and Gradle with dependency caching
- 🧪 Runs the unit tests, and optionally an `itest` task
- 🔎 Runs a SonarCloud analysis whose branch and pull request context is auto-detected from the GitHub environment
- 🐳 Optionally logs in to Docker Hub, so Testcontainers-based tests are not hit by anonymous pull rate limits
- 📤 Uploads the built jar as an artifact, ready for `docker-build.yml` to copy into an image
- 📊 Always uploads the Gradle HTML test and coverage reports (`build/reports`), including on failure

## ⚙️ Inputs

| Name | Description | Required | Default |
| ------------------------- | ------------------------------------------------------------------------------------------------ | -------- | ----------------- |
| `artifact-name` | Name of the uploaded jar artifact | No | `"boot-jar"` |
| `artifact-retention-days` | Retention of the uploaded jar artifact, in days | No | `1` |
| `dockerhub-login` | Log in to Docker Hub before the build. Enable it when the tests pull images, e.g. Testcontainers | No | `false` |
| `java-version` | Java version to use | No | `"21"` |
| `refresh-dependencies` | Run Gradle with `--refresh-dependencies` (only useful for `-SNAPSHOT` or dynamic versions) | No | `false` |
| `run-itest` | Run the `itest` Gradle task after the unit tests | No | `false` |
| `sonar` | Run the SonarQube/SonarCloud analysis | No | `false` |
| `upload-jar` | Upload the built jar as an artifact, so that a later job can build an OCI image from it | No | `false` |

## 🔐 Secrets

| Name | Description | Required |
| -------------------- | -------------------------------- | ---------------------------- |
| `dockerhub-username` | Docker Hub username | When `dockerhub-login: true` |
| `dockerhub-password` | Docker Hub token | When `dockerhub-login: true` |
| `sonar-token` | SonarCloud token | When `sonar: true` |

## 📤 Outputs

| Name | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `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 |
| `artifact-name` | Name of the uploaded jar artifact, echoed back for convenience |

## 💻 Example Usage

Building and testing a service, then building an OCI image from the very same jar. `upload-jar` is opt-in
because the workflow also serves pure libraries, which have no image to build and nothing to hand over:

```yaml
name: CI

on:
pull_request:
push:
branches: [main]

jobs:
build-and-test:
# ⚠️ use tagged version here
uses: iExecBlockchainComputing/github-actions-workflows/.github/workflows/java-build.yml@main
with:
java-version: "21"
sonar: true
dockerhub-login: true
# opt in to the jar upload: only projects that ship an OCI image need it
upload-jar: true
secrets:
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN_PULL_ONLY }}
sonar-token: ${{ secrets.SONAR_TOKEN }}

build-image:
needs: build-and-test
# ⚠️ use tagged version here
uses: iExecBlockchainComputing/github-actions-workflows/.github/workflows/docker-build.yml@main
with:
image-name: docker-regis.iex.ec/my-service
image-tag: dev-${{ github.sha }}
registry: docker-regis.iex.ec
push: true
# the jar built above is downloaded into the build context
artifact-name: ${{ needs.build-and-test.outputs.artifact-name }}
artifact-path: build/libs
build-args: jar=${{ needs.build-and-test.outputs.jar-path }}
secrets:
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-password: ${{ secrets.DOCKERHUB_TOKEN_PULL_ONLY }}
username: ${{ secrets.NEXUS_USERNAME }}
password: ${{ secrets.NEXUS_PASSWORD }}
```

## 📝 Notes

- 🔎 The SonarQube Gradle plugin reads the branch, pull request number and repository from the `GITHUB_*` environment
variables, so none of the `sonar.pullrequest.*` or `sonar.branch.name` properties need to be passed.
The `sonar.projectKey` property is derived from SonarQube Gradle plugin and Gradle project properties.
The `sonar.organization` property (mandatory on SonarQube Cloud) is not auto-detected and derived from the calling repository.
The checkout uses `fetch-depth: 0` because Sonar attributes lines to authors through `git blame`.
- 🏷️ The checkout also needs the full history because the iExec `build.gradle` files derive the project version
from the tag pointing at `HEAD`, appending `-NEXT-SNAPSHOT` when there is none.
- 🔄 `refresh-dependencies` defaults to `false`. `setup-gradle` caches the Gradle home between runs — writing it
from the default branch and restoring it read-only elsewhere — so a build can legitimately resolve against a
cache populated days earlier. That only matters for changing modules (`-SNAPSHOT`) and dynamic versions, which
Gradle re-checks at most every 24 hours; enable the input when a project depends on one. For the pinned versions
the iExec projects resolve from jitpack, it forces Gradle to revalidate the metadata of every module on every
build for no benefit.
- 📤 The jar artifact is uploaded flat, so `artifact-path: build/libs` in `docker-build.yml` restores it at the
path reported by the `jar-path` output.
- 🧩 Pure libraries pass `upload-jar: false`: no artifact is uploaded and the `jar-path` output is never set
(it evaluates to an empty string on the caller side, which is safe to ignore).
- 🔐 The workflow declares `permissions: contents: read` + `actions: write`: the first is needed by `checkout`,
the second by `upload-artifact`. An explicit `permissions` block resets every other scope to `none`, so dropping
`actions: write` silently breaks the report artifact.
- 🌐 The `test-reports` artifact (7-day retention) contains the human-readable HTML reports from `build/reports`
(unit tests, JaCoCo coverage). GitHub does not render HTML inline on a PR: open it from PR → **Checks** → the run
→ **Summary** (bottom *Artifacts* section), download, unzip and open locally. `upload-artifact` also exposes an
`artifact-url` output if you later want to post a direct download link as a PR comment.

## 🛠️ Troubleshooting

- **`Could not resolve com.github.iExecBlockchainComputing...`** — the dependency is not on jitpack yet. jitpack
builds a version on first request, so a freshly pushed tag can take a few minutes to become resolvable.
- **`Cannot perform inline analysis, no branch or pull request found`** — the checkout is shallow. This workflow
sets `fetch-depth: 0`; a caller wrapping it differently has to do the same.
- **`You must define the following mandatory properties ... sonar.organization`** — the SonarQube Cloud organization
could not be resolved. Either the repository owner is not a SonarQube Cloud organization key, or the project
lives under a different one. Update the project configuration on SonarQube Cloud.
- **Testcontainers fails pulling an image** — set `dockerhub-login: true` and pass the Docker Hub secrets.
1 change: 1 addition & 0 deletions java-build/workflow-sha256
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0cfcab0eef18565a49557ca272394093e9d8d2a45dd09407b037f1ee0268f701
4 changes: 4 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"component": "docker-build-cloud",
"extra-label": "docker-build-cloud"
},
"java-build": {
"component": "java-build",
"extra-label": "java-build"
},
"propose-safe-multisig-tx": {
"component": "propose-safe-multisig-tx",
"extra-label": "propose-safe-multisig-tx"
Expand Down
Loading