Skip to content

Conversation

@petersmythe
Copy link
Contributor

I want to change the build approach from the one run on Jenkins (release.sh) to a multi GitHub runner (on native AMD64 and ARM64 platforms) that is far more efficient and runs in a fraction of the time.

Please review the new publish.yml which is manually triggered (workflow_dispatch) from a new tiny job in Jenkins that would replace the previous one, passing the GeoServer version and build number along.

The publish.yml GitHub action ("Build and Publish Multi-Arch GeoServer Images") creates a matrix of non-GDAL and GDAL and AMD64 and ARM64 jobs, and once they are all finished, pulls all images together into 2 multi-architecture images (one for each of non-GDAL and GDAL) as can be seen and used from here: https://hub.docker.com/repository/docker/petersmythe/geoserver-test

Also, various -latest and -nightly tags are added for users convenience e.g.
image

All images are multi-architecture: currently: AMD64 and ARM64

There are -nightly build tags for each named series (the current main (blank), stable and maintenance series)

There are -latest release tags for each named series above (released approximately every second month)

There are -latest release tags for a specific major.minor version (e.g. 3.0, 2.28, 2.27)

All images have an optional -gdal variant which includes the GDAL libraries built for that architecture

Finally, the publish.yml action can publish to the OSGeo repo, where the existing 2 AMD64 images reside. I have not tested this yet.

The whole process has been extensively tested on Jenkins and petersmythe/docker fork to the petersmythe/geoserver-test repo on Docker Hub.


Other enhancements include renaming the existing GitHub Actions to better explain their purpose. Also, an improvement to install-extensions.sh to better "guess" the module paths from the GeoServer version number.

Going forward, when a new series is released, .github/workflows/publish.yml will need to be updated:

image

Ref: https://github.blog/changelog/2025-01-16-linux-arm64-hosted-runners-now-available-for-free-in-public-repositories-public-preview/

Linked previous attempt: #85

@petersmythe
Copy link
Contributor Author

petersmythe commented Jan 19, 2026

Requesting a review on this DRAFT PR, which still points to my test repos/jobs, for you to evaluate, after which I will submit a final commit with the official geoserver/docker & OSGeo repos substituted.

New jobs can be triggered from https://build.geoserver.org/view/release/job/geoserver-release-docker-github by supplying a version number. It simply does this:

image

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new multi-architecture build pipeline for GeoServer Docker images supporting both AMD64 and ARM64 platforms. The approach shifts from Jenkins-based builds to GitHub Actions with native runners for each architecture, significantly improving build efficiency.

Changes:

  • New GitHub Actions workflow (publish.yml) for building and publishing multi-architecture Docker images with matrix builds across AMD64/ARM64 and GDAL/non-GDAL variants
  • Enhanced install-extensions.sh script with automatic version inference from plugin URLs and fallback discovery mechanism for plugin filenames
  • Renamed existing GitHub Actions workflows for clarity (pr-validation.yml and pages.yml)

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 20 comments.

File Description
.github/workflows/publish.yml New multi-architecture build workflow with version parsing, matrix builds, and manifest creation for Docker Hub publishing
install-extensions.sh Enhanced with version inference logic, URL normalization, plugin discovery mechanism, and improved error handling
.github/workflows/pr-validation.yml Renamed for clarity and updated matrix configuration to use consistent variable names
.github/workflows/pages.yml Renamed to better describe its purpose of building docs and deploying to GitHub Pages
Comments suppressed due to low confidence (1)

.github/workflows/pr-validation.yml:1

  • The workflow name says "Validate a PR on push to master" but the trigger includes both pull_request events and push to master. The name is misleading as it suggests it only runs on PRs, but it also runs on direct pushes to master. Consider updating the name to accurately reflect both trigger conditions, such as "Validate PRs and master branch builds" or similar.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 154 to 158
if [[ "$VERSION" == "${MAIN_VERSION}-SNAPSHOT" ]]; then
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,latest"
elif [[ "$VERSION" == "${STABLE_VERSION}-SNAPSHOT" ]]; then
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,stable-nightly"
elif [[ "$VERSION" == "${MAINTENANCE_VERSION}-SNAPSHOT" ]]; then
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic for determining semantic tags has an issue with version matching. The patterns use "${MAIN_VERSION}."* but MAIN_VERSION is "3.0", so this will only match versions like "3.0.0", "3.0.1", etc. However, the environment variable comparisons on lines 154, 156, and 158 use a different pattern without the dot (e.g., "${MAIN_VERSION}-SNAPSHOT" which would be "3.0-SNAPSHOT"). This inconsistency means that for a version like "3.0.0", the tag logic would work correctly, but the snapshot check would fail. Consider using consistent pattern matching or ensure the logic handles both release and snapshot versions correctly.

