diff --git a/.github/workflows/gradle-build-metadata.yml b/.github/workflows/gradle-build-metadata.yml
new file mode 100644
index 0000000..73364ff
--- /dev/null
+++ b/.github/workflows/gradle-build-metadata.yml
@@ -0,0 +1,123 @@
+name: Build Metadata
+
+on:
+ workflow_call:
+ inputs:
+ gradle-version:
+ description: Version returned from get-gradle-version workflow.
+ required: true
+ type: string
+
+ outputs:
+ artifact-version:
+ description: Final artifact version.
+ value: ${{ jobs.metadata.outputs.artifact-version }}
+ docker-registry:
+ description: Docker Hub organization.
+ value: ${{ jobs.metadata.outputs.docker-registry }}
+ is-release:
+ description: Whether this is a release build.
+ value: ${{ jobs.metadata.outputs.is-release }}
+ is-main-branch:
+ description: Whether this build is running on the default branch.
+ value: ${{ jobs.metadata.outputs.is-main-branch }}
+ do-docker-push:
+ description: Whether Docker images should be pushed.
+ value: ${{ jobs.metadata.outputs.do-docker-push }}
+ repo-name:
+ description: Name of the GitHub Repository.
+ value: ${{ jobs.metadata.outputs.repo-name}}
+ repo-description:
+ description: Description of the GitHub Repository.
+ value: ${{ jobs.metadata.outputs.repo-description }}
+
+jobs:
+ metadata:
+ name: Generate Build Metadata
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ outputs:
+ artifact-version: ${{ steps.metadata.outputs.artifact-version }}
+ docker-registry: ${{ steps.metadata.outputs.docker-registry }}
+ is-release: ${{ steps.metadata.outputs.is-release }}
+ is-main-branch: ${{ steps.metadata.outputs.is-main-branch }}
+ do-docker-push: ${{ steps.metadata.outputs.do-docker-push }}
+ repo-name: ${{ steps.metadata.outputs.repo-name }}
+ repo-description: ${{ steps.metadata.outputs.repo-description }}
+
+ steps:
+ - name: Generate metadata
+ id: metadata
+ shell: bash
+ env:
+ GRADLE_VERSION: ${{ inputs.gradle-version }}
+ RUN_NUMBER: ${{ github.run_number }}
+ REF_TYPE: ${{ github.ref_type }}
+ REF_NAME: ${{ github.ref_name }}
+ EVENT_NAME: ${{ github.event_name }}
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ REPO_DESC: ${{ github.event.repository.description }}
+ run: |
+ BASE_NUMBER=2000
+ BUILD_NUMBER=$(( BASE_NUMBER + RUN_NUMBER ))
+ echo "Gradle version: ${GRADLE_VERSION}"
+ echo "Git ref: ${REF_NAME} (${REF_TYPE})"
+
+ if [[ "${REF_TYPE}" == "tag" ]]; then
+ TAG_VERSION="${REF_NAME#v}"
+ if [[ "${TAG_VERSION}" != "${GRADLE_VERSION}" ]]; then
+ echo "ERROR: Git tag '${REF_NAME}' does not match project version '${GRADLE_VERSION} from gradle.properties."
+ exit 1
+ fi
+ ARTIFACT_VERSION="${GRADLE_VERSION}"
+ IS_RELEASE=true
+ DOCKER_REGISTRY="folioorg"
+ elif [[ "${GRADLE_VERSION}" == *"-SNAPSHOT" ]]; then
+ ARTIFACT_VERSION="${GRADLE_VERSION}.${BUILD_NUMBER}"
+ IS_RELEASE=false
+ DOCKER_REGISTRY="folioci"
+ else
+ ARTIFACT_VERSION="${GRADLE_VERSION}"
+ IS_RELEASE=false
+ DOCKER_REGISTRY="folioci"
+ fi
+
+ if [[ "${REF_NAME}" == "${DEFAULT_BRANCH}" ]]; then
+ IS_MAIN_BRANCH=true
+ else
+ IS_MAIN_BRANCH=false
+ fi
+
+ if [[ "${EVENT_NAME}" == "pull_request" ]]; then
+ DO_DOCKER_PUSH=false
+ elif [[ "${IS_RELEASE}" == "true" ]]; then
+ DO_DOCKER_PUSH=true
+ elif [[ "${GRADLE_VERSION}" == *"-SNAPSHOT" && "${IS_MAIN_BRANCH}" == "true" ]]; then
+ DO_DOCKER_PUSH=true
+ else
+ DO_DOCKER_PUSH=false
+ fi
+
+ {
+ echo "artifact-version=${ARTIFACT_VERSION}"
+ echo "docker-registry=${DOCKER_REGISTRY}"
+ echo "is-release=${IS_RELEASE}"
+ echo "is-main-branch=${IS_MAIN_BRANCH}"
+ echo "do-docker-push=${DO_DOCKER_PUSH}"
+ echo "repo-name=${GITHUB_REPOSITORY##*/}"
+ echo "repo-description=${REPO_DESC}"
+ } >> "$GITHUB_OUTPUT"
+
+ {
+ echo "### Build Metadata"
+ echo ""
+ echo "| Property | Value |"
+ echo "|----------|-------|"
+ echo "| Artifact Version | ${ARTIFACT_VERSION} |"
+ echo "| Docker Registry | ${DOCKER_REGISTRY} |"
+ echo "| Release Build | ${IS_RELEASE} |"
+ echo "| Main Branch | ${IS_MAIN_BRANCH} |"
+ echo "| Docker Push | ${DO_DOCKER_PUSH} |"
+ echo "| Git Ref | ${REF_NAME} (${REF_TYPE}) |"
+ } >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml
new file mode 100644
index 0000000..aecdb12
--- /dev/null
+++ b/.github/workflows/gradle-build.yml
@@ -0,0 +1,69 @@
+name: Gradle Build
+
+on:
+ workflow_call:
+ inputs:
+ gradle-directory:
+ description: Directory containing the Gradle project.
+ required: true
+ type: string
+ java-version:
+ description: Java version.
+ required: true
+ type: string
+ build-command:
+ description: Gradle command.
+ required: false
+ type: string
+ default: "assemble"
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: read
+ defaults:
+ run:
+ shell: bash
+ working-directory: ${{ inputs.gradle-directory }}
+
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+ submodules: recursive
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/actions/wrapper-validation@v6
+
+ - name: Setup Java
+ uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: ${{ inputs.java-version }}
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v6
+
+ - name: Build Project
+ run: |
+ ./gradlew \
+ ${{ inputs.build-command }} \
+ --console plain \
+ --no-daemon
+
+ - name: Upload application JAR
+ uses: actions/upload-artifact@v7
+ with:
+ name: built-jars
+ path: |
+ ${{ inputs.gradle-directory }}/build/libs/*.jar
+
+ - name: Upload ModuleDescriptor
+ uses: actions/upload-artifact@v7
+ with:
+ name: ModuleDescriptor.json
+ path: |
+ ${{ inputs.gradle-directory }}/build/resources/main/okapi/ModuleDescriptor.json
diff --git a/.github/workflows/gradle-dependency-submission.yml b/.github/workflows/gradle-dependency-submission.yml
new file mode 100644
index 0000000..e456c81
--- /dev/null
+++ b/.github/workflows/gradle-dependency-submission.yml
@@ -0,0 +1,48 @@
+name: Dependency Submission
+
+on:
+ workflow_call:
+ inputs:
+ gradle-directory:
+ description: Directory containing the Gradle project.
+ required: true
+ type: string
+ java-version:
+ description: Java version.
+ required: true
+ type: string
+
+jobs:
+ dependency-submission:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: write
+ defaults:
+ run:
+ shell: bash
+ working-directory: ${{ inputs.gradle-directory }}
+
+ steps:
+ - name: Checkout source
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+ submodules: recursive
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/actions/wrapper-validation@v6
+
+ - name: Setup Java
+ uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: ${{ inputs.java-version }}
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v6
+
+ - name: Submit Dependency Graph
+ uses: gradle/actions/dependency-submission@v6
+ with:
+ build-root-directory: ${{ inputs.gradle-directory }}
diff --git a/.github/workflows/gradle-docker-publish.yml b/.github/workflows/gradle-docker-publish.yml
new file mode 100644
index 0000000..b2dad9a
--- /dev/null
+++ b/.github/workflows/gradle-docker-publish.yml
@@ -0,0 +1,166 @@
+name: Gradle Docker Build and Publish
+
+on:
+ workflow_call:
+ inputs:
+ artifact-id:
+ description: Docker image name.
+ required: true
+ type: string
+ artifact-version:
+ description: Artifact version.
+ required: true
+ type: string
+ docker-registry:
+ description: Docker registry/organization.
+ required: true
+ type: string
+ docker-health-command:
+ description: Docker health check command.
+ required: false
+ type: string
+ default: ''
+ do-docker-push:
+ description: Whether to publish the Docker image.
+ required: true
+ type: boolean
+ docker-label-documentation:
+ description: Documentation URL for OCI label.
+ required: false
+ type: string
+ default: ''
+ secrets:
+ dockerhub-username:
+ required: true
+ dockerhub-token:
+ required: true
+
+jobs:
+ docker:
+ name: Docker Build${{ inputs.do-docker-push && ' and Publish' || '' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ permissions:
+ contents: read
+
+ steps:
+ # Checkout
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+
+ # Download Gradle build artifacts
+ - name: Download built JARs
+ uses: actions/download-artifact@v8
+ with:
+ name: built-jars
+ path: .
+
+ - name: Restore Gradle build directory
+ run: |
+ mkdir -p service/build/libs
+ mv -- *.jar service/build/libs/
+
+ # Notify whether publishing Docker image
+ - name: Notify whether doing Docker push
+ run: |
+ if ${{ inputs.do-docker-push }}; then
+ echo "Will build and publish Docker image." | tee -a "$GITHUB_STEP_SUMMARY"
+ else
+ echo "Will build Docker image only (publish=false)." | tee -a "$GITHUB_STEP_SUMMARY"
+ fi
+
+ # Login to Docker Hub
+ - name: Login to Docker Hub
+ if: inputs.do-docker-push
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.dockerhub-username }}
+ password: ${{ secrets.dockerhub-token }}
+
+ # Extract Docker metadata
+ - name: Extract Docker metadata
+ id: meta
+ uses: docker/metadata-action@v6
+ with:
+ images: ${{ inputs.docker-registry }}/${{ inputs.artifact-id }}
+
+ labels: |
+ org.opencontainers.image.title=FOLIO ${{ inputs.artifact-id }}
+ org.opencontainers.image.documentation=${{ inputs.docker-label-documentation }}
+ org.opencontainers.image.vendor=The Open Library Foundation
+ org.opencontainers.image.licenses=Apache-2.0
+ org.opencontainers.image.version=${{ inputs.artifact-version }}
+
+ tags: |
+ type=raw,value=latest
+ type=raw,value=${{ inputs.artifact-version }}
+
+ # Prepare Docker test image
+ - name: Prepare Docker test tag
+ id: prepare-docker-test-tag
+ run: |
+ echo "docker-test-tag=folioci/${{ inputs.artifact-id }}:test" >> "$GITHUB_OUTPUT"
+
+ if ${{ inputs.docker-health-command != '' }}; then
+ echo "docker-health-command='${{ inputs.docker-health-command }}'" | tee -a "$GITHUB_STEP_SUMMARY"
+ else
+ echo "> [!WARNING]" >> "$GITHUB_STEP_SUMMARY"
+ {
+ echo "> Not doing docker health check. The inputs.docker-health-command is not declared."
+ echo "> The docker image might not be reliable."
+ echo "> Refer to [documentation](https://github.com/folio-org/.github/blob/master/README-maven.md#configuration-docker-health-command)."
+ } | tee -a "$GITHUB_STEP_SUMMARY"
+ fi
+
+ # Build and validate Docker image
+ - name: Build and test Docker image
+ if: inputs.docker-health-command != ''
+ run: |
+ echo "Building test image..."
+ docker build \
+ --pull=true \
+ --no-cache=true \
+ -t "${{ steps.prepare-docker-test-tag.outputs.docker-test-tag }}" .
+
+ echo "Starting test container..."
+
+ docker run \
+ --detach \
+ --health-timeout=2s \
+ --health-retries=2 \
+ --cidfile docker_test.cid \
+ --health-cmd='${{ inputs.docker-health-command }}' \
+ "${{ steps.prepare-docker-test-tag.outputs.docker-test-tag }}"
+
+ cid="$(cat docker_test.cid)"
+ echo "Waiting for container health..."
+ max_startup_wait=60
+ for ((i=1;i<=max_startup_wait;i++)); do
+ health=$(docker inspect "$cid" | jq -r '.[0].State.Health.Status')
+ echo "Current status: ${health}"
+ if [[ "${health}" == "starting" ]]; then
+ sleep 1
+ else
+ break
+ fi
+ done
+ if [[ "${health}" != "healthy" ]]; then
+ echo "Container health check failed."
+ echo "========== Docker Logs =========="
+ docker logs "$cid" || true
+ echo "========== Docker Inspect =========="
+ docker inspect "$cid" || true
+ exit 1
+ fi
+ echo "Container health check passed."
+
+ # Build and optionally publish Docker image
+ - name: Build${{ inputs.do-docker-push && ' and Publish' || '' }} Docker image
+ uses: docker/build-push-action@v7
+ with:
+ context: .
+ push: ${{ inputs.do-docker-push }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/gradle-get-version-number.yml b/.github/workflows/gradle-get-version-number.yml
new file mode 100644
index 0000000..01ebe71
--- /dev/null
+++ b/.github/workflows/gradle-get-version-number.yml
@@ -0,0 +1,44 @@
+name: Get Gradle Version
+
+on:
+ workflow_call:
+ inputs:
+ gradle-directory:
+ description: Directory containing the Gradle project.
+ required: true
+ type: string
+
+ outputs:
+ gradle-version:
+ description: Version reported by Gradle.
+ value: ${{ jobs.version.outputs.gradle-version }}
+
+jobs:
+ version:
+ name: Determine Gradle Version
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ outputs:
+ gradle-version: ${{ steps.version.outputs.gradle-version }}
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 1
+
+ - name: Validate Gradle Wrapper
+ uses: gradle/actions/wrapper-validation@v6
+
+ - name: Read Gradle version
+ id: version
+ working-directory: ${{ inputs.gradle-directory }}
+ shell: bash
+ run: |
+ VERSION="$(awk -F= '/^appVersion=/ {print $2}' gradle.properties)"
+ if [[ -z "$VERSION" ]]; then
+ echo "Unable to determine Gradle project version."
+ exit 1
+ fi
+ echo "Detected Gradle version: $VERSION"
+ echo "gradle-version=$VERSION" >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/gradle-module-descriptor-publish.yml b/.github/workflows/gradle-module-descriptor-publish.yml
new file mode 100644
index 0000000..f46c4f6
--- /dev/null
+++ b/.github/workflows/gradle-module-descriptor-publish.yml
@@ -0,0 +1,46 @@
+name: Publish ModuleDescriptor
+
+on:
+ workflow_call:
+ inputs:
+ module-descriptor-registry:
+ description: URL of the ModuleDescriptor registry.
+ required: true
+ type: string
+ secrets:
+ registry-username:
+ required: true
+ registry-password:
+ required: true
+
+jobs:
+ publish:
+ name: Publish ModuleDescriptor
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: read
+ steps:
+ # Download ModuleDescriptor produced by gradle-build.yml
+ - name: Download ModuleDescriptor
+ uses: actions/download-artifact@v8
+ with:
+ name: ModuleDescriptor.json
+
+ # Publish ModuleDescriptor
+ - name: Publish ModuleDescriptor
+ uses: fjogeleit/http-request-action@v2
+ with:
+ url: ${{ inputs.module-descriptor-registry }}/_/proxy/modules
+ method: POST
+ contentType: application/json; charset=utf-8
+ customHeaders: >
+ {
+ "Accept": "application/json; charset=utf-8"
+ }
+ timeout: 10000
+ retry: 10
+ retryWait: 21000
+ file: ModuleDescriptor.json
+ username: ${{ secrets.registry-username }}
+ password: ${{ secrets.registry-password }}
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
new file mode 100644
index 0000000..5e3b045
--- /dev/null
+++ b/.github/workflows/gradle.yml
@@ -0,0 +1,136 @@
+name: Gradle Backend Module
+
+on:
+ workflow_call:
+ inputs:
+ artifact-id:
+ description: Artifact/module name (e.g. mod-agreements).
+ required: true
+ type: string
+ gradle-directory:
+ description: Directory containing the Gradle project.
+ required: false
+ default: service
+ type: string
+ publish-module-descriptor:
+ description: Publish module descriptor?
+ required: false
+ type: boolean
+ default: true
+ module-descriptor-registry:
+ description: Okapi ModuleDescriptor registry URL.
+ required: false
+ default: https://folio-registry.dev.folio.org
+ type: string
+ docker-health-command:
+ description: Docker health check command.
+ required: false
+ default: ""
+ type: string
+ docker-label-documentation:
+ description: OCI documentation label.
+ required: false
+ default: ""
+ type: string
+ java-version:
+ description: Java version.
+ required: false
+ type: string
+ default: "17"
+ secrets:
+ DOCKERHUB_USERNAME:
+ required: true
+ DOCKERHUB_TOKEN:
+ required: true
+ FOLIO_REGISTRY_USERNAME:
+ required: true
+ FOLIO_REGISTRY_PASSWORD:
+ required: true
+
+# Determine Gradle version
+jobs:
+ get-gradle-version:
+ name: Determine Gradle Version
+ uses: ./.github/workflows/gradle-get-version-number.yml
+ with:
+ gradle-directory: ${{ inputs.gradle-directory }}
+
+# Build metadata
+ metadata:
+ name: Build Metadata
+ needs:
+ - get-gradle-version
+ uses: ./.github/workflows/gradle-build-metadata.yml
+ with:
+ gradle-version: ${{ needs.get-gradle-version.outputs.gradle-version }}
+
+# Build application
+ gradle-build:
+ name: Gradle Build
+ needs:
+ - metadata
+ uses: ./.github/workflows/gradle-build.yml
+ with:
+ gradle-directory: ${{ inputs.gradle-directory }}
+ java-version: ${{ inputs.java-version }}
+
+# Dependency submission
+ dependency-submission:
+ name: Dependency Submission
+ needs:
+ - gradle-build
+ uses: ./.github/workflows/gradle-dependency-submission.yml
+ with:
+ gradle-directory: ${{ inputs.gradle-directory }}
+ java-version: ${{ inputs.java-version }}
+
+# Docker build
+ docker-build:
+ name: Docker Build
+ needs:
+ - metadata
+ - gradle-build
+ uses: ./.github/workflows/gradle-docker-publish.yml
+ with:
+ artifact-id: ${{ inputs.artifact-id }}
+ artifact-version: ${{ needs.metadata.outputs.artifact-version }}
+ docker-registry: ${{ needs.metadata.outputs.docker-registry }}
+ docker-health-command: ${{ inputs.docker-health-command }}
+ docker-label-documentation: ${{ inputs.docker-label-documentation }}
+ do-docker-push: ${{ fromJSON(needs.metadata.outputs.do-docker-push) }}
+ secrets:
+ dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
+ dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN}}
+
+# Docker image description
+ docker-description:
+ name: Publish image description
+ needs:
+ - metadata
+ - docker-build
+ if: |
+ !cancelled() && needs.metadata.outputs.do-docker-push == 'true'
+ uses: ./.github/workflows/docker-description.yml
+ with:
+ artifact-id: ${{ inputs.artifact-id }}
+ docker-registry: ${{ needs.metadata.outputs.docker-registry }}
+ repo-description: ${{ needs.metadata.outputs.repo-description }}
+ publish-module-descriptor: ${{ inputs.publish-module-descriptor }}
+ secrets:
+ dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
+ dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
+
+# Publish ModuleDescriptor
+ publish-module-descriptor:
+ name: Publish ModuleDescriptor
+ if: |
+ !cancelled() && inputs.publish-module-descriptor && needs.metadata.outputs.do-docker-push == 'true'
+ needs:
+ - metadata
+ - docker-build
+ uses: ./.github/workflows/gradle-module-descriptor-publish.yml
+ with:
+ module-descriptor-registry: ${{ inputs.module-descriptor-registry }}
+ secrets:
+ registry-username: ${{ secrets.FOLIO_REGISTRY_USERNAME }}
+ registry-password: ${{ secrets.FOLIO_REGISTRY_PASSWORD }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e338b0..94a0ff6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## IN PROGRESS
* FOLIO-4555 Double-quote variables - in #164, #166
+* FOLIO-4554 Create Reusable workflows for Gradle based modules - in #165
* (Add more progress summary items here.)
## [1.16.3](https://github.com/folio-org/.github/tree/v1.16.3) (2026-07-14)
diff --git a/README-gradle.md b/README-gradle.md
new file mode 100644
index 0000000..589efa8
--- /dev/null
+++ b/README-gradle.md
@@ -0,0 +1,346 @@
+# Centralised GitHub Workflows for Gradle
+
+
+* [Introduction](#introduction)
+* [Usage](#usage)
+* [Configuration](#configuration)
+ * [Configuration: java-version](#configuration-java-version)
+ * [Configuration: publish-module-descriptor](#configuration-publish-module-descriptor)
+ * [Configuration: allow-snapshots-release](#configuration-allow-snapshots-release)
+ * [Configuration: apt-packages](#configuration-apt-packages)
+ * [Configuration: do-sonar-scan](#configuration-do-sonar-scan)
+ * [Configuration: do-docker](#configuration-do-docker)
+ * [Configuration: docker-enable-other-artifacts](#configuration-docker-enable-other-artifacts)
+ * [Configuration: docker-health-command](#configuration-docker-health-command)
+ * [Configuration: docker-label-documentation](#configuration-docker-label-documentation)
+* [Docker image metadata](#docker-image-metadata)
+* [Install the caller Workflow](#install-the-caller-workflow)
+* [Release procedures](#release-procedures)
+ * [Release procedures FAQ](#release-procedures-faq)
+* [Limitations](#limitations)
+ * [Only top-level Dockerfile](#only-top-level-dockerfile)
+* [Oddities](#oddities)
+ * [Timeout at ModuleDescriptor registry](#timeout-at-moduledescriptor-registry)
+
+## Introduction
+
+The Workflows in this repository named `maven*.yml` are for building Maven-based back-end modules.
+Docker images are published to FOLIO Docker Hub.
+ModuleDescriptors are published to the FOLIO Registry.
+
+Refer to example build system and workflows at https://github.com/folio-org/mod-settings
+
+## Usage
+
+Create a `.github/workflows` directory in the root of the module repository, and add a file named `maven.yml` with the following content.
+
+If there is already a workflow named maven.yml for verifying basic Maven builds, then rename that file.
+It will ease management to have the same filename at every repository.
+
+Follow [Install the caller Workflow](#install-the-caller-workflow) section below to install the initial workflow.
+
+After the first Actions run, do not rename the filename of this caller workflow, as that will reset the GitHub run number and so wreck the sequential order of the ModuleDescriptor identifiers.
+
+
+```yaml
+# https://github.com/folio-org/.github/blob/master/README-maven.md
+
+name: Maven central workflow
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ maven:
+ uses: folio-org/.github/.github/workflows/maven.yml@v1
+ # Only handle push events from the main branch or tags, to decrease PR noise
+ if: github.ref_name == github.event.repository.default_branch || github.event_name != 'push' || github.ref_type == 'tag'
+ secrets: inherit
+```
+
+## Configuration
+
+If there is a need to over-ride defaults, then add configuration variables to the single "with:" section of the module maven.yml Workflow.
+
+Add the section at the end of the Workflow immediately after the "secrets" item.
+For example:
+
+```yaml
+ # ...
+ secrets: inherit
+ with:
+ java-version: '17'
+ # Add configuration variables here if needed.
+```
+
+### Configuration: java-version
+
+Allowed values: 17 or 21 or 25
+
+Optional. Default = '21'
+
+```yaml
+ with:
+ java-version: '17'
+```
+
+### Configuration: publish-module-descriptor
+
+Some Maven-based projects do not have a ModuleDescriptor.
+
+Optional. Default = true
+
+```yaml
+ with:
+ publish-module-descriptor: false
+```
+
+### Configuration: allow-snapshots-release
+
+Normally a release must not use dependencies that are "snapshot" versions.
+
+On rare occasions this might be needed.
+
+Optional. Default = false
+
+```yaml
+ with:
+ allow-snapshots-release: true
+```
+
+### Configuration: apt-packages
+
+Some Maven-based repositories need extra dependencies, e.g. mod-copycat.
+
+There is a restricted list of packages that can be installed via apt-get. \
+The current list: 'libyaz5'
+
+Note that the GitHub runner already provides various other software.
+Visit the "Set up job > Runner Image" section of a recent run to see the "Included Software" list.
+
+If there is another dependency needed, then follow the FAQ [How to raise a DevOps Jira ticket](https://dev.folio.org/faqs/how-to-raise-devops-ticket/).
+
+This configuration variable is a comma-separated list.
+
+Optional. Default = '' (i.e. none)
+
+```yaml
+ with:
+ apt-packages: 'libyaz5'
+```
+
+### Configuration: do-sonar-scan
+
+Sonar can be disabled if that is needed, for example when a new project is not yet ready to commence the scans.
+
+Optional. Default = true
+
+```yaml
+ with:
+ do-sonar-scan: false
+```
+
+### Configuration: do-docker
+
+Some Maven-based projects do not utilise Docker. If so then provide this variable as "false".
+
+If this variable is "false", then also no ModuleDescriptor will be published.
+
+> [!NOTE]
+> See [Limitations - Only top-level Dockerfile](#only-top-level-dockerfile) at this stage.
+
+Optional. Default = true
+
+```yaml
+ with:
+ do-docker: false
+```
+
+### Configuration: docker-enable-other-artifacts
+
+If this variable is provided and "true", then do download the "build-artifacts" artifact which was prepared via the earlier job.
+(Otherwise only the default "built-jars" artifact is downloaded.)
+
+This will include everything from the "target" directory.
+
+Be sure to use the `.dockerignore` file to filter only the desired pieces.
+
+Optional. Default = false
+
+```yaml
+ with:
+ docker-enable-other-artifacts: true
+```
+
+### Configuration: docker-health-command
+
+If this variable is provided, then the Docker Health Check will be run prior to the final building of the image.
+If it fails, then no Docker image is built, and a ModuleDescriptor will not be published.
+
+> [!IMPORTANT]
+> The Health Check is required for Docker-providing modules.
+> Refer to [DR-000007 - Back End Module Health Check Protocol](https://folio-org.atlassian.net/wiki/x/kiJN).
+
+Note that the workflow will utilise this variable if provided, but does not enforce it.
+The status will be reported to the workflow "Summary".
+
+Optional. Default = None
+
+```yaml
+ with:
+ docker-health-command: 'wget --no-verbose --tries=1 --spider http://localhost:8081/admin/health || exit 1'
+```
+
+### Configuration: docker-label-documentation
+
+If not provided then the "org.opencontainers.image.documentation" label of the Docker image will be empty.
+
+Optional. Default = None
+
+```yaml
+ with:
+ docker-label-documentation: 'https://.../documentation.md'
+```
+
+## Docker image metadata
+
+The docker image will have various labels automatically applied.
+
+Note: If the "org.opencontainers.image.description" label of the generated image is empty, then that is because the module's GitHub repository is missing the "About" description in the top-right corner of its GitHub front page.
+See advice at [Create a new FOLIO module and do initial setup](https://dev.folio.org/guidelines/create-new-repo/),
+and bear in mind that Docker Hub imposes a [content length limit](https://github.com/peter-evans/dockerhub-description#content-limits) of 100 bytes for that short-description, so it will be truncated at that.
+
+See also the [Configuration: docker-label-documentation](#configuration-docker-label-documentation) variable.
+
+## Install the caller Workflow
+
+> [!NOTE]
+> If there is not yet a JIRA ticket at the co-ordination Epic [FOLIO-4443](https://folio-org.atlassian.net/browse/FOLIO-4443) then please raise one in a similar manner to the others, and add that as the Parent.
+
+Create a new branch at the module repository.
+
+Create a file at `.github/workflows/maven.yml` as explained at the [Usage](#usage) section.
+
+Add other [Configuration](#configuration) variables to suit the needs of the module, e.g. `docker-health-command` variable.
+Align properties with the old Jenkinsfile (noting the defaults shown in the [Configuration](#configuration) section).
+
+Do `git mv Jenkinsfile Jenkinsfile-disabled` (so that it can be restored quickly if needed, and still be able to review its properties).
+
+Commit and push.
+
+(If it is desired to do a branch run prior to raising the pull-request, then "dispatch" the workflow on that branch.
+However the line 12 "if:" will need to be temporarily commented-out for one run, because the workflow does not yet exist on mainline branch.)
+
+Raise the pull-request, and review the run results.
+
+The merge will be denied. The "check" for the old Jenkins "pr-merge" will fail.
+
+Edit "Branch protection" to delete that check, and add a new `GitHub Actions` check:
+
+For most Docker-providing repositories the check will be: \
+`maven / docker-publish / Docker build`
+
+For non-Docker repositories the check will be: \
+`maven / Build / Build`
+
+If assistance is needed with "Branch protection" then [contact](https://dev.folio.org/faqs/how-to-raise-devops-ticket/#general-folio-devops) FOLIO DevOps and advise the checks that you need.
+
+Wait until after the next "Platform build" to give some time if things go amiss.
+https://dev.folio.org/guides/automation/#platform-hourly-build (finishes approx 53m past)
+https://github.com/folio-org/platform-complete/commits/snapshot/
+
+Merge and watch the mainline branch run.
+
+Review the results for the Docker image and ModuleDescriptor. The identifier for all modules will use base number 2000 plus the sequential workflow run_number (e.g. 2002 for the second run).
+
+Visit the following resources (adjusted for the relevant repository name):
+* https://hub.docker.com/r/folioci/mod-settings/tags
+* https://hub.docker.com/r/folioci/mod-settings (for new generated description)
+* https://folio-registry.dev.folio.org/_/proxy/modules?filter=mod-settings&latest=1
+* https://folio-registry.dev.folio.org/_/proxy/modules?filter=mod-settings&latest=1&full=true
+* https://repository.folio.org/#browse/browse:maven-snapshots:org%2Ffolio%2Fmod-settings
+* https://sonarcloud.io/project/overview?id=org.folio:mod-settings
+
+Await success of the subsequent "Platform hourly build" and see snapshot branch updated.
+
+If there is a need to quickly revert to Jenkins-based build, then [delete](https://github.com/folio-org/mod-settings/blob/master/.github/workflows/delete-test-md.yml) the published ModuleDescriptor (with great care), re-configure the branch protection checks, restore the Jenkinsfile.
+
+## Release procedures
+
+1. Create a temporary release branch `tmp-release-X.Y.Z`;
+2. Commit all relevant changes and this release's date to `NEWS.md`;
+ - `git log --pretty=format:"%s" $(git describe --tags --abbrev=0)..HEAD | grep -e '^.[A-Z]\+-[0-9]\+' | sort -u` can be used to grab all commits with Jira-like names since the last tag
+4. Run `mvn -DautoVersionSubmodules=true release:clean release:prepare` and follow the interactive instructions:
+ - Ensure all snapshot dependencies are resolved (unless the workflow has [allow-snapshots-release](#configuration-allow-snapshots-release) enabled),
+ - Use the format `vX.Y.Z` for the created tag,
+ - Set the new development version by:
+ - Incrementing the **minor** version for regular releases (`X.Y+1.0`) or
+ - Incrementing the **patch version** for bugfix releases (`X.Y.Z+1`);
+5. Push the temporary branch to GitHub and create a pull request against the mainline branch;
+6. Once the PR passes, merge the pull request (do _not_ use a squash commit — merge the full release branch history) and push the tag (`git push --tags`);
+7. Wait for the tag's GitHub Actions build to run (you can find it in the list under the `Actions` tab — look for the middle column specifying the tag's name);
+8. Announce it to the world:
+ - Create a release on GitHub using the tag already pushed; the description should be the same as the entries in `NEWS.md` and `latest` should be set if applicable;
+ - Send an annoucement to [#folio-releases on Slack](https://open-libr-foundation.slack.com/archives/CGPMHLX9B);
+ - Ensure all applicable Jira tickets were given the proper `Fix version`; and
+ - Mark the Jira version as released and create a new one; and
+9. Prepare for future development locally by running `mvn release:clean`.
+
+### Release procedures FAQ
+
+
+ Can't use Maven's release plugin due to failing tests?
+
+ If you are unable to use the Maven release plugin due to test issues (e.g. unable to run tests on your machine's architecture), you may skip tests with the following command:
+ ```sh
+ mvn -DskipTests -Darguments=-DskipTests -DautoVersionSubmodules=true release:clean release:prepare
+ ```
+
+
+
+ Don't want to use Maven's release plugin whatsoever?
+
+ If you don't want to use the release plugin, you may perform its steps manually. Instead of running step 3 manually, do the following (and then resume the normal release procedure):
+ 1. Resolve all snapshot dependencies, remove the `-SNAPSHOT` from the POM's current version, and set the source control's `` to the current `vX.Y.Z` (see [this example](https://github.com/folio-org/mod-lists/pull/265/changes/b00c3820f01f741f22c94bd21a703e357883ff95));
+ 2. Commit these changes to your branch and create a tag `vX.Y.Z`;
+ 3. Restore snapshot dependencies as applicable, restore the source control's tag to `HEAD`, and set the POM's version to the next snapshot; and
+ 4. Commit these changes as a separate commit.
+
+
+
+ Doing a hotfix to an older branch of your repository that still uses Jenkins?
+
+ You have two options if you need to release on an older branch that does not use the new GitHub Actions workflow.
+
+ However note that Option 1 is preferred because Jenkins might go away soon.
+
+ 1. Migrate the branch to GitHub Actions (see [Usage](#usage)); or
+ 2. Use Jenkins for the release.
+
+ If using Jenkins, everything will be the same except step six. Instead, navigate to the tag's build page at `https://jenkins-aws.indexdata.com/job/folio-org/job/mod-MY-MODULE/view/tags/job/vX.Y.Z/` and click `Build now` in the sidebar. Once this is complete, resume the normal release procedure.
+
+
+> [!NOTE]
+>
+> Skipping local test execution (in the plugin or by bypassing the plugin entirely) will only skip tests locally — tests still **must** pass in GitHub Actions for the release to proceed.
+
+## Limitations
+
+### Only top-level Dockerfile
+
+At this stage only a top-level Dockerfile is utilised. So these Workflows are not yet ready for projects that have lower-level Dockerfile.
+
+## Oddities
+
+### Timeout at ModuleDescriptor registry
+
+Occasionally the job to "Publish ModuleDescriptor" gets a timeout at the registry.
+
+In this case the Docker image would be published but not the associated ModuleDescriptor.
+
+Either re-run that failed job, or "dispatch" the complete workflow again to publish a new Docker image and ModuleDescriptor.