diff --git a/.github/workflows/java-build.yml b/.github/workflows/java-build.yml new file mode 100644 index 0000000..16ae96d --- /dev/null +++ b/.github/workflows/java-build.yml @@ -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=`" + 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 }} diff --git a/README.md b/README.md index 99f50da..dc403d5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/java-build/README.md b/java-build/README.md new file mode 100644 index 0000000..4693581 --- /dev/null +++ b/java-build/README.md @@ -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=`. 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. diff --git a/java-build/workflow-sha256 b/java-build/workflow-sha256 new file mode 100644 index 0000000..504e8e2 --- /dev/null +++ b/java-build/workflow-sha256 @@ -0,0 +1 @@ +0cfcab0eef18565a49557ca272394093e9d8d2a45dd09407b037f1ee0268f701 diff --git a/release-please-config.json b/release-please-config.json index 274a1ec..ae92cb1 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -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"