Suggested change
if [[ "$VERSION" == "${MAIN_VERSION}-SNAPSHOT" ]]; then
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,latest"
elif [[ "$VERSION" == "${STABLE_VERSION}-SNAPSHOT" ]]; then
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,stable-nightly"
elif [[ "$VERSION" == "${MAINTENANCE_VERSION}-SNAPSHOT" ]]; then
if [[ "$VERSION" == "${MAIN_VERSION}-SNAPSHOT" || "$VERSION" == "${MAIN_VERSION}."*"-SNAPSHOT" ]]; then
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,latest"
elif [[ "$VERSION" == "${STABLE_VERSION}-SNAPSHOT" || "$VERSION" == "${STABLE_VERSION}."*"-SNAPSHOT" ]]; then
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,stable-nightly"
elif [[ "$VERSION" == "${MAINTENANCE_VERSION}-SNAPSHOT" || "$VERSION" == "${MAINTENANCE_VERSION}."*"-SNAPSHOT" ]]; then

Copilot uses AI. Check for mistakes.
Comment on lines 47 to 53
FILE=$(echo "${LISTING_ONE}" | sed -n 's/.*href="\([^" ]*'"${EXTENSION}"'-plugin\.zip\)".*/\1/p' | head -n 1 || true)
# ensure we only have a bare filename
FILE=$(basename "${FILE}")
if [ -n "${FILE}" ]; then
echo "Found candidate file: ${FILE}"
NEW_URL="${BASE_URL}/${FILE}"
VERSION=$(echo "${FILE}" | sed -n 's/^geoserver-\(.*\)-'"${EXTENSION}"'-plugin\.zip$/\1/p')
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sed pattern used for extracting the version from the filename may not correctly handle all version formats. For example, if the extension name contains a hyphen or matches part of the version string, the regex could produce incorrect results. The pattern 's/^geoserver-\(.*\)-'"${EXTENSION}"'-plugin\.zip$/\1/p' assumes that EXTENSION doesn't contain special regex characters. Consider escaping the EXTENSION variable or using a more robust parsing method that doesn't rely on string matching with potentially user-controlled input.

Suggested change
FILE=$(echo "${LISTING_ONE}" | sed -n 's/.*href="\([^" ]*'"${EXTENSION}"'-plugin\.zip\)".*/\1/p' | head -n 1 || true)
# ensure we only have a bare filename
FILE=$(basename "${FILE}")
if [ -n "${FILE}" ]; then
echo "Found candidate file: ${FILE}"
NEW_URL="${BASE_URL}/${FILE}"
VERSION=$(echo "${FILE}" | sed -n 's/^geoserver-\(.*\)-'"${EXTENSION}"'-plugin\.zip$/\1/p')
# escape EXTENSION for safe use inside sed regular expressions
EXTENSION_REGEX_ESCAPED=$(printf '%s\n' "${EXTENSION}" | sed 's/[][\\.^$*+?{}|()]/\\&/g')
FILE=$(printf '%s\n' "${LISTING_ONE}" | sed -n 's/.*href="\([^" ]*'"${EXTENSION_REGEX_ESCAPED}"'-plugin\.zip\)".*/\1/p' | head -n 1 || true)
# ensure we only have a bare filename
FILE=$(basename "${FILE}")
if [ -n "${FILE}" ]; then
echo "Found candidate file: ${FILE}"
NEW_URL="${BASE_URL}/${FILE}"
VERSION=$(printf '%s\n' "${FILE}" | sed -n 's/^geoserver-\(.*\)-'"${EXTENSION_REGEX_ESCAPED}"'-plugin\.zip$/\1/p')

Copilot uses AI. Check for mistakes.
EXTENSION=$(echo "${EXTENSION}" | xargs)
[ -z "$EXTENSION" ] && continue
# find any downloaded plugin matching the extension name (handles discovered filenames)
ADDITIONAL_LIB=$(ls -1 "${ADDITIONAL_LIBS_DIR%/}"/geoserver-*-${EXTENSION}-plugin.zip 2>/dev/null | head -n 1 || true)
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wildcard pattern in the ls command could be vulnerable to issues if the EXTENSION variable contains special shell characters. While the script does trim whitespace from EXTENSION on line 79, it doesn't sanitize or validate against potentially problematic characters that could affect the glob pattern. Consider adding validation to ensure EXTENSION contains only expected characters (e.g., alphanumeric, underscore, hyphen) before using it in file operations.

