-
Notifications
You must be signed in to change notification settings - Fork 136
Publish Multi-architecture GeoServer Docker images, esp ARM64 #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
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:
|
There was a problem hiding this 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.shscript with automatic version inference from plugin URLs and fallback discovery mechanism for plugin filenames - Renamed existing GitHub Actions workflows for clarity (
pr-validation.ymlandpages.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.
.github/workflows/publish.yml
Outdated
| 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 |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| 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 |
install-extensions.sh
Outdated
| 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') |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| 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') |
| 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) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
.github/workflows/publish.yml
Outdated
| GS_VERSION=${{ inputs.version }} | ||
| GS_BUILD=${{ inputs.build_number }} |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| GS_VERSION=${{ inputs.version }} | |
| GS_BUILD=${{ inputs.build_number }} | |
| GS_VERSION=${{ needs.prepare.outputs.version }} | |
| GS_BUILD=${{ needs.prepare.outputs.build_number }} |
install-extensions.sh
Outdated
| 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}" |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
.github/workflows/publish.yml
Outdated
| 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 ' *) |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
install-extensions.sh
Outdated
| FILE=$(basename "${FILE}") | ||
| if [ -n "${FILE}" ]; then |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| 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 |
.github/workflows/publish.yml
Outdated
| 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 |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| - platform: linux/arm64 | ||
| runner: ubuntu-24.04-arm | ||
| arch: arm64 | ||
| gdal: false | ||
| - platform: linux/arm64 | ||
| runner: ubuntu-24.04-arm |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| - 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' }} |
.github/workflows/publish.yml
Outdated
|
|
||
| # Add nightly tags for snapshot builds | ||
| if [[ "$VERSION" == "${MAIN_VERSION}-SNAPSHOT" ]]; then | ||
| ADDITIONAL_TAGS="$ADDITIONAL_TAGS,latest" |
Copilot
AI
Jan 19, 2026
There was a problem hiding this comment.
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.
| ADDITIONAL_TAGS="$ADDITIONAL_TAGS,latest" | |
| ADDITIONAL_TAGS="$ADDITIONAL_TAGS,main-nightly" |
|
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. |
|
Wow, Copilot, you've given me lots more work to do! Thanks. |
…proved validation and error handling
…ad_extension function
|
All Copilot feedback implemented. Ready for human review |

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.ymlGitHub 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-testAlso, various

-latestand-nightlytags are added for users convenience e.g.Finally, the
publish.ymlaction 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.shto better "guess" the module paths from the GeoServer version number.Going forward, when a new series is released, .
github/workflows/publish.ymlwill need to be updated: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