Add convenience package build workflow for cloudberry-backup (#108)

Introduce a manually-triggered GitHub Actions workflow that builds
portable tar.gz packages for Apache cloudberry-backup from its
official source release tarball, and tests them against Apache
Cloudberry built from its own official source release.

Key design decisions:
- cloudberry-backup binaries are built on Rocky 8 (glibc 2.28)
  to maximise compatibility across Linux distributions.
- Cloudberry is built per-platform from its source release
  tarball (~8 min), avoiding dependence on pre-built Docker
  images or external DEB/RPM repositories.
- Build-and-test are fused into one job per platform: after
  Cloudberry compiles, gpdemo spins up directly in the same
  container, eliminating artifact packaging, transfer, and a
  separate test job.
- Only .sha512 checksums are generated; .asc signatures remain
  a release manager local step.
diff --git a/.github/workflows/package-convenience-binaries.yml b/.github/workflows/package-convenience-binaries.yml
new file mode 100644
index 0000000..40c50ef
--- /dev/null
+++ b/.github/workflows/package-convenience-binaries.yml
@@ -0,0 +1,924 @@
+# --------------------------------------------------------------------
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed
+# with this work for additional information regarding copyright
+# ownership. The ASF licenses this file to You under the Apache
+# License, Version 2.0 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of the
+# License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+# --------------------------------------------------------------------
+# GitHub Actions Workflow: Apache Cloudberry-backup Convenience Package Build
+# --------------------------------------------------------------------
+# Description:
+#
+#   This workflow manually builds convenience portable tarball packages
+#   from an ASF-approved Apache Cloudberry-backup source release tarball,
+#   and tests them against Apache Cloudberry built from its official
+#   source release tarball.
+#
+# Workflow Overview:
+#
+#   1. verify-cloudberry-backup-source
+#      Validates inputs, downloads cloudberry-backup source tarball
+#      + .asc + .sha512, verifies GPG signature and checksum, uploads
+#      verified source as a workflow artifact.
+#
+#   2. verify-cloudberry-source
+#      Same as above, but for the Cloudberry source release tarball.
+#
+#   3. build-backup-packages (matrix: amd64 / arm64)
+#      Downloads the verified cloudberry-backup source artifact, runs
+#      `make package` on Rocky 8 (glibc 2.28) for maximum run-time
+#      compatibility, generates .sha512 checksums, uploads per-arch
+#      portable tar.gz packages.
+#
+#   4. build-cloudberry (matrix: 5 platforms × amd64 / arm64 = 10)
+#      Extracts Cloudberry source, runs configure + build inside the
+#      official Cloudberry build container (~8 min per platform).
+#      After the build, Cloudberry is already installed at
+#      /usr/local/cloudberry-db.  The job then downloads the matching
+#      cloudberry-backup package, installs it, creates a gpdemo demo
+#      cluster, and runs a functional backup + restore smoke test —
+#      all inside the same container, no separate test job needed.
+#      Platforms: rocky8, rocky9, rocky10, ubuntu22.04, ubuntu24.04.
+#
+# Scope:
+# - Intended for official Apache Cloudberry-backup source releases
+#   managed by the release manager. It's designed for 2.2+ release.
+# - Produces convenience binaries only; detached .asc signatures remain a
+#   release manager local step.
+# --------------------------------------------------------------------
+
+name: Apache Cloudberry-backup Convenience Package Build
+
+on:
+  workflow_dispatch:
+    inputs:
+      # ================================================================
+      # cloudberry-backup source release inputs
+      # ================================================================
+      version:
+        description: '[cloudberry-backup] Release version, e.g. 2.2.0-incubating'
+        required: true
+        type: string
+      source_url:
+        description: '[cloudberry-backup] Apache source tarball URL from downloads.apache.org'
+        required: true
+        type: string
+      source_asc_url:
+        description: '[cloudberry-backup] Detached GPG signature URL for the source tarball (.asc)'
+        required: true
+        type: string
+      source_sha512_url:
+        description: '[cloudberry-backup] SHA-512 checksum URL for the source tarball (.sha512)'
+        required: true
+        type: string
+
+      # ================================================================
+      # Cloudberry source release inputs (for the test environment)
+      # ================================================================
+      cloudberry_version:
+        description: '[Cloudberry] Release version, e.g. 2.2.0-incubating'
+        required: true
+        type: string
+      cloudberry_source_url:
+        description: '[Cloudberry] Apache source tarball URL from downloads.apache.org'
+        required: true
+        type: string
+      cloudberry_source_asc_url:
+        description: '[Cloudberry] Detached GPG signature URL for the source tarball (.asc)'
+        required: true
+        type: string
+      cloudberry_source_sha512_url:
+        description: '[Cloudberry] SHA-512 checksum URL for the source tarball (.sha512)'
+        required: true
+        type: string
+
+permissions:
+  contents: read
+
+concurrency:
+  group: backup-package-build-${{ github.ref }}-${{ inputs.version }}
+  cancel-in-progress: true
+
+env:
+  LOG_RETENTION_DAYS: 14
+  KEYS_URL: https://downloads.apache.org/incubator/cloudberry/KEYS
+
+jobs:
+  # ====================================================================
+  # Job 1: Verify the Apache Cloudberry-backup source release
+  # ====================================================================
+  verify-cloudberry-backup-source:
+    name: Verify cloudberry-backup source
+    runs-on: ubuntu-24.04
+    timeout-minutes: 15
+    outputs:
+      source_tarball_name: ${{ steps.validate.outputs.source_tarball_name }}
+      artifact_name: ${{ steps.validate.outputs.artifact_name }}
+      packaging_version: ${{ steps.validate.outputs.packaging_version }}
+    steps:
+      - name: Validate manual inputs
+        id: validate
+        shell: bash
+        env:
+          VERSION: ${{ github.event.inputs.version }}
+          SOURCE_URL: ${{ github.event.inputs.source_url }}
+          SOURCE_ASC_URL: ${{ github.event.inputs.source_asc_url }}
+          SOURCE_SHA512_URL: ${{ github.event.inputs.source_sha512_url }}
+        run: |
+          set -euo pipefail
+
+          if [[ -z "${VERSION}" ]]; then
+            echo "::error::version must not be empty"
+            exit 1
+          fi
+
+          source_tarball_name="apache-cloudberry-backup-${VERSION}-src.tar.gz"
+          artifact_name="verified-source-release-cbbackup-${VERSION}"
+          packaging_version="${VERSION%-incubating}"
+
+          if [[ -z "${packaging_version}" ]]; then
+            echo "::error::Unable to derive packaging version from version=${VERSION}"
+            exit 1
+          fi
+
+          validate_apache_url() {
+            local value="$1"
+            local label="$2"
+            local prefix="https://downloads.apache.org/incubator/cloudberry/"
+            if [[ -z "${value}" ]]; then
+              echo "::error::${label} must not be empty"
+              exit 1
+            fi
+            if [[ "${value}" != "${prefix}"* ]]; then
+              echo "::error::${label} must use downloads.apache.org (got: ${value})"
+              exit 1
+            fi
+          }
+
+          validate_apache_url "${SOURCE_URL}" "source_url"
+          validate_apache_url "${SOURCE_ASC_URL}" "source_asc_url"
+          validate_apache_url "${SOURCE_SHA512_URL}" "source_sha512_url"
+
+          if [[ "${SOURCE_URL}" != */"${source_tarball_name}" ]]; then
+            echo "::error::source_url must end with /${source_tarball_name}"
+            exit 1
+          fi
+          if [[ "${SOURCE_ASC_URL}" != */"${source_tarball_name}.asc" ]]; then
+            echo "::error::source_asc_url must end with /${source_tarball_name}.asc"
+            exit 1
+          fi
+          if [[ "${SOURCE_SHA512_URL}" != */"${source_tarball_name}.sha512" ]]; then
+            echo "::error::source_sha512_url must end with /${source_tarball_name}.sha512"
+            exit 1
+          fi
+
+          echo "source_tarball_name=${source_tarball_name}" >> "${GITHUB_OUTPUT}"
+          echo "artifact_name=${artifact_name}" >> "${GITHUB_OUTPUT}"
+          echo "packaging_version=${packaging_version}" >> "${GITHUB_OUTPUT}"
+
+      - name: Download source release and verification files
+        shell: bash
+        env:
+          SOURCE_URL: ${{ github.event.inputs.source_url }}
+          SOURCE_ASC_URL: ${{ github.event.inputs.source_asc_url }}
+          SOURCE_SHA512_URL: ${{ github.event.inputs.source_sha512_url }}
+          SOURCE_TARBALL_NAME: ${{ steps.validate.outputs.source_tarball_name }}
+        run: |
+          set -euo pipefail
+          mkdir -p verified-source
+
+          echo "=== Downloading source tarball... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/${SOURCE_TARBALL_NAME}" \
+            "${SOURCE_URL}"
+
+          echo "=== Downloading signature... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/${SOURCE_TARBALL_NAME}.asc" \
+            "${SOURCE_ASC_URL}"
+
+          echo "=== Downloading checksum... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/${SOURCE_TARBALL_NAME}.sha512" \
+            "${SOURCE_SHA512_URL}"
+
+          echo "=== Downloading KEYS... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/KEYS" \
+            "${KEYS_URL}"
+
+          echo "=== All files downloaded successfully. ==="
+          ls -la verified-source/
+
+      - name: Verify source release signature and checksum
+        shell: bash
+        env:
+          SOURCE_TARBALL_NAME: ${{ steps.validate.outputs.source_tarball_name }}
+        run: |
+          set -euo pipefail
+
+          export GNUPGHOME="${RUNNER_TEMP}/gnupg"
+          mkdir -p "${GNUPGHOME}"
+          chmod 700 "${GNUPGHOME}"
+
+          echo "=== Importing project KEYS... ==="
+          gpg --import verified-source/KEYS
+
+          echo "=== Verifying GPG signature... ==="
+          gpg --verify \
+            "verified-source/${SOURCE_TARBALL_NAME}.asc" \
+            "verified-source/${SOURCE_TARBALL_NAME}"
+
+          echo "=== Verifying SHA-512 checksum... ==="
+          (
+            cd verified-source
+            sha512sum -c "${SOURCE_TARBALL_NAME}.sha512"
+          )
+
+          echo "=== Verifying tarball integrity (listing contents)... ==="
+          tar -tzf "verified-source/${SOURCE_TARBALL_NAME}" >/dev/null
+          echo "=== cloudberry-backup source release verification passed. ==="
+
+      - name: Summarize verified source release
+        shell: bash
+        env:
+          VERSION: ${{ github.event.inputs.version }}
+          SOURCE_URL: ${{ github.event.inputs.source_url }}
+          SOURCE_ASC_URL: ${{ github.event.inputs.source_asc_url }}
+          SOURCE_SHA512_URL: ${{ github.event.inputs.source_sha512_url }}
+        run: |
+          {
+            echo "# Verified cloudberry-backup source release"
+            echo "- Version: ${VERSION}"
+            echo "- Packaging version: ${{ steps.validate.outputs.packaging_version }}"
+            echo "- Source URL: ${SOURCE_URL}"
+            echo "- Signature URL: ${SOURCE_ASC_URL}"
+            echo "- Checksum URL: ${SOURCE_SHA512_URL}"
+            echo "- KEYS URL: ${KEYS_URL}"
+            echo "- GPG verification: PASS"
+            echo "- SHA-512 verification: PASS"
+          } >> "${GITHUB_STEP_SUMMARY}"
+
+      - name: Upload verified source release
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ steps.validate.outputs.artifact_name }}
+          retention-days: ${{ env.LOG_RETENTION_DAYS }}
+          if-no-files-found: error
+          path: verified-source/*
+
+  # ====================================================================
+  # Job 2: Verify the Apache Cloudberry source release
+  # ====================================================================
+  verify-cloudberry-source:
+    name: Verify Cloudberry source
+    runs-on: ubuntu-24.04
+    timeout-minutes: 15
+    outputs:
+      source_tarball_name: ${{ steps.validate.outputs.source_tarball_name }}
+      artifact_name: ${{ steps.validate.outputs.artifact_name }}
+    steps:
+      - name: Validate manual inputs
+        id: validate
+        shell: bash
+        env:
+          VERSION: ${{ github.event.inputs.cloudberry_version }}
+          SOURCE_URL: ${{ github.event.inputs.cloudberry_source_url }}
+          SOURCE_ASC_URL: ${{ github.event.inputs.cloudberry_source_asc_url }}
+          SOURCE_SHA512_URL: ${{ github.event.inputs.cloudberry_source_sha512_url }}
+        run: |
+          set -euo pipefail
+
+          if [[ -z "${VERSION}" ]]; then
+            echo "::error::cloudberry_version must not be empty"
+            exit 1
+          fi
+
+          source_tarball_name="apache-cloudberry-${VERSION}-src.tar.gz"
+          artifact_name="verified-source-release-cloudberry-${VERSION}"
+
+          validate_apache_url() {
+            local value="$1"
+            local label="$2"
+            local prefix="https://downloads.apache.org/incubator/cloudberry/"
+            if [[ -z "${value}" ]]; then
+              echo "::error::${label} must not be empty"
+              exit 1
+            fi
+            if [[ "${value}" != "${prefix}"* ]]; then
+              echo "::error::${label} must use downloads.apache.org (got: ${value})"
+              exit 1
+            fi
+          }
+
+          validate_apache_url "${SOURCE_URL}" "cloudberry_source_url"
+          validate_apache_url "${SOURCE_ASC_URL}" "cloudberry_source_asc_url"
+          validate_apache_url "${SOURCE_SHA512_URL}" "cloudberry_source_sha512_url"
+
+          if [[ "${SOURCE_URL}" != */"${source_tarball_name}" ]]; then
+            echo "::error::cloudberry_source_url must end with /${source_tarball_name}"
+            exit 1
+          fi
+          if [[ "${SOURCE_ASC_URL}" != */"${source_tarball_name}.asc" ]]; then
+            echo "::error::cloudberry_source_asc_url must end with /${source_tarball_name}.asc"
+            exit 1
+          fi
+          if [[ "${SOURCE_SHA512_URL}" != */"${source_tarball_name}.sha512" ]]; then
+            echo "::error::cloudberry_source_sha512_url must end with /${source_tarball_name}.sha512"
+            exit 1
+          fi
+
+          echo "source_tarball_name=${source_tarball_name}" >> "${GITHUB_OUTPUT}"
+          echo "artifact_name=${artifact_name}" >> "${GITHUB_OUTPUT}"
+
+      - name: Download source release and verification files
+        shell: bash
+        env:
+          SOURCE_URL: ${{ github.event.inputs.cloudberry_source_url }}
+          SOURCE_ASC_URL: ${{ github.event.inputs.cloudberry_source_asc_url }}
+          SOURCE_SHA512_URL: ${{ github.event.inputs.cloudberry_source_sha512_url }}
+          SOURCE_TARBALL_NAME: ${{ steps.validate.outputs.source_tarball_name }}
+        run: |
+          set -euo pipefail
+          mkdir -p verified-source
+
+          echo "=== Downloading Cloudberry source tarball... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/${SOURCE_TARBALL_NAME}" \
+            "${SOURCE_URL}"
+
+          echo "=== Downloading signature... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/${SOURCE_TARBALL_NAME}.asc" \
+            "${SOURCE_ASC_URL}"
+
+          echo "=== Downloading checksum... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/${SOURCE_TARBALL_NAME}.sha512" \
+            "${SOURCE_SHA512_URL}"
+
+          echo "=== Downloading KEYS... ==="
+          curl --fail --location --silent --show-error \
+            --output "verified-source/KEYS" \
+            "${KEYS_URL}"
+
+          ls -la verified-source/
+
+      - name: Verify source release signature and checksum
+        shell: bash
+        env:
+          SOURCE_TARBALL_NAME: ${{ steps.validate.outputs.source_tarball_name }}
+        run: |
+          set -euo pipefail
+
+          export GNUPGHOME="${RUNNER_TEMP}/gnupg"
+          mkdir -p "${GNUPGHOME}"
+          chmod 700 "${GNUPGHOME}"
+
+          echo "=== Importing project KEYS... ==="
+          gpg --import verified-source/KEYS
+
+          echo "=== Verifying GPG signature... ==="
+          gpg --verify \
+            "verified-source/${SOURCE_TARBALL_NAME}.asc" \
+            "verified-source/${SOURCE_TARBALL_NAME}"
+
+          echo "=== Verifying SHA-512 checksum... ==="
+          (
+            cd verified-source
+            sha512sum -c "${SOURCE_TARBALL_NAME}.sha512"
+          )
+
+          tar -tzf "verified-source/${SOURCE_TARBALL_NAME}" >/dev/null
+          echo "=== Cloudberry source release verification passed. ==="
+
+      - name: Summarize verified source release
+        shell: bash
+        env:
+          VERSION: ${{ github.event.inputs.cloudberry_version }}
+          SOURCE_URL: ${{ github.event.inputs.cloudberry_source_url }}
+          SOURCE_ASC_URL: ${{ github.event.inputs.cloudberry_source_asc_url }}
+          SOURCE_SHA512_URL: ${{ github.event.inputs.cloudberry_source_sha512_url }}
+        run: |
+          {
+            echo "# Verified Cloudberry source release"
+            echo "- Version: ${VERSION}"
+            echo "- Source URL: ${SOURCE_URL}"
+            echo "- Signature URL: ${SOURCE_ASC_URL}"
+            echo "- Checksum URL: ${SOURCE_SHA512_URL}"
+            echo "- KEYS URL: ${KEYS_URL}"
+            echo "- GPG verification: PASS"
+            echo "- SHA-512 verification: PASS"
+          } >> "${GITHUB_STEP_SUMMARY}"
+
+      - name: Upload verified source release
+        uses: actions/upload-artifact@v4
+        with:
+          name: ${{ steps.validate.outputs.artifact_name }}
+          retention-days: ${{ env.LOG_RETENTION_DAYS }}
+          if-no-files-found: error
+          path: verified-source/*
+
+  # ====================================================================
+  # Job 3: Build cloudberry-backup portable tarball packages.
+  #
+  # Runs BEFORE build-cloudberry so the backup package is ready when
+  # Cloudberry finishes building.  Built on Rocky 8 (glibc 2.28) for
+  # maximum run-time compatibility across Linux distributions.
+  # ====================================================================
+  build-backup-packages:
+    name: Build backup ${{ matrix.arch }}
+    needs: verify-cloudberry-backup-source
+    runs-on: ${{ matrix.runner }}
+    timeout-minutes: 30
+    container:
+      image: ${{ matrix.build_container_image }}
+      options: >-
+        --user root
+        --hostname cdw
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - arch: linux-amd64
+            runner: ubuntu-24.04
+            goarch: amd64
+            build_container_image: apache/incubator-cloudberry:cbdb-build-rocky8-latest
+          - arch: linux-arm64
+            runner: ubuntu-24.04-arm
+            goarch: arm64
+            build_container_image: apache/incubator-cloudberry:cbdb-build-rocky8-latest
+
+    steps:
+      - name: Initialize build container
+        shell: bash
+        run: |
+          set -euo pipefail
+          su - gpadmin -c "/tmp/init_system.sh"
+
+      - name: Verify build environment
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          echo "Build host: $(uname -m)"
+          su - gpadmin -c "
+            echo \"Go version: \$(go version)\"
+            echo \"GCC version: \$(gcc --version | head -1 || true)\"
+            echo \"glibc version: \$(ldd --version 2>&1 | head -1 || true)\"
+          "
+
+      - name: Download verified cloudberry-backup source
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ needs.verify-cloudberry-backup-source.outputs.artifact_name }}
+          path: ${{ github.workspace }}/verified-source
+
+      - name: Extract source release
+        id: extract
+        shell: bash
+        env:
+          SOURCE_TARBALL_NAME: ${{ needs.verify-cloudberry-backup-source.outputs.source_tarball_name }}
+        run: |
+          set -euo pipefail
+          tarball_path="${GITHUB_WORKSPACE}/verified-source/${SOURCE_TARBALL_NAME}"
+          source_root_name="$(tar -tzf "${tarball_path}" | head -1 | cut -d/ -f1 || true)"
+
+          echo "Extracting source: ${source_root_name}"
+          tar -xzf "${tarball_path}" -C "${GITHUB_WORKSPACE}"
+          source_dir="${GITHUB_WORKSPACE}/${source_root_name}"
+          echo "source_dir=${source_dir}" >> "${GITHUB_OUTPUT}"
+          echo "Source extracted to: ${source_dir}"
+          ls -la "${source_dir}"
+
+      - name: Build convenience package
+        id: build
+        shell: bash
+        env:
+          SOURCE_DIR: ${{ steps.extract.outputs.source_dir }}
+          VERSION: ${{ github.event.inputs.version }}
+        run: |
+          set -euo pipefail
+
+          # Give gpadmin ownership so make package can write build/ artifacts
+          chown -R gpadmin:gpadmin "${SOURCE_DIR}"
+
+          echo "Building package natively on $(uname -m)..."
+          su - gpadmin -c "
+            set -euo pipefail
+            export GOPATH=\$HOME/go
+            export PATH=\$PATH:/usr/local/go/bin:\$GOPATH/bin
+            cd '${SOURCE_DIR}'
+            make package 2>&1
+          "
+
+          build_dir="${SOURCE_DIR}/build"
+          package_file=$(ls -1 "${build_dir}"/*.tar.gz 2>/dev/null | head -1 || true)
+          if [[ -z "${package_file}" || ! -f "${package_file}" ]]; then
+            echo "::error::Package file not found in ${build_dir}"
+            ls -la "${build_dir}" || true
+            exit 1
+          fi
+          echo "Package built: ${package_file}"
+          echo "package_file=${package_file}" >> "${GITHUB_OUTPUT}"
+          echo "Package contents:"
+          tar -tzf "${package_file}"
+
+      - name: Generate SHA512 checksum
+        shell: bash
+        env:
+          BUILD_DIR: ${{ steps.extract.outputs.source_dir }}/build
+        run: |
+          set -euo pipefail
+          artifact_dir="${GITHUB_WORKSPACE}/package-artifacts"
+          mkdir -p "${artifact_dir}"
+          cp "${BUILD_DIR}"/*.tar.gz "${artifact_dir}/"
+          (
+            cd "${artifact_dir}"
+            for pkg in *.tar.gz; do
+              sha512sum "${pkg}" > "${pkg}.sha512"
+              echo "Generated: ${pkg}.sha512"
+              cat "${pkg}.sha512"
+            done
+          )
+
+      - name: Summarize build
+        shell: bash
+        env:
+          VERSION: ${{ github.event.inputs.version }}
+          ARCH: ${{ matrix.arch }}
+        run: |
+          artifact_dir="${GITHUB_WORKSPACE}/package-artifacts"
+          {
+            echo "# Build: ${ARCH}"
+            echo "- Release version: ${VERSION}"
+            echo "- Architecture: ${ARCH}"
+            echo "- Runner: ${{ matrix.runner }}"
+            echo "- Build container: ${{ matrix.build_container_image }}"
+            echo "- glibc baseline: Rocky 8 (glibc 2.28) for max compatibility"
+            echo "- Packages and checksums:"
+            (
+              cd "${artifact_dir}"
+              for pkg in *.tar.gz; do
+                echo "  - ${pkg}"
+                echo "  - ${pkg}.sha512"
+              done
+            )
+          } >> "${GITHUB_STEP_SUMMARY}"
+
+      - name: Upload package artifacts
+        uses: actions/upload-artifact@v4
+        with:
+          name: packages-${{ matrix.arch }}
+          retention-days: ${{ env.LOG_RETENTION_DAYS }}
+          if-no-files-found: error
+          path: package-artifacts/
+
+  # ====================================================================
+  # Job 4: Build Apache Cloudberry from verified source (~8 min),
+  #        then test the cloudberry-backup package against it.
+  #
+  # Cloudberry is built and installed inside the build container at
+  # /usr/local/cloudberry-db.  After the build, we download the
+  # cloudberry-backup package artifact, install it, spin up gpdemo,
+  # and run a functional backup + restore smoke test — all in one job.
+  # ====================================================================
+  build-cloudberry:
+    name: Build & test ${{ matrix.target_os }}-${{ matrix.arch }}
+    needs:
+      - verify-cloudberry-source
+      - build-backup-packages
+    runs-on: ${{ matrix.runner }}
+    timeout-minutes: 75
+    container:
+      image: ${{ matrix.build_container_image }}
+      options: >-
+        --user root
+        --hostname cdw
+        --shm-size=2gb
+        -v /usr/share:/host_usr_share
+        -v /usr/local:/host_usr_local
+        -v /opt:/host_opt
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          # Rocky 8
+          - {target_os: rocky8, arch: amd64, backup_arch: linux-amd64, runner: ubuntu-24.04, build_container_image: apache/incubator-cloudberry:cbdb-build-rocky8-latest}
+          - {target_os: rocky8, arch: arm64, backup_arch: linux-arm64, runner: ubuntu-24.04-arm, build_container_image: apache/incubator-cloudberry:cbdb-build-rocky8-latest}
+          # Rocky 9
+          - {target_os: rocky9, arch: amd64, backup_arch: linux-amd64, runner: ubuntu-24.04, build_container_image: apache/incubator-cloudberry:cbdb-build-rocky9-latest}
+          - {target_os: rocky9, arch: arm64, backup_arch: linux-arm64, runner: ubuntu-24.04-arm, build_container_image: apache/incubator-cloudberry:cbdb-build-rocky9-latest}
+          # Rocky 10
+          - {target_os: rocky10, arch: amd64, backup_arch: linux-amd64, runner: ubuntu-24.04, build_container_image: apache/incubator-cloudberry:cbdb-build-rocky10-latest}
+          - {target_os: rocky10, arch: arm64, backup_arch: linux-arm64, runner: ubuntu-24.04-arm, build_container_image: apache/incubator-cloudberry:cbdb-build-rocky10-latest}
+          # Ubuntu 22.04
+          - {target_os: ubuntu22.04, arch: amd64, backup_arch: linux-amd64, runner: ubuntu-24.04, build_container_image: apache/incubator-cloudberry:cbdb-build-ubuntu22.04-latest}
+          - {target_os: ubuntu22.04, arch: arm64, backup_arch: linux-arm64, runner: ubuntu-24.04-arm, build_container_image: apache/incubator-cloudberry:cbdb-build-ubuntu22.04-latest}
+          # Ubuntu 24.04
+          - {target_os: ubuntu24.04, arch: amd64, backup_arch: linux-amd64, runner: ubuntu-24.04, build_container_image: apache/incubator-cloudberry:cbdb-build-ubuntu24.04-latest}
+          - {target_os: ubuntu24.04, arch: arm64, backup_arch: linux-arm64, runner: ubuntu-24.04-arm, build_container_image: apache/incubator-cloudberry:cbdb-build-ubuntu24.04-latest}
+
+    steps:
+      # ----------------------------------------------------------------
+      # Phase A: Build Cloudberry from verified source
+      # ----------------------------------------------------------------
+
+      - name: Free disk space
+        shell: bash
+        run: |
+          set -euo pipefail
+          rm -rf /host_opt/hostedtoolcache || true
+          rm -rf /host_usr_local/lib/android || true
+          rm -rf /host_usr_share/dotnet || true
+          rm -rf /host_opt/ghc || true
+          rm -rf /host_usr_local/.ghcup || true
+          rm -rf /host_usr_share/swift || true
+          rm -rf /host_usr_local/share/powershell || true
+          rm -rf /host_usr_local/share/chromium || true
+          rm -rf /host_usr_share/miniconda || true
+          rm -rf /host_opt/az || true
+          rm -rf /host_usr_share/sbt || true
+          df -h /
+
+      - name: Initialize build container
+        shell: bash
+        run: |
+          set -euo pipefail
+          su - gpadmin -c "/tmp/init_system.sh"
+
+      - name: Download verified Cloudberry source
+        uses: actions/download-artifact@v4
+        with:
+          name: ${{ needs.verify-cloudberry-source.outputs.artifact_name }}
+          path: ${{ github.workspace }}/verified-source
+
+      - name: Extract Cloudberry source
+        id: extract
+        shell: bash
+        env:
+          SOURCE_TARBALL_NAME: ${{ needs.verify-cloudberry-source.outputs.source_tarball_name }}
+        run: |
+          set -euo pipefail
+          tarball_path="${GITHUB_WORKSPACE}/verified-source/${SOURCE_TARBALL_NAME}"
+          source_root_name="$(tar -tzf "${tarball_path}" | head -1 | cut -d/ -f1 || true)"
+
+          tar -xzf "${tarball_path}" -C "${GITHUB_WORKSPACE}"
+          mkdir -p "${GITHUB_WORKSPACE}/cloudberry"
+          mv "${GITHUB_WORKSPACE}/${source_root_name}"/* "${GITHUB_WORKSPACE}/cloudberry/"
+          mv "${GITHUB_WORKSPACE}/${source_root_name}"/.[!.]* "${GITHUB_WORKSPACE}/cloudberry/" 2>/dev/null || true
+          rmdir "${GITHUB_WORKSPACE}/${source_root_name}"
+
+          source_dir="${GITHUB_WORKSPACE}/cloudberry"
+          chown -R gpadmin:gpadmin "${GITHUB_WORKSPACE}"
+          echo "source_dir=${source_dir}" >> "${GITHUB_OUTPUT}"
+          echo "Cloudberry source extracted to: ${source_dir}"
+
+      - name: Configure Cloudberry
+        shell: bash
+        env:
+          SOURCE_DIR: ${{ steps.extract.outputs.source_dir }}
+        run: |
+          set -euo pipefail
+          export BUILD_DESTINATION="/usr/local/cloudberry-db"
+
+          mkdir -p "${SOURCE_DIR}/build-logs"
+          chown -R gpadmin:gpadmin "${SOURCE_DIR}/build-logs"
+          chmod +x "${SOURCE_DIR}"/devops/build/automation/cloudberry/scripts/configure-cloudberry.sh
+
+          echo "=== Configuring Cloudberry... ==="
+          if ! su - gpadmin -c "cd ${SOURCE_DIR} && SRC_DIR=${SOURCE_DIR} BUILD_USER=github-actions BUILD_DESTINATION=${BUILD_DESTINATION} ${SOURCE_DIR}/devops/build/automation/cloudberry/scripts/configure-cloudberry.sh"; then
+            echo "::error::Cloudberry configure script failed"
+            exit 1
+          fi
+          echo "=== Cloudberry configuration complete. ==="
+
+      - name: Build Cloudberry
+        shell: bash
+        env:
+          SOURCE_DIR: ${{ steps.extract.outputs.source_dir }}
+        run: |
+          set -euo pipefail
+          export BUILD_DESTINATION="/usr/local/cloudberry-db"
+
+          chmod +x "${SOURCE_DIR}"/devops/build/automation/cloudberry/scripts/build-cloudberry.sh
+
+          echo "=== Building Cloudberry... ==="
+          if ! su - gpadmin -c "cd ${SOURCE_DIR} && SRC_DIR=${SOURCE_DIR} BUILD_DESTINATION=${BUILD_DESTINATION} ${SOURCE_DIR}/devops/build/automation/cloudberry/scripts/build-cloudberry.sh"; then
+            echo "::error::Cloudberry build script failed"
+            exit 1
+          fi
+          echo "=== Cloudberry build complete. ==="
+
+      - name: Verify Cloudberry installation
+        shell: bash
+        run: |
+          set -euo pipefail
+          echo "Verifying Cloudberry installation..."
+          if [[ ! -f /usr/local/cloudberry-db/cloudberry-env.sh ]]; then
+            echo "::error::Cloudberry installation not found at /usr/local/cloudberry-db"
+            exit 1
+          fi
+          ls -la /usr/local/cloudberry-db/bin/
+          echo "Cloudberry installation verified."
+
+      # ----------------------------------------------------------------
+      # Phase B: Download & test the cloudberry-backup package
+      # ----------------------------------------------------------------
+
+      - name: Download backup package
+        uses: actions/download-artifact@v4
+        with:
+          name: packages-${{ matrix.backup_arch }}
+          path: ${{ github.workspace }}/package-artifacts
+
+      - name: Verify backup package checksum
+        id: verify-package
+        shell: bash
+        run: |
+          set -euo pipefail
+          artifact_dir="${GITHUB_WORKSPACE}/package-artifacts"
+
+          package_file=$(ls "${artifact_dir}"/*.tar.gz 2>/dev/null | head -1 || true)
+          if [[ -z "${package_file}" || ! -f "${package_file}" ]]; then
+            echo "::error::No tar.gz package found in ${artifact_dir}"
+            ls -la "${artifact_dir}" || true
+            exit 1
+          fi
+
+          checksum_file="${package_file}.sha512"
+          if [[ ! -f "${checksum_file}" ]]; then
+            echo "::error::Checksum file not found: ${checksum_file}"
+            exit 1
+          fi
+
+          ( cd "${artifact_dir}" && sha512sum -c "$(basename "${checksum_file}")" )
+          echo "package_file=${package_file}" >> "${GITHUB_OUTPUT}"
+          echo "Checksum verification passed."
+
+      - name: Extract and install package
+        shell: bash
+        env:
+          PACKAGE_FILE: ${{ steps.verify-package.outputs.package_file }}
+        run: |
+          set -euo pipefail
+
+          work_dir="${GITHUB_WORKSPACE}/pkg-test"
+          mkdir -p "${work_dir}"
+
+          echo "=== Extracting package ==="
+          tar -xzf "${PACKAGE_FILE}" -C "${work_dir}"
+
+          extracted_dir=$(ls -1d "${work_dir}"/apache-cloudberry-backup-incubating-*/ 2>/dev/null | head -1 || true)
+          if [[ -z "${extracted_dir}" ]]; then
+            echo "::error::Extracted package directory not found"
+            ls -la "${work_dir}"
+            exit 1
+          fi
+
+          echo "Package extracted to: ${extracted_dir}"
+          echo "Extracted contents:"
+          ls -lhR "${extracted_dir}"
+
+          echo "=== Installing via install.sh ==="
+          chmod +x "${extracted_dir}/install.sh"
+          sed -i 's/sudo //g' "${extracted_dir}/install.sh"
+          export GPHOME="/usr/local/cloudberry-db"
+          "${extracted_dir}/install.sh" 2>&1
+          echo "Installation complete."
+
+      - name: Verify installed commands
+        shell: bash
+        run: |
+          set -euo pipefail
+          GPHOME="/usr/local/cloudberry-db"
+          expected="gpbackup gprestore gpbackup_helper gpbackup_s3_plugin gpbackman gpbackup_exporter"
+
+          echo "=== Verifying installed binaries ==="
+          for binary in ${expected}; do
+            bin_path="${GPHOME}/bin/${binary}"
+            if [[ ! -f "${bin_path}" ]]; then
+              echo "::warning::${binary} not found — may be absent in older releases"
+              continue
+            fi
+            version_output=$("${bin_path}" --version 2>&1) || {
+              echo "::warning::${binary} --version failed"
+              continue
+            }
+            echo "  ${binary}: ${version_output}"
+          done
+          echo "Command verification complete."
+
+      - name: Run backup/restore functional test
+        shell: bash
+        env:
+          TARGET_OS: ${{ matrix.target_os }}
+          ARCH: ${{ matrix.arch }}
+        run: |
+          set -euo pipefail
+
+          work_dir="${GITHUB_WORKSPACE}/functional-test"
+          mkdir -p "${work_dir}"
+          chown -R gpadmin:gpadmin "${GITHUB_WORKSPACE}"
+
+          cat <<'SCRIPT' > /tmp/run_backup_restore_test.sh
+          #!/bin/bash
+          set -euo pipefail
+
+          GPHOME="/usr/local/cloudberry-db"
+
+          source "${GPHOME}/cloudberry-env.sh"
+
+          echo "=== Creating demo cluster ==="
+          cd "${HOME}"
+          gpdemo
+          source "${HOME}/gpdemo-env.sh"
+
+          echo "=== Verifying cluster is running ==="
+          psql -d postgres -c "SELECT 1 AS cluster_ok;"
+          psql -d postgres -c "SELECT version();"
+
+          echo "=== Preparing test database ==="
+          test_db="package_test_db"
+          psql -d postgres -c "DROP DATABASE IF EXISTS ${test_db}"
+          createdb "${test_db}"
+          psql -d "${test_db}" -c "CREATE TABLE test_tbl (id int, name text) DISTRIBUTED RANDOMLY;"
+          psql -d "${test_db}" -c "INSERT INTO test_tbl VALUES (1, 'hello'), (2, 'world');"
+          psql -d "${test_db}" -c "SELECT count(*) AS row_count FROM test_tbl;"
+
+          echo "=== Running gpbackup ==="
+          backup_dir="/tmp/backup_restore_test"
+          rm -rf "${backup_dir}"
+          mkdir -p "${backup_dir}"
+
+          backup_log="${backup_dir}/gpbackup_output.log"
+          gpbackup --dbname "${test_db}" --backup-dir "${backup_dir}" 2>&1 | tee "${backup_log}"
+
+          timestamp=$(grep -E "Backup Timestamp[[:space:]]*=" "${backup_log}" | grep -Eo "[[:digit:]]{14}" | head -1 || true)
+
+          if [[ -z "${timestamp}" ]]; then
+            latest_gpbackup_log=$(ls -1t "${HOME}/gpAdminLogs"/gpbackup_*.log 2>/dev/null | head -1 || true)
+            if [[ -n "${latest_gpbackup_log}" ]]; then
+              timestamp=$(grep -E "Backup Timestamp[[:space:]]*=" "${latest_gpbackup_log}" | grep -Eo "[[:digit:]]{14}" | head -1 || true)
+            fi
+          fi
+
+          if [[ -z "${timestamp}" ]]; then
+            echo "ERROR: Could not parse backup timestamp from gpbackup logs"
+            echo "=== Backup log ==="
+            cat "${backup_log}" || true
+            echo "=== Backup directory contents ==="
+            find "${backup_dir}" -type f | sort
+            exit 1
+          fi
+
+          echo "Backup timestamp: ${timestamp}"
+
+          echo "=== Dropping test database ==="
+          dropdb "${test_db}"
+
+          echo "=== Running gprestore ==="
+          gprestore --timestamp "${timestamp}" --backup-dir "${backup_dir}" --create-db --on-error-continue 2>&1
+
+          echo "=== Verification: connecting to restored database ==="
+          row_count=$(psql -d "${test_db}" -t -c "SELECT count(*) FROM test_tbl" | xargs)
+          if [[ "${row_count}" != "2" ]]; then
+            echo "ERROR: Expected 2 rows in restored test_tbl, got ${row_count}"
+            exit 1
+          fi
+          echo "Restore verified: ${row_count} rows in test_tbl"
+
+          echo "=== All functional tests passed ==="
+          SCRIPT
+
+          chmod +x /tmp/run_backup_restore_test.sh
+
+          set +e
+          su - gpadmin -c "/tmp/run_backup_restore_test.sh"
+          test_status=$?
+          set -e
+
+          {
+            echo "## Cloudberry-backup package functional test"
+            echo "- Target: ${{ matrix.target_os }}/${{ matrix.arch }}"
+            echo "- Package: ${{ steps.verify-package.outputs.package_file }}"
+            if [[ ${test_status} -eq 0 ]]; then
+              echo "- Result: PASS"
+            else
+              echo "- Result: FAIL"
+            fi
+          } >> "${GITHUB_STEP_SUMMARY}"
+
+          exit ${test_status}