Copilot uses AI. Check for mistakes.
Comment on lines 234 to 235
GS_VERSION=${{ inputs.version }}
GS_BUILD=${{ inputs.build_number }}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When workflow_dispatch inputs are not provided and the workflow is triggered by push, the script falls back to default values. However, if inputs.version is accessed later in the workflow (line 234), it will be empty when triggered by push, even though VERSION was set in the prepare job. The build job references inputs.version and inputs.build_number directly in build-args, which means these will be empty strings for push-triggered builds. These should reference the outputs from the prepare job or use conditional logic to fall back to appropriate values.

Suggested change
GS_VERSION=${{ inputs.version }}
GS_BUILD=${{ inputs.build_number }}
GS_VERSION=${{ needs.prepare.outputs.version }}
GS_BUILD=${{ needs.prepare.outputs.build_number }}

Copilot uses AI. Check for mistakes.
Comment on lines 37 to 60
wget --progress=bar:force:noscroll -c --no-check-certificate "${URL}" -O "${DOWNLOAD_FILE}"
else
echo "URL does not exist: ${URL}"
# Try to discover an actual plugin file at the base URL and use it
BASE_URL="${URL%/geoserver-*-${EXTENSION}-plugin.zip}"
if [ -n "${BASE_URL}" ]; then
echo "Attempting to discover plugin filename from ${BASE_URL}/"
LISTING=$(curl -s "${BASE_URL}/" || true)
# flatten and extract the href value for the matching plugin file
LISTING_ONE=$(echo "${LISTING}" | tr '\n' ' ')
FILE=$(echo "${LISTING_ONE}" | sed -n 's/.*href="\([^" ]*'"${EXTENSION}"'-plugin\.zip\)".*/\1/p' | head -n 1 || true)
# ensure we only have a bare filename
FILE=$(basename "${FILE}")
if [ -n "${FILE}" ]; then
echo "Found candidate file: ${FILE}"
NEW_URL="${BASE_URL}/${FILE}"
VERSION=$(echo "${FILE}" | sed -n 's/^geoserver-\(.*\)-'"${EXTENSION}"'-plugin\.zip$/\1/p')
if [ -n "${VERSION}" ]; then
GEOSERVER_VERSION="${VERSION}"
echo "Resolved GEOSERVER_VERSION=${GEOSERVER_VERSION} from ${FILE}"
fi
DOWNLOAD_FILE="${DOWNLOAD_DIR}${FILE}"
echo -e "\nDownloading ${EXTENSION} extension from ${NEW_URL} to ${DOWNLOAD_FILE}"
wget --progress=bar:force:noscroll -c --no-check-certificate "${NEW_URL}" -O "${DOWNLOAD_FILE}"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script uses wget --no-check-certificate which disables SSL/TLS certificate verification. This is a security risk as it makes the download vulnerable to man-in-the-middle attacks. Even though this might be needed for some internal or test environments, it's dangerous for production use. Consider either fixing the certificate issues on the source servers or at minimum adding a comment explaining why this is necessary and the associated risks.

Copilot uses AI. Check for mistakes.
Comment on lines 308 to 331
docker buildx imagetools create $(printf -- '-t %s ' $TAGS) \
$(printf 'petersmythe/geoserver-test@sha256:%s ' *)

echo "Created manifest for tags: $TAGS"

- name: Create manifest list and push (with GDAL)
working-directory: /tmp/digests-gdal
run: |
PRIMARY_TAG="${{ needs.prepare.outputs.primary_tag }}-gdal"
ADDITIONAL_TAGS="${{ needs.prepare.outputs.additional_tags }}"

# Build tag list with -gdal suffix
TAGS="petersmythe/geoserver-test:$PRIMARY_TAG"

if [ -n "$ADDITIONAL_TAGS" ]; then
IFS=',' read -ra TAG_ARRAY <<< "$ADDITIONAL_TAGS"
for tag in "${TAG_ARRAY[@]}"; do
TAGS="$TAGS petersmythe/geoserver-test:$tag-gdal"
done
fi

