diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a822f69c..1f20485c 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,4 +1,4 @@ -name: Pages +name: Build docs and deploy to GitHub Pages on: push: paths: diff --git a/.github/workflows/docker.yml b/.github/workflows/pr-validation.yml similarity index 92% rename from .github/workflows/docker.yml rename to .github/workflows/pr-validation.yml index 06035cf2..fdef9f50 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/pr-validation.yml @@ -1,20 +1,24 @@ -name: build +name: Validate a PR on push to master on: + pull_request: + types: [opened, synchronize, reopened] push: branches: - master jobs: build: - name: Build (${{ matrix.name }}) + name: Build (${{ matrix.display_name }}) runs-on: ubuntu-latest strategy: matrix: include: - - build_gdal: false - name: default - build_gdal: true - name: gdal + name: with-gdal + display_name: "with GDAL" + - build_gdal: false + name: without-gdal + display_name: "without GDAL" steps: - name: Checkout code uses: actions/checkout@v6 @@ -40,7 +44,7 @@ jobs: # Integration Tests - Start container and perform basic health checks - name: Start GeoServer container run: | - if [[ "${{ matrix.name }}" == "gdal" ]]; then + if [[ "${{ matrix.build_gdal }}" == "true" ]]; then # Start GDAL image with extension installation docker run -d --name geoserver-test-${{ matrix.name }} \ -p 8080:8080 \ @@ -115,7 +119,7 @@ jobs: - name: Test gdal specific functionality run: | - if [[ "${{ matrix.name }}" == "gdal" ]]; then + if [[ "${{ matrix.build_gdal }}" == "true" ]]; then echo "Testing GDAL-specific functionality..." # Check GDAL installation docker exec geoserver-test-${{ matrix.name }} which gdalinfo || echo "GDAL tools check" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..22cb6da2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,550 @@ +name: Build and Publish Multi-Arch GeoServer Images + +on: + workflow_dispatch: + inputs: + version: + description: 'GeoServer version (e.g., 2.28.1, 2.27-SNAPSHOT, 3.0-RC)' + required: true + type: string + build_number: + description: 'Build number (optional, defaults to "github")' + required: false + type: string + default: 'github' + push: + branches: + - ci/publish-test + +concurrency: + group: publish-${{ github.event.inputs.version || 'push-test' }} + cancel-in-progress: true + +permissions: + contents: read + packages: write + id-token: write + attestations: write + +env: + # ------------------------------------------------------------------ + # GeoServer Version Series Configuration + # + # These variables define the currently supported GeoServer release series. + # They MUST be updated when a new major or minor series is released (every 6 months). + # + # MAIN_VERSION: Current development series (main branch, experimental) + # STABLE_VERSION: Current stable release series (recommended for production) + # MAINTENANCE_VERSION: Previous stable series (receives critical fixes only) + # + # These values determine: + # - Docker image tags (e.g., "nightly", "stable-latest", "maintenance-latest") + # - Base Tomcat/JDK images for each series + # - Download URLs for WAR files and plugins + # - Git branch mappings for nightly builds + # + # When a new series is released (e.g., 3.1 becomes main): + # 1. Update MAIN_VERSION to the new series (e.g., "3.1") + # 2. Update STABLE_VERSION to the previous main (e.g., "3.0") + # 3. Update MAINTENANCE_VERSION to the previous stable (e.g., "2.28") + # 4. Update base image selection logic if JDK requirements change (line 89) + # + # WARNING: Incorrect values will cause images to be tagged incorrectly, + # use wrong base images, and download from incorrect plugin URLs. + # ------------------------------------------------------------------ + MAIN_VERSION: "3.0" + STABLE_VERSION: "2.28" + MAINTENANCE_VERSION: "2.27" + # Primary registry: pushed to during build, receives personal account credentials + PRIMARY_REGISTRY: "petersmythe/geoserver-test" + # Secondary registry: mirrored after build, receives OSGeo credentials + SECONDARY_REGISTRY: "geoserver-docker.osgeo.org/geoserver" + +jobs: + # ============================================================ + # JOB 1: Prepare build parameters + # ============================================================ + prepare: + name: Prepare Build Parameters + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + version: ${{ steps.parse.outputs.version }} + build_number: ${{ steps.parse.outputs.build_number }} + major: ${{ steps.parse.outputs.major }} + minor: ${{ steps.parse.outputs.minor }} + base_image: ${{ steps.parse.outputs.base_image }} + branch: ${{ steps.parse.outputs.branch }} + is_nightly: ${{ steps.parse.outputs.is_nightly }} + war_url: ${{ steps.parse.outputs.war_url }} + stable_plugin_url: ${{ steps.parse.outputs.stable_plugin_url }} + community_plugin_url: ${{ steps.parse.outputs.community_plugin_url }} + primary_tag: ${{ steps.parse.outputs.primary_tag }} + additional_tags: ${{ steps.parse.outputs.additional_tags }} + + steps: + - name: Parse Version and Determine Build Parameters + id: parse + run: | + set -euo pipefail + + VERSION="${{ inputs.version }}" + BUILD="${{ inputs.build_number }}" + + # If triggered by a push (test), allow a sensible default so this can be run without workflow_dispatch inputs + if [ -z "$VERSION" ] && [ "${GITHUB_EVENT_NAME}" = "push" ]; then + VERSION="3.0-SNAPSHOT" + BUILD="${GITHUB_RUN_NUMBER:-github}" + echo "No version input (push trigger). Falling back to VERSION=$VERSION BUILD=$BUILD" + fi + + # If BUILD is empty, use a default + if [ -z "$BUILD" ]; then + BUILD="${GITHUB_RUN_NUMBER:-github}" + echo "Build number not provided. Using BUILD=$BUILD" + fi + + # Output version and build number for use in other jobs + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "build_number=$BUILD" >> $GITHUB_OUTPUT + + echo "Parsing version: $VERSION" + + # Extract major.minor version + if [[ $VERSION =~ ^([0-9]+)\.([0-9]+) ]]; then + MAJOR="${BASH_REMATCH[1]}" + MINOR="${BASH_REMATCH[2]}" + echo "major=$MAJOR" >> $GITHUB_OUTPUT + echo "minor=$MINOR" >> $GITHUB_OUTPUT + else + echo "ERROR: Unable to parse version $VERSION" + exit 1 + fi + + # Determine base image based on version + if [[ "$VERSION" == "3"* ]]; then + BASE_IMAGE="tomcat:11.0-jdk21-temurin-noble" + elif [[ "$VERSION" == "2.28"* ]]; then + BASE_IMAGE="tomcat:9.0-jdk21-temurin-noble" + elif [[ "$VERSION" == "2.27"* ]] || [[ "$VERSION" == "2.26"* ]]; then + BASE_IMAGE="tomcat:9.0-jdk17-temurin-noble" + else + BASE_IMAGE="tomcat:9.0-jdk11-temurin-noble" + fi + echo "base_image=$BASE_IMAGE" >> $GITHUB_OUTPUT + + # Determine branch and tag based on version pattern + if [[ "$VERSION" == *"-M"* ]]; then + # Milestone release (e.g., 2.28-M0) + BRANCH="$VERSION" + PRIMARY_TAG="$VERSION" + IS_NIGHTLY="true" + elif [[ "$VERSION" == *"-RC"* ]]; then + # Release candidate (e.g., 2.28-RC) + BRANCH="$VERSION" + PRIMARY_TAG="$VERSION" + IS_NIGHTLY="true" + elif [[ "$VERSION" == "${{ env.MAIN_VERSION }}"* ]]; then + # Main branch (e.g., 3.0-SNAPSHOT or 3.0.0) + if [[ "$VERSION" == *"-SNAPSHOT"* ]]; then + BRANCH="main" + PRIMARY_TAG="${{ env.MAIN_VERSION }}.x" + IS_NIGHTLY="true" + else + BRANCH="main" + PRIMARY_TAG="$VERSION" + IS_NIGHTLY="false" + fi + else + # Stable or maintenance branch + if [[ "$VERSION" == *"-SNAPSHOT"* ]]; then + BRANCH="${MAJOR}.${MINOR}.x" + PRIMARY_TAG="${MAJOR}.${MINOR}.x" + IS_NIGHTLY="true" + else + BRANCH="${MAJOR}.${MINOR}.x" + PRIMARY_TAG="$VERSION" + IS_NIGHTLY="false" + fi + fi + + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + echo "is_nightly=$IS_NIGHTLY" >> $GITHUB_OUTPUT + echo "primary_tag=$PRIMARY_TAG" >> $GITHUB_OUTPUT + + # Determine download URLs + if [[ "$IS_NIGHTLY" == "true" ]]; then + WAR_URL="https://build.geoserver.org/geoserver/$BRANCH/geoserver-$BRANCH-latest-war.zip" + STABLE_PLUGIN_URL="https://build.geoserver.org/geoserver/$BRANCH/ext-latest" + COMMUNITY_PLUGIN_URL="https://build.geoserver.org/geoserver/$BRANCH/community-latest" + else + WAR_URL="https://downloads.sourceforge.net/project/geoserver/GeoServer/$VERSION/geoserver-$VERSION-war.zip" + STABLE_PLUGIN_URL="https://downloads.sourceforge.net/project/geoserver/GeoServer/${VERSION}/extensions" + COMMUNITY_PLUGIN_URL="https://build.geoserver.org/geoserver/${BRANCH}/community-latest" + fi + + echo "war_url=$WAR_URL" >> $GITHUB_OUTPUT + echo "stable_plugin_url=$STABLE_PLUGIN_URL" >> $GITHUB_OUTPUT + echo "community_plugin_url=$COMMUNITY_PLUGIN_URL" >> $GITHUB_OUTPUT + + # Determine additional tags based on version pattern + ADDITIONAL_TAGS="" + + # Add series-latest tag (e.g., 2.28-latest) + if [[ "$IS_NIGHTLY" == "false" ]]; then + ADDITIONAL_TAGS="${MAJOR}.${MINOR}-latest" + fi + + # Add semantic tags for stable/maintenance releases only + # (main releases get x.y-latest but no semantic tag since they're experimental) + if [[ "$VERSION" == "${STABLE_VERSION}."* ]] && [[ "$IS_NIGHTLY" == "false" ]]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,stable-latest" + elif [[ "$VERSION" == "${MAINTENANCE_VERSION}."* ]] && [[ "$IS_NIGHTLY" == "false" ]]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,maintenance-latest" + fi + + # Add nightly tags for snapshot builds + if [[ "$VERSION" == "${MAIN_VERSION}"*"-SNAPSHOT" ]]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,nightly" + elif [[ "$VERSION" == "${STABLE_VERSION}"*"-SNAPSHOT" ]]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,stable-nightly" + elif [[ "$VERSION" == "${MAINTENANCE_VERSION}"*"-SNAPSHOT" ]]; then + ADDITIONAL_TAGS="$ADDITIONAL_TAGS,maintenance-nightly" + fi + + # Clean up leading comma + ADDITIONAL_TAGS=$(echo "$ADDITIONAL_TAGS" | sed 's/^,//') + + echo "additional_tags=$ADDITIONAL_TAGS" >> $GITHUB_OUTPUT + + echo "============================================" + echo "Build Configuration:" + echo " Version: $VERSION" + echo " Branch: $BRANCH" + echo " Base Image: $BASE_IMAGE" + echo " Is Nightly: $IS_NIGHTLY" + echo " Primary Tag: $PRIMARY_TAG" + echo " Additional Tags: $ADDITIONAL_TAGS" + echo " WAR URL: $WAR_URL" + echo "============================================" + + - name: Validate GeoServer WAR URL + run: | + set -euo pipefail + echo "Validating GeoServer WAR URL: ${{ steps.parse.outputs.war_url }}" + if ! wget --spider "${{ steps.parse.outputs.war_url }}"; then + echo "ERROR: GeoServer WAR URL is not accessible: ${{ steps.parse.outputs.war_url }}" >&2 + exit 1 + fi + echo "GeoServer WAR URL is accessible" + + # ============================================================ + # JOB 2: Build images on native runners (matrix) + # ============================================================ + build: + name: Build ${{ matrix.platform }} ${{ matrix.gdal && 'with GDAL' || 'without GDAL' }} + runs-on: ${{ matrix.runner }} + needs: prepare + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + # AMD64 builds + - platform: linux/amd64 + runner: ubuntu-latest + arch: amd64 + gdal: false + - platform: linux/amd64 + runner: ubuntu-latest + arch: amd64 + gdal: true + # ARM64 builds + - platform: linux/arm64 + runner: ubuntu-24.04-arm + arch: arm64 + gdal: false + - platform: linux/arm64 + runner: ubuntu-24.04-arm + arch: arm64 + gdal: true + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Primary Registry (Docker Hub) + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Download GeoServer WAR + run: | + mkdir -p geoserver + cd geoserver + wget -c "${{ needs.prepare.outputs.war_url }}" + + - name: Build and export digest + id: build + uses: docker/build-push-action@v5 + with: + context: . + platforms: ${{ matrix.platform }} + build-args: | + GS_VERSION=${{ needs.prepare.outputs.version }} + GS_BUILD=${{ needs.prepare.outputs.build_number }} + BUILD_GDAL=${{ matrix.gdal }} + GEOSERVER_BASE_IMAGE=${{ needs.prepare.outputs.base_image }} + 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 }} + cache-from: type=gha,scope=build-${{ matrix.arch }}-${{ matrix.gdal && 'gdal' || 'nogdal' }} + cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}-${{ matrix.gdal && 'gdal' || 'nogdal' }} + provenance: true + sbom: true + outputs: type=image,name=${{ env.PRIMARY_REGISTRY }},push-by-digest=true,name-canonical=true,push=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.arch }}-${{ matrix.gdal && 'gdal' || 'nogdal' }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + # ============================================================ + # JOB 3: Merge digests into multi-arch manifests + # ============================================================ + merge: + name: Create Multi-Arch Manifests + runs-on: ubuntu-latest + needs: + - prepare + - build + timeout-minutes: 15 + + steps: + - name: Download all digests (no GDAL) + uses: actions/download-artifact@v4 + with: + pattern: digests-*-nogdal + merge-multiple: true + path: /tmp/digests-nogdal + + - name: Download all digests (with GDAL) + uses: actions/download-artifact@v4 + with: + pattern: digests-*-gdal + merge-multiple: true + path: /tmp/digests-gdal + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Primary Registry (Docker Hub) + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create manifest list and push to Primary Registry (no GDAL) + working-directory: /tmp/digests-nogdal + run: | + set -euo pipefail + + # Validate digest files exist + DIGEST_COUNT=$(ls -1 2>/dev/null | wc -l) + if [ "$DIGEST_COUNT" -eq 0 ]; then + echo "ERROR: No digest files found in /tmp/digests-nogdal" >&2 + echo "Expected digests from build jobs (amd64 and arm64)" >&2 + exit 1 + fi + echo "Found $DIGEST_COUNT digest file(s)" + + PRIMARY_TAG="${{ needs.prepare.outputs.primary_tag }}" + ADDITIONAL_TAGS="${{ needs.prepare.outputs.additional_tags }}" + + # Build tag list for primary registry + TAGS="${{ env.PRIMARY_REGISTRY }}:$PRIMARY_TAG" + + if [ -n "$ADDITIONAL_TAGS" ]; then + IFS=',' read -ra TAG_ARRAY <<< "$ADDITIONAL_TAGS" + for tag in "${TAG_ARRAY[@]}"; do + TAGS="$TAGS ${{ env.PRIMARY_REGISTRY }}:$tag" + done + fi + + # Create manifest and push to primary registry + docker buildx imagetools create $(printf -- '-t %s ' $TAGS) \ + $(printf '${{ env.PRIMARY_REGISTRY }}@sha256:%s ' *) + + echo "Created manifest for tags: $TAGS" + + - name: Create manifest list and push (with GDAL) + working-directory: /tmp/digests-gdal + run: | + set -euo pipefail + + # Validate digest files exist + DIGEST_COUNT=$(ls -1 2>/dev/null | wc -l) + if [ "$DIGEST_COUNT" -eq 0 ]; then + echo "ERROR: No digest files found in /tmp/digests-gdal" >&2 + echo "Expected digests from build jobs (amd64 and arm64)" >&2 + exit 1 + fi + echo "Found $DIGEST_COUNT digest file(s)" + + PRIMARY_TAG="${{ needs.prepare.outputs.primary_tag }}-gdal" + ADDITIONAL_TAGS="${{ needs.prepare.outputs.additional_tags }}" + + # Build tag list with -gdal suffix + TAGS="${{ env.PRIMARY_REGISTRY }}:$PRIMARY_TAG" + + if [ -n "$ADDITIONAL_TAGS" ]; then + IFS=',' read -ra TAG_ARRAY <<< "$ADDITIONAL_TAGS" + for tag in "${TAG_ARRAY[@]}"; do + TAGS="$TAGS ${{ env.PRIMARY_REGISTRY }}:$tag-gdal" + done + fi + + # Create manifest + docker buildx imagetools create $(printf -- '-t %s ' $TAGS) \ + $(printf '${{ env.PRIMARY_REGISTRY }}@sha256:%s ' *) + + echo "Created manifest for tags: $TAGS" + + - name: Inspect manifests + run: | + docker buildx imagetools inspect ${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }} + docker buildx imagetools inspect ${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }}-gdal + + - name: Generate Job Summary + run: | + cat >> $GITHUB_STEP_SUMMARY << 'EOF' + ## GeoServer Multi-Arch Build Summary + + ### Build Information + - **Version**: ${{ needs.prepare.outputs.version }} + - **Build Number**: ${{ needs.prepare.outputs.build_number }} + - **Base Image**: ${{ needs.prepare.outputs.base_image }} + - **Is Nightly**: ${{ needs.prepare.outputs.is_nightly }} + + ### Images Created + + #### Without GDAL + - Primary: [${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }}](https://hub.docker.com/r/${{ env.PRIMARY_REGISTRY }}/tags) + EOF + + # Add additional tags if present + ADDITIONAL_TAGS="${{ needs.prepare.outputs.additional_tags }}" + if [ -n "$ADDITIONAL_TAGS" ]; then + IFS=',' read -ra TAG_ARRAY <<< "$ADDITIONAL_TAGS" + for tag in "${TAG_ARRAY[@]}"; do + echo "- Additional: [${{ env.PRIMARY_REGISTRY }}:$tag](https://hub.docker.com/r/${{ env.PRIMARY_REGISTRY }}/tags)" >> $GITHUB_STEP_SUMMARY + done + fi + + cat >> $GITHUB_STEP_SUMMARY << 'EOF' + + #### With GDAL + - Primary: [${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }}-gdal](https://hub.docker.com/r/${{ env.PRIMARY_REGISTRY }}/tags) + EOF + + if [ -n "$ADDITIONAL_TAGS" ]; then + IFS=',' read -ra TAG_ARRAY <<< "$ADDITIONAL_TAGS" + for tag in "${TAG_ARRAY[@]}"; do + echo "- Additional: [${{ env.PRIMARY_REGISTRY }}:$tag-gdal](https://hub.docker.com/r/${{ env.PRIMARY_REGISTRY }}/tags)" >> $GITHUB_STEP_SUMMARY + done + fi + + cat >> $GITHUB_STEP_SUMMARY << 'EOF' + + ### Architectures + - linux/amd64 + - linux/arm64 + + ### Pull Commands + ```bash + # Without GDAL + docker pull ${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }} + + # With GDAL + docker pull ${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }}-gdal + ``` + EOF + + # ============================================================ + # JOB 4: Publish to Secondary Registry + # ============================================================ + publish-secondary: + name: Mirror Images to Secondary Registry + runs-on: ubuntu-latest + needs: + - prepare + - merge + timeout-minutes: 15 + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Secondary Registry (OSGeo) + uses: docker/login-action@v3 + with: + registry: geoserver-docker.osgeo.org + username: ${{ secrets.OSGEO_REPO_USERNAME }} + password: ${{ secrets.OSGEO_REPO_PASSWORD }} + + - name: Copy multi-arch manifests to secondary registry + run: | + # Copy multi-arch manifest from primary to secondary registry + docker buildx imagetools create \ + -t ${{ env.SECONDARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }} \ + ${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }} + + # Apply additional tags if present + ADDITIONAL_TAGS="${{ needs.prepare.outputs.additional_tags }}" + if [ -n "$ADDITIONAL_TAGS" ]; then + IFS=',' read -ra TAG_ARRAY <<< "$ADDITIONAL_TAGS" + for tag in "${TAG_ARRAY[@]}"; do + docker buildx imagetools create \ + -t ${{ env.SECONDARY_REGISTRY }}:${tag} \ + ${{ env.PRIMARY_REGISTRY }}:${tag} + done + fi + + # Copy multi-arch manifest for the GDAL variant + docker buildx imagetools create \ + -t ${{ env.SECONDARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }}-gdal \ + ${{ env.PRIMARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }}-gdal + + if [ -n "$ADDITIONAL_TAGS" ]; then + IFS=',' read -ra TAG_ARRAY <<< "$ADDITIONAL_TAGS" + for tag in "${TAG_ARRAY[@]}"; do + docker buildx imagetools create \ + -t ${{ env.SECONDARY_REGISTRY }}:${tag}-gdal \ + ${{ env.PRIMARY_REGISTRY }}:${tag}-gdal + done + fi + + echo "Successfully copied multi-arch manifests to ${{ env.SECONDARY_REGISTRY }}." + echo "Verify at: https://hub.docker.com/r/${{ env.SECONDARY_REGISTRY }}/tags" + + # Inspect to confirm multi-arch + echo "" + echo "Inspecting primary manifest:" + docker buildx imagetools inspect ${{ env.SECONDARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }} + + echo "" + echo "Inspecting primary manifest (with GDAL):" + docker buildx imagetools inspect ${{ env.SECONDARY_REGISTRY }}:${{ needs.prepare.outputs.primary_tag }}-gdal diff --git a/install-extensions.sh b/install-extensions.sh index fea456bd..7b9a78c9 100644 --- a/install-extensions.sh +++ b/install-extensions.sh @@ -1,19 +1,97 @@ #!/bin/bash # Inspired by https://github.com/kartoza/docker-geoserver +# Helper: normalize a URL by stripping any trailing slash +normalize_url() { + echo "${1%/}" +} + +# Version inference: Extract GeoServer version from plugin URLs if not explicitly set +# This handles cases where GEOSERVER_VERSION is not provided as a build arg +if [ -z "${GEOSERVER_VERSION}" ]; then + # Try extracting version from STABLE_PLUGIN_URL (e.g., .../geoserver/2.28.x/ext-latest/ -> 2.28.x) + if [ -n "${STABLE_PLUGIN_URL}" ]; then + VERSION=$(echo "${STABLE_PLUGIN_URL}" | sed -n 's#.*/geoserver/\([^/]*\)/.*#\1#p') + fi + # Fallback to COMMUNITY_PLUGIN_URL if stable URL didn't yield a version + if [ -z "${VERSION}" ] && [ -n "${COMMUNITY_PLUGIN_URL}" ]; then + VERSION=$(echo "${COMMUNITY_PLUGIN_URL}" | sed -n 's#.*/geoserver/\([^/]*\)/.*#\1#p') + fi + if [ -n "${VERSION}" ]; then + GEOSERVER_VERSION="${VERSION}" + echo "Inferred GEOSERVER_VERSION=${GEOSERVER_VERSION} from plugin URL" + else + echo "Warning: GEOSERVER_VERSION is not set and could not be inferred from plugin URLs" + fi +fi + function download_extension() { URL=$1 EXTENSION=$2 - DOWNLOAD_FILE="${ADDITIONAL_LIBS_DIR}geoserver-${GEOSERVER_VERSION}-${EXTENSION}-plugin.zip" + # Escape special regex characters in extension name for safe use in sed patterns + EXTENSION_REGEX_ESCAPED=$(printf '%s\n' "${EXTENSION}" | sed 's/[][\\.^$*+?{}|()]/\\&/g') + DOWNLOAD_DIR="${ADDITIONAL_LIBS_DIR%/}/" + DOWNLOAD_FILE="${DOWNLOAD_DIR}geoserver-${GEOSERVER_VERSION}-${EXTENSION}-plugin.zip" if [ -e "$DOWNLOAD_FILE" ]; then echo "$DOWNLOAD_FILE already exists. Skipping download." else + # Try downloading from expected URL first if curl --output /dev/null --silent --head --fail "${URL}"; then echo -e "\nDownloading ${EXTENSION} extension from ${URL} to ${DOWNLOAD_FILE}" - wget --progress=bar:force:noscroll -c --no-check-certificate "${URL}" -O ${DOWNLOAD_FILE} - else + wget --progress=bar:force:noscroll --tries=3 -c "${URL}" -O "${DOWNLOAD_FILE}" + else echo "URL does not exist: ${URL}" + # Fallback: scrape directory listing to discover actual filename + # This handles cases where version format in filename differs from expected + BASE_URL="${URL%/geoserver-*-${EXTENSION}-plugin.zip}" + if [ -n "${BASE_URL}" ]; then + echo "Attempting to discover plugin filename from ${BASE_URL}/" + # Curl failure is tolerated (|| true) since directory scraping is optional fallback + LISTING=$(curl -fsS "${BASE_URL}/" 2>/dev/null || true) + if [ -z "${LISTING}" ]; then + echo "Unable to retrieve directory listing from ${BASE_URL}/; skipping automatic plugin discovery." + else + # Parse HTML to extract href matching the extension plugin pattern + LISTING_ONE=$(echo "${LISTING}" | tr '\n' ' ') + FILE=$(echo "${LISTING_ONE}" | sed -n 's/.*href="\([^" ]*'"${EXTENSION_REGEX_ESCAPED}"'-plugin\\.zip\)".*/\1/p' | head -n 1 || true) + + # Basic sanity checks before using the discovered value + if [ -n "${FILE}" ]; then + # Security: reject absolute URLs or paths (only accept simple filenames) + if echo "${FILE}" | grep -qE '://' || echo "${FILE}" | grep -q '/'; then + echo "Discovered candidate '${FILE}' is not a simple filename; skipping." + FILE="" + fi + fi + + if [ -n "${FILE}" ]; then + # Ensure we only have a bare filename + FILE=$(basename "${FILE}") + # Validate filename matches expected pattern: geoserver---plugin.zip + if ! echo "${FILE}" | grep -qE '^geoserver-[^-][^/]*-'"${EXTENSION_REGEX_ESCAPED}"'-plugin\.zip$'; then + echo "Discovered candidate filename '${FILE}' does not match expected pattern; skipping." + FILE="" + fi + fi + + if [ -n "${FILE}" ]; then + echo "Found candidate file: ${FILE}" + NEW_URL="${BASE_URL}/${FILE}" + # Extract version from discovered filename if GEOSERVER_VERSION is not yet set + VERSION=$(echo "${FILE}" | sed -n 's/^geoserver-\(.*\)-'"${EXTENSION_REGEX_ESCAPED}"'-plugin\\.zip$/\1/p') + if [ -n "${VERSION}" ] && [ -z "${GEOSERVER_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 --tries=3 -c "${NEW_URL}" -O "${DOWNLOAD_FILE}" + else + echo "No matching plugin found at ${BASE_URL}/" + fi + fi + fi fi fi } @@ -22,15 +100,23 @@ function download_extension() { if [ "$INSTALL_EXTENSIONS" = "true" ]; then echo "Starting download of extensions" if [ ! -d "$ADDITIONAL_LIBS_DIR" ]; then - mkdir -p $ADDITIONAL_LIBS_DIR + mkdir -p "$ADDITIONAL_LIBS_DIR" fi + BASE_STABLE_URL=$(normalize_url "${STABLE_PLUGIN_URL}") + BASE_COMM_URL=$(normalize_url "${COMMUNITY_PLUGIN_URL}") + for EXTENSION in $(echo "${STABLE_EXTENSIONS}" | tr ',' ' '); do - URL="${STABLE_PLUGIN_URL}/geoserver-${GEOSERVER_VERSION}-${EXTENSION}-plugin.zip" - download_extension ${URL} ${EXTENSION} + EXTENSION=$(echo "${EXTENSION}" | xargs) + [ -z "$EXTENSION" ] && continue + URL="${BASE_STABLE_URL}/geoserver-${GEOSERVER_VERSION}-${EXTENSION}-plugin.zip" + download_extension "${URL}" "${EXTENSION}" done + for EXTENSION in $(echo "${COMMUNITY_EXTENSIONS}" | tr ',' ' '); do - URL="${COMMUNITY_PLUGIN_URL}/geoserver-${GEOSERVER_VERSION}-${EXTENSION}-plugin.zip" - download_extension ${URL} ${EXTENSION} + EXTENSION=$(echo "${EXTENSION}" | xargs) + [ -z "$EXTENSION" ] && continue + URL="${BASE_COMM_URL}/geoserver-${GEOSERVER_VERSION}-${EXTENSION}-plugin.zip" + download_extension "${URL}" "${EXTENSION}" done echo "Finished download of extensions" fi @@ -38,14 +124,22 @@ fi # Install the extensions echo "Starting installation of extensions" for EXTENSION in $(echo "${STABLE_EXTENSIONS},${COMMUNITY_EXTENSIONS}" | tr ',' ' '); do - ADDITIONAL_LIB=${ADDITIONAL_LIBS_DIR}geoserver-${GEOSERVER_VERSION}-${EXTENSION}-plugin.zip + EXTENSION=$(echo "${EXTENSION}" | xargs) + [ -z "$EXTENSION" ] && continue + # Validate extension name contains only safe characters (lowercase letters, numbers, hyphens, underscores) + if ! [[ "$EXTENSION" =~ ^[a-z0-9_-]+$ ]]; then + echo "WARNING: Skipping invalid extension name: ${EXTENSION}" >&2 + continue + fi + # Find downloaded plugin (handles both expected and discovered filenames) + ADDITIONAL_LIB=$(ls -1 "${ADDITIONAL_LIBS_DIR%/}"/geoserver-*-${EXTENSION}-plugin.zip 2>/dev/null | head -n 1 || true) [ -e "$ADDITIONAL_LIB" ] || continue if [[ $ADDITIONAL_LIB == *.zip ]]; then - unzip -q -o -d ${GEOSERVER_LIB_DIR} ${ADDITIONAL_LIB} "*.jar" + unzip -q -o -d "${GEOSERVER_LIB_DIR}" "${ADDITIONAL_LIB}" "*.jar" echo "Installed all jar files from ${ADDITIONAL_LIB}" elif [[ $ADDITIONAL_LIB == *.jar ]]; then - cp ${ADDITIONAL_LIB} ${GEOSERVER_LIB_DIR} + cp "${ADDITIONAL_LIB}" "${GEOSERVER_LIB_DIR}" echo "Installed ${ADDITIONAL_LIB}" else echo "Skipping ${ADDITIONAL_LIB}: unknown file extension."