# Create manifest
docker buildx imagetools create $(printf -- '-t %s ' $TAGS) \
$(printf 'petersmythe/geoserver-test@sha256:%s ' *)
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manifest creation commands in lines 308-309 and 330-331 use shell parameter expansion with unquoted globs. If the /tmp/digests-* directories are empty or don't contain the expected files, the wildcard expansion * could expand to the literal * character or cause unexpected behavior. Consider adding validation to ensure digest files exist before attempting to create the manifest, or add error handling to provide a clearer error message if no digests are found.

Copilot uses AI. Check for mistakes.
Comment on lines 49 to 50
FILE=$(basename "${FILE}")
if [ -n "${FILE}" ]; then
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sed pattern used for href extraction could match unexpected content from HTML if the directory listing format is not standard. The pattern 's/.*href="\([^" ]*'"${EXTENSION}"'-plugin\.zip\)".*/\1/p' may fail or produce incorrect results if the HTML contains malformed quotes, spaces in unexpected places, or if the extension name appears in unexpected contexts. Consider adding additional validation after extraction to ensure the FILE variable contains a valid filename format before proceeding with the download.

Suggested change
FILE=$(basename "${FILE}")
if [ -n "${FILE}" ]; then
FILE=$(basename "${FILE}")
# validate that the discovered filename matches the expected plugin pattern
if [ -n "${FILE}" ]; then
case "${FILE}" in
geoserver-*-"${EXTENSION}"-plugin.zip) ;;
*)
echo "Discovered filename '${FILE}' does not match expected pattern 'geoserver-*-${EXTENSION}-plugin.zip'; ignoring."
FILE=""
;;
esac
fi
if [ -n "${FILE}" ]; then

Copilot uses AI. Check for mistakes.
WAR_ZIP_URL=${{ needs.prepare.outputs.war_url }}
STABLE_PLUGIN_URL=${{ needs.prepare.outputs.stable_plugin_url }}
COMMUNITY_PLUGIN_URL=${{ needs.prepare.outputs.community_plugin_url }}
outputs: type=image,name=petersmythe/geoserver-test,push-by-digest=true,name-canonical=true,push=true
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow hardcodes the Docker Hub repository name "petersmythe/geoserver-test" in multiple places (lines 241, 298, 303, 309, 320, 325, 331, 337, 338, 360, 361, 366, 367). This is a test repository according to the PR description. Before merging to production, these references should be updated to point to the official OSGeo repository. Consider using an environment variable or input parameter to make the repository configurable and avoid accidental pushes to the test repository in production.

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +204
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
gdal: false
- platform: linux/arm64
runner: ubuntu-24.04-arm
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow references "ubuntu-24.04-arm" as a runner for ARM64 builds, but this runner type is only available in public preview as mentioned in the PR description. There's a risk that this runner type might not be available in all repositories or might have quota limitations. Consider adding a fallback mechanism or documentation about the prerequisites needed for this workflow to run successfully, particularly around ARM64 runner availability.

Suggested change
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
gdal: false
- platform: linux/arm64
runner: ubuntu-24.04-arm
# Note: ubuntu-24.04-arm is a preview runner. To use a different ARM64 runner
# (for example, a self-hosted runner), define the repository or organization
# variable ARM64_RUNNER with the desired runner label.
- platform: linux/arm64
runner: ${{ vars.ARM64_RUNNER || 'ubuntu-24.04-arm' }}
arch: arm64
gdal: false
- platform: linux/arm64
runner: ${{ vars.ARM64_RUNNER || 'ubuntu-24.04-arm' }}

Copilot uses AI. Check for mistakes.

# Add nightly tags for snapshot builds
if [[ "$VERSION" == "${MAIN_VERSION}-SNAPSHOT" ]]; then
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,latest"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional logic for adding the "latest" tag has a conflict. For MAIN_VERSION releases (line 146), "latest" is added. However, for MAIN_VERSION-SNAPSHOT builds (line 155), "latest" is also added. This means both snapshot and release builds of the main version will use the same "latest" tag, which could lead to the "latest" tag unexpectedly pointing to a snapshot build instead of the most recent stable release. Consider using different tag names like "main-latest" for releases and "main-nightly" or just "nightly" for snapshots to avoid confusion.

Suggested change
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,latest"
ADDITIONAL_TAGS="$ADDITIONAL_TAGS,main-nightly"

Copilot uses AI. Check for mistakes.
@github-advanced-security
Copy link

This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation.

@petersmythe
Copy link
Contributor Author

Wow, Copilot, you've given me lots more work to do! Thanks.

@petersmythe petersmythe marked this pull request as ready for review January 31, 2026 13:09
@petersmythe
Copy link
Contributor Author

All Copilot feedback implemented. Ready for human review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants