From 5cf1ace13d74a5505cf452b06c9c910f90a35682 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Thu, 9 Jul 2026 18:20:12 +0500 Subject: [PATCH 1/2] Build & push native RPM/DEB packages on tag --- .github/workflows/release.yml | 1023 ++++++++++++++++++++++++++++++++- common/build.sh | 29 + common/common-functions.sh | 281 +++++++++ pkg/build-deb.sh | 85 +++ pkg/build-rpm.sh | 74 +++ pkg/common.sh | 25 + pkg/common/config.yml | 9 + pkg/deb/debian/control | 14 + pkg/deb/debian/docs | 1 + pkg/deb/debian/format | 2 + pkg/deb/debian/rules | 23 + pkg/rpm/docloader.spec | 49 ++ 12 files changed, 1591 insertions(+), 24 deletions(-) create mode 100644 common/build.sh create mode 100755 common/common-functions.sh create mode 100755 pkg/build-deb.sh create mode 100755 pkg/build-rpm.sh create mode 100755 pkg/common.sh create mode 100644 pkg/common/config.yml create mode 100644 pkg/deb/debian/control create mode 100644 pkg/deb/debian/docs create mode 100644 pkg/deb/debian/format create mode 100644 pkg/deb/debian/rules create mode 100644 pkg/rpm/docloader.spec diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a406e48..2bdbad1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,32 +1,1007 @@ +# ------------------------------------------------------------------------- +# +# pgEdge Docloader +# +# Copyright (c) 2025 - 2026, pgEdge, Inc. +# This software is released under The PostgreSQL License +# +# Release pipeline: +# 1. GoReleaser builds the Go binaries and publishes the GitHub release +# with all tarballs. build-rpm.sh / build-deb.sh wget the source +# tarball for their arch straight from the release URL, so the +# release MUST exist before the RPM/DEB build cells run — hence the +# package jobs depend on `goreleaser`. +# 2. Build RPMs (almalinux 9/10, amd64+arm64) and DEBs (jammy / noble / +# bullseye / bookworm / trixie, amd64+arm64) via pgedge-builder-action. +# Each cell's build script downloads its own source tarball from the +# release, so there is no artifact-staging step (unlike the Go-binary +# pipelines that stage tarballs from workflow artifacts). +# 3. Push RPMs to dnf.pgedge.com and DEBs to apt.pgedge.com (batched +# multi-target invocation per family; cross-repo lock is per +# (family, repo_type)). +# 4. Publish a manifest under _pending/// for QA to +# certify; promote.yml in pgedge-repo-manager picks it up from there. +# +# ------------------------------------------------------------------------- + name: Release on: - push: - tags: - - 'v*' + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + simulate_tag: + description: | + Pretend a v* tag was pushed (e.g. "v1.0.0-test1") so we + can exercise determine-repo-type / package-rpm / package-deb + without writing to a live GitHub release or live repos. + Leave empty for a normal branch test (build cells skipped). + required: false + default: '' + force_push: + description: | + Publish RPMs/DEBs even if some matrix cells failed. Only + honored on real tag pushes. Default false. + required: false + default: 'false' permissions: - contents: write + # Least-privilege default — overridden per-job only where write is + # genuinely needed (see the `goreleaser` job for the contents: write + # override required by GoReleaser to publish the GitHub release). + contents: read + +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: false jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.25' - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 - with: - distribution: goreleaser - version: latest - args: release --clean + # ========================================================================= + # Build matrix — delegated to pgedge-detect-build-matrix so this workflow + # uses the same cell schema (component / image / arch / pg_version / …) as + # the unified extension pipeline. pkg/ is classified as pg-agnostic + # (no PER_PG_VERSION, not postgresql-N / libpq / pgedge-enterprise-*), so + # the action emits cells with empty pg_version and pg_in_name=false. + # Edit the `archs`, `rpm_images`, `deb_images` inputs to add/remove + # platforms; the action defaults match the standard pgedge build matrix. + # ========================================================================= + detect-matrix: + name: Detect build matrix + runs-on: ubuntu-latest + outputs: + rpm_matrix: ${{ steps.detect.outputs.rpm_matrix }} + deb_matrix: ${{ steps.detect.outputs.deb_matrix }} + has_rpm: ${{ steps.detect.outputs.has_rpm }} + has_deb: ${{ steps.detect.outputs.has_deb }} + dnf_ts: ${{ steps.detect.outputs.dnf_ts }} + apt_ts: ${{ steps.detect.outputs.apt_ts }} + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Checkout pgedge-detect-build-matrix + env: + TOKEN: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + run: | + set -euo pipefail + mkdir -p .github/actions + git clone --depth 1 \ + "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-detect-build-matrix.git" \ + ".github/actions/pgedge-detect-build-matrix" + + - name: Detect build matrix + id: detect + uses: ./.github/actions/pgedge-detect-build-matrix + with: + component_name: pkg + # pgedge-docloader has no postgresql-N/ siblings; pkg is + # pg-agnostic so the action accepts an empty input and + # emits cells with pg_version="" (see pgedge-detect-build-matrix README). + archs: '["amd64","arm64"]' + rpm_images: '["almalinux:9","almalinux:10"]' + deb_images: '["ubuntu:jammy","ubuntu:noble","debian:bullseye","debian:bookworm","debian:trixie"]' + + # ========================================================================= + # Tag parsing → (repo_type, component-version, component-buildnum). On + # workflow_dispatch the `simulate_tag` input substitutes for a real tag + # so branch testing exercises the same parsing logic. + # ========================================================================= + determine-repo-type: + name: Determine repo routing + runs-on: ubuntu-latest + if: github.ref_type == 'tag' || github.event.inputs.simulate_tag != '' + outputs: + repo-type: ${{ steps.route.outputs.repo_type }} + component-version: ${{ steps.parse.outputs.version }} + component-buildnum: ${{ steps.parse.outputs.buildnum }} + effective-tag: ${{ steps.parse.outputs.effective_tag }} + simulated: ${{ steps.parse.outputs.simulated }} + steps: + - name: Checkout pgedge-parse-release-tag + env: + TOKEN: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + run: | + set -euo pipefail + mkdir -p .github/actions + git clone --depth 1 \ + "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-parse-release-tag.git" \ + ".github/actions/pgedge-parse-release-tag" + + - name: Parse release tag + id: parse + uses: ./.github/actions/pgedge-parse-release-tag + with: + simulate_tag: ${{ github.event.inputs.simulate_tag }} + + - name: Route suffix → repo_type + id: route + env: + # Both passed via env: so a hostile tag value can't be + # template-substituted into the shell body. + SUFFIX: ${{ steps.parse.outputs.suffix }} + EFFECTIVE_TAG: ${{ steps.parse.outputs.effective_tag }} + run: | + set -euo pipefail + # Routing is policy and stays per-repo — different + # components may legitimately route differently. Adjust + # this case statement to change pgedge-docloader's + # mapping; other consumers of pgedge-parse-release-tag + # are unaffected. + case "$SUFFIX" in + test*|dev*) repo_type=daily ;; + rc*|beta*|alpha*|"") repo_type=staging ;; + *) + echo "::error::Unsupported suffix '$SUFFIX' (effective tag: $EFFECTIVE_TAG)" + exit 1 + ;; + esac + echo "repo_type=$repo_type" >> "$GITHUB_OUTPUT" + echo "Routed suffix '$SUFFIX' → repo_type=$repo_type" + + # ========================================================================= + # GoReleaser — builds the Go binaries and publishes the GitHub release + # with all tarballs. Tag-only: branch tests (simulate_tag) never publish + # a release. The RPM/DEB build cells depend on this job because their + # build-{rpm,deb}.sh wget the tarballs from the release URL. + # ========================================================================= + goreleaser: + name: Build (GoReleaser) + # Runs on a real tag AND under simulate_tag branch testing. On a real + # tag it publishes the GitHub release; on simulate_tag it builds the + # tarballs in --snapshot mode (publishes nothing). Either way it uploads + # the tarballs as `release-artifacts`, which the packaging jobs stage — + # so packaging no longer needs a pre-existing release, and pushes stay + # tag-only (see push-dnf/push-apt). + if: github.ref_type == 'tag' || github.event.inputs.simulate_tag != '' + # GoReleaser pushes the release with GITHUB_TOKEN on a real tag — opt + # back into write here (workflow default is read). + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + with: + go-version: '1.25' + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 + with: + distribution: goreleaser + version: latest + # --snapshot on a non-tag (simulate_tag) dispatch builds the + # tarballs without publishing a GitHub release. + args: release --clean ${{ (github.event_name == 'workflow_dispatch' && github.ref_type != 'tag') && '--snapshot' || '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Use the simulated tag (if any) so archive names carry the + # right version even in snapshot mode. + GORELEASER_CURRENT_TAG: ${{ github.event.inputs.simulate_tag || github.ref_name }} + + - name: Upload release artifacts (for packaging) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: release-artifacts + path: dist/*.tar.gz + retention-days: 1 + + # ========================================================================= + # RPM build matrix — one cell per (EL major, arch). Each cell downloads + # the arch-specific source tarball from the GitHub release (created by the + # goreleaser job) and produces arch-specific pgedge-docloader RPMs. + # ========================================================================= + package-rpm: + name: Package RPM (${{ matrix.image }} ${{ matrix.arch }}) + needs: [detect-matrix, determine-repo-type, goreleaser] + # The goreleaser job runs on both real tags and simulate_tag (snapshot) + # and uploads the tarballs this job stages, so require its success. + # Only the push jobs are tag-gated, so simulate builds but never pushes. + if: | + always() && + needs.determine-repo-type.result == 'success' && + needs.detect-matrix.result == 'success' && + needs.goreleaser.result == 'success' && + needs.detect-matrix.outputs.has_rpm == 'true' + # arm64 cells run on native arm64 runners to avoid QEMU emulation + # (emulated glibc ldconfig segfaults during build-env setup); amd64 + # stays on the standard x86_64 runner. + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.detect-matrix.outputs.rpm_matrix) }} + env: + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + COMPONENT_VERSION: ${{ needs.determine-repo-type.outputs.component-version }} + COMPONENT_BUILDNUM: ${{ needs.determine-repo-type.outputs.component-buildnum }} + COMPONENT_BRANCH: ${{ needs.determine-repo-type.outputs.effective-tag }} + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Derive os_version from image + id: cell + env: + IMAGE: ${{ matrix.image }} + run: | + set -euo pipefail + # pgedge-detect-build-matrix emits cells with `image` like + # 'almalinux:9'; we still want the bare major version for + # artifact paths and the upload-artifact name. + os_version="${IMAGE##*:}" + echo "os_version=${os_version}" >> "$GITHUB_OUTPUT" + + - name: Download release artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: release-artifacts + path: go-dist + + - name: Stage release-artifacts/ for the build container + run: | + set -euo pipefail + # build-rpm.sh prefers ./release-artifacts/docloader.tar.gz + # over wget'ing the GitHub release, so simulate_tag builds work + # without a published release. Pick this cell's arch tarball. + case "${{ matrix.arch }}" in + amd64) tok=x86_64 ;; + arm64) tok=arm64 ;; + *) echo "::error::unexpected arch ${{ matrix.arch }}"; exit 1 ;; + esac + mkdir -p release-artifacts + cp go-dist/pgedge-docloader_*_Linux_${tok}.tar.gz release-artifacts/docloader.tar.gz + # LICENCE.md isn't in the GoReleaser archive (its files glob is + # LICENSE*), so stage the repo copy for the build scripts. + cp LICENCE.md release-artifacts/LICENCE.md + ls -la release-artifacts/ + rm -rf go-dist + + - name: Checkout pgEdge action repos + env: + TOKEN: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + run: | + set -euo pipefail + mkdir -p .github/actions + git clone --depth 1 \ + "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-builder-action.git" \ + ".github/actions/pgedge-builder-action" + + - name: Build RPM + uses: ./.github/actions/pgedge-builder-action + with: + image: ${{ matrix.image }} + arch: ${{ matrix.arch }} + component_name: pkg + component_branch: ${{ env.COMPONENT_BRANCH }} + component_version: ${{ env.COMPONENT_VERSION }} + component_buildnum: ${{ env.COMPONENT_BUILDNUM }} + repo_type: ${{ env.REPO_TYPE }} + gpg_private_key: ${{ secrets.GPG_FIPS_RPM_PRIVATE_KEY }} + gpg_public_key: ${{ secrets.GPG_FIPS_RPM_PUBLIC_KEY }} + pgedge_builder_token: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + + - name: Upload RPM cell artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: rpm__el-${{ steps.cell.outputs.os_version }}__${{ matrix.arch }} + path: output/el${{ steps.cell.outputs.os_version }}-${{ matrix.arch }}/ + retention-days: 7 + + # ========================================================================= + # DNF push — single job batches all (EL major, arch) cells per family + # into one pgedge-dnf-repo-builder invocation (multi-target branch). + # ========================================================================= + push-dnf: + name: Push DNF (all EL majors × archs, batched) + # Strictly tag-only — branch tests (simulate_tag) never push to live repos. + # force_push lets a maintainer publish from a partial build (some + # cells failed). Without it, any failed cell skips the whole push. + if: | + always() && github.ref_type == 'tag' && + (needs.package-rpm.result == 'success' || + (github.event.inputs.force_push == 'true' && needs.package-rpm.result != 'cancelled')) + needs: [detect-matrix, determine-repo-type, package-rpm] + runs-on: ubuntu-latest + concurrency: + group: dnf-push-${{ needs.determine-repo-type.outputs.repo-type }} + cancel-in-progress: false + # Timestamp is sourced from detect-matrix so dnf_ts and apt_ts are + # guaranteed distinct (the action sleeps 20s between them). If + # push-dnf generated its own at organize-time it could collide with + # push-apt's when both started in the same second. + outputs: + timestamp: ${{ needs.detect-matrix.outputs.dnf_ts }} env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + COMPONENT_VERSION: ${{ needs.determine-repo-type.outputs.component-version }} + COMPONENT_NAME: ${{ github.event.repository.name }} + RPM_MATRIX: ${{ needs.detect-matrix.outputs.rpm_matrix }} + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Download all RPM cell artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: rpm__el-* + path: rpm-artifacts + merge-multiple: false + + - name: Organize RPMs by EL major (per arch) + run: | + set -euo pipefail + # Iterate (os_version, arch) derived from the detect-matrix + # rpm_matrix cells (image = 'almalinux:9' → os_version=9). + # All archs for a given OS land in one directory; the dnf + # repo builder writes them under the os_version's bucket + # prefix, arch dimension preserved inside each RPM filename. + while IFS=$'\t' read -r os_version arch; do + dest="rpms-el${os_version}" + mkdir -p "${dest}" + src_dir="rpm-artifacts/rpm__el-${os_version}__${arch}" + if [ -d "${src_dir}" ]; then + find "${src_dir}" -type f -name '*.rpm' -exec mv -v {} "${dest}/" \; + fi + done < <(echo "$RPM_MATRIX" | jq -r '.include[] | [(.image | split(":")[1]), .arch] | @tsv') + + # Per-OS summary + for os in $(echo "$RPM_MATRIX" | jq -r '[.include[].image | split(":")[1]] | unique | .[]'); do + count=$(find "rpms-el${os}" -maxdepth 1 -name '*.rpm' 2>/dev/null | wc -l) + echo "EL${os}: ${count} RPMs" + done + + rm -rf rpm-artifacts + + - name: Build targets JSON for push + backup + id: targets + run: | + set -euo pipefail + os_versions=$(echo "$RPM_MATRIX" \ + | jq -c '[.include[].image | split(":")[1]] | unique') + push=$(echo "$os_versions" \ + | jq -c 'map({"os-version": ., "rpm-dir": ("rpms-el" + .)})') + backup=$(echo "$os_versions" \ + | jq -c 'map({"os-version": ., "output-dir": ("rpms-el" + .)})') + echo "push=${push}" >> "$GITHUB_OUTPUT" + echo "backup=${backup}" >> "$GITHUB_OUTPUT" + + - name: Checkout pgEdge action repos @multi-target + env: + TOKEN: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + run: | + set -euo pipefail + mkdir -p .github/actions + for repo in pgedge-dnf-repo-builder pgedge-backup-artifacts; do + git clone --depth 1 --branch multi-target \ + "https://x-access-token:${TOKEN}@github.com/pgEdge/${repo}.git" \ + ".github/actions/${repo}" + done + + - name: Push RPMs to yum repo (all EL majors batched) + uses: ./.github/actions/pgedge-dnf-repo-builder + with: + s3-bucket: ${{ secrets.S3_BUCKET_NAME }} + cf-distribution: ${{ secrets.CLOUDFRONT_DISTRIBUTION }} + repo-type: ${{ env.REPO_TYPE }} + targets: ${{ steps.targets.outputs.push }} + # Cross-repo push lock scoped to (family, repo_type). + lock-bucket: ${{ secrets.S3_BACKUP_BUCKET_NAME }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Back up RPMs to S3 (all EL majors batched) + uses: ./.github/actions/pgedge-backup-artifacts + with: + s3-backup-bucket: ${{ secrets.S3_BACKUP_BUCKET_NAME }} + repo-type: ${{ env.REPO_TYPE }} + component-name: ${{ env.COMPONENT_NAME }} + component-version: ${{ env.COMPONENT_VERSION }} + timestamp: ${{ needs.detect-matrix.outputs.dnf_ts }} + targets: ${{ steps.targets.outputs.backup }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + # ========================================================================= + # DEB build matrix — one cell per (distro, arch). Each cell downloads the + # arch-specific source tarball from the GitHub release (created by the + # goreleaser job) and produces arch-specific pgedge-docloader DEBs. + # ========================================================================= + package-deb: + name: Package DEB (${{ matrix.image }} ${{ matrix.arch }}) + needs: [detect-matrix, determine-repo-type, goreleaser] + # See package-rpm for the rationale on these gates; same shape here. + if: | + always() && + needs.determine-repo-type.result == 'success' && + needs.detect-matrix.result == 'success' && + needs.goreleaser.result == 'success' && + needs.detect-matrix.outputs.has_deb == 'true' + # arm64 cells run on native arm64 runners to avoid QEMU emulation + # (emulated glibc ldconfig segfaults during build-env setup); amd64 + # stays on the standard x86_64 runner. + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.detect-matrix.outputs.deb_matrix) }} + env: + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + COMPONENT_VERSION: ${{ needs.determine-repo-type.outputs.component-version }} + COMPONENT_BUILDNUM: ${{ needs.determine-repo-type.outputs.component-buildnum }} + COMPONENT_BRANCH: ${{ needs.determine-repo-type.outputs.effective-tag }} + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Derive distro from image + id: cell + env: + IMAGE: ${{ matrix.image }} + run: | + set -euo pipefail + # pgedge-detect-build-matrix emits cells with `image` like + # 'ubuntu:jammy' or 'debian:bookworm'; we still want the + # bare distro name for artifact paths / upload-artifact. + distro="${IMAGE##*:}" + echo "distro=${distro}" >> "$GITHUB_OUTPUT" + + - name: Download release artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: release-artifacts + path: go-dist + + - name: Stage release-artifacts/ for the build container + run: | + set -euo pipefail + # build-deb.sh prefers ./release-artifacts/docloader.tar.gz + # over wget'ing the GitHub release, so simulate_tag builds work + # without a published release. Pick this cell's arch tarball. + case "${{ matrix.arch }}" in + amd64) tok=x86_64 ;; + arm64) tok=arm64 ;; + *) echo "::error::unexpected arch ${{ matrix.arch }}"; exit 1 ;; + esac + mkdir -p release-artifacts + cp go-dist/pgedge-docloader_*_Linux_${tok}.tar.gz release-artifacts/docloader.tar.gz + # LICENCE.md isn't in the GoReleaser archive (its files glob is + # LICENSE*), so stage the repo copy for the build scripts. + cp LICENCE.md release-artifacts/LICENCE.md + ls -la release-artifacts/ + rm -rf go-dist + + - name: Checkout pgEdge action repos + env: + TOKEN: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + run: | + set -euo pipefail + mkdir -p .github/actions + git clone --depth 1 \ + "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-builder-action.git" \ + ".github/actions/pgedge-builder-action" + + - name: Build DEB + uses: ./.github/actions/pgedge-builder-action + with: + image: ${{ matrix.image }} + arch: ${{ matrix.arch }} + component_name: pkg + component_branch: ${{ env.COMPONENT_BRANCH }} + component_version: ${{ env.COMPONENT_VERSION }} + component_buildnum: ${{ env.COMPONENT_BUILDNUM }} + repo_type: ${{ env.REPO_TYPE }} + gpg_private_key: ${{ secrets.GPG_FIPS_DEB_PRIVATE_KEY }} + gpg_public_key: ${{ secrets.GPG_FIPS_DEB_PUBLIC_KEY }} + pgedge_builder_token: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + + - name: Upload DEB cell artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: deb__${{ steps.cell.outputs.distro }}__${{ matrix.arch }} + path: output/${{ steps.cell.outputs.distro }}-${{ matrix.arch }}/ + retention-days: 7 + + # ========================================================================= + # APT push — single job batches all (distro, arch) cells per family + # into one pgedge-apt-repo-builder invocation. + # ========================================================================= + push-apt: + name: Push APT (all distros × archs, batched) + if: | + always() && github.ref_type == 'tag' && + (needs.package-deb.result == 'success' || + (github.event.inputs.force_push == 'true' && needs.package-deb.result != 'cancelled')) + needs: [detect-matrix, determine-repo-type, package-deb] + runs-on: ubuntu-latest + concurrency: + group: apt-push-${{ needs.determine-repo-type.outputs.repo-type }} + cancel-in-progress: false + # Sourced from detect-matrix — see push-dnf for the rationale on + # why this isn't generated at organize-time. + outputs: + timestamp: ${{ needs.detect-matrix.outputs.apt_ts }} + env: + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + COMPONENT_VERSION: ${{ needs.determine-repo-type.outputs.component-version }} + COMPONENT_NAME: ${{ github.event.repository.name }} + DEB_MATRIX: ${{ needs.detect-matrix.outputs.deb_matrix }} + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Download all DEB cell artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + pattern: deb__* + path: deb-artifacts + merge-multiple: false + + - name: Organize DEBs by distro (per arch) + run: | + set -euo pipefail + # Per-distro single directory; reprepro keys on Architecture: + # within each .deb so we don't need per-arch subdirs. + # distro is derived from the matrix cell's `image` field + # (e.g. 'ubuntu:jammy' → distro=jammy). + while IFS=$'\t' read -r distro arch; do + dest="debs-${distro}" + mkdir -p "${dest}" + src_dir="deb-artifacts/deb__${distro}__${arch}" + if [ -d "${src_dir}" ]; then + find "${src_dir}" -type f -name '*.deb' -exec mv -v {} "${dest}/" \; + fi + done < <(echo "$DEB_MATRIX" | jq -r '.include[] | [(.image | split(":")[1]), .arch] | @tsv') + + for distro in $(echo "$DEB_MATRIX" | jq -r '[.include[].image | split(":")[1]] | unique | .[]'); do + count=$(find "debs-${distro}" -maxdepth 1 -name '*.deb' 2>/dev/null | wc -l) + echo "${distro}: ${count} DEBs" + done + + rm -rf deb-artifacts + + - name: Build targets JSON for push + backup + id: targets + run: | + set -euo pipefail + # Unique images (full 'family:distro' pairs), arch dim collapsed. + unique_images=$(echo "$DEB_MATRIX" \ + | jq -c '[.include[].image] | unique') + push=$(echo "$unique_images" \ + | jq -c 'map({"os-name": ., "deb-dir": ("debs-" + (. | split(":")[1]))})') + backup=$(echo "$unique_images" \ + | jq -c 'map({"os-version": (. | split(":")[1]), "output-dir": ("debs-" + (. | split(":")[1]))})') + echo "push=${push}" >> "$GITHUB_OUTPUT" + echo "backup=${backup}" >> "$GITHUB_OUTPUT" + + - name: Checkout pgEdge action repos @multi-target + env: + TOKEN: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + run: | + set -euo pipefail + mkdir -p .github/actions + for repo in pgedge-apt-repo-builder pgedge-backup-artifacts; do + git clone --depth 1 --branch multi-target \ + "https://x-access-token:${TOKEN}@github.com/pgEdge/${repo}.git" \ + ".github/actions/${repo}" + done + + - name: Push DEBs to apt repo (all distros batched) + uses: ./.github/actions/pgedge-apt-repo-builder + with: + s3-bucket: ${{ secrets.APT_S3_BUCKET_NAME }} + cf-distribution: ${{ secrets.APT_CLOUDFRONT_DISTRIBUTION }} + repo-type: ${{ env.REPO_TYPE }} + targets: ${{ steps.targets.outputs.push }} + # Cross-repo push lock scoped to (family, repo_type). + lock-bucket: ${{ secrets.S3_BACKUP_BUCKET_NAME }} + gpg-private-key: ${{ secrets.GPG_FIPS_DEB_PRIVATE_KEY }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Back up DEBs to S3 (all distros batched) + uses: ./.github/actions/pgedge-backup-artifacts + with: + s3-backup-bucket: ${{ secrets.S3_BACKUP_BUCKET_NAME }} + repo-type: ${{ env.REPO_TYPE }} + component-name: ${{ env.COMPONENT_NAME }} + component-version: ${{ env.COMPONENT_VERSION }} + timestamp: ${{ needs.detect-matrix.outputs.apt_ts }} + targets: ${{ steps.targets.outputs.backup }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + # ========================================================================= + # Aggregated manifest — written to _runs/ (legacy audit path) and, when + # at least one push family succeeded, to _pending/ for qa-certify and + # promote.yml. cells[] is gated on PUSH_*_RESULT == 'success' so a + # failed-but-tagged run never enqueues a no-op QA candidate. + # ========================================================================= + publish-manifest: + name: Publish aggregated backup manifest + needs: [detect-matrix, determine-repo-type, push-dnf, push-apt] + if: always() && needs.determine-repo-type.result == 'success' + runs-on: ubuntu-latest + env: + COMPONENT_NAME: ${{ github.event.repository.name }} + steps: + - name: Install jq + awscli + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y python3-pip jq + pip3 install --user awscli + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Build manifest (with cells[] for qa-certify/promote) + id: build_manifest + env: + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + CV: ${{ needs.determine-repo-type.outputs.component-version }} + CBN: ${{ needs.determine-repo-type.outputs.component-buildnum }} + EFFECTIVE_TAG: ${{ needs.determine-repo-type.outputs.effective-tag }} + SIMULATED: ${{ needs.determine-repo-type.outputs.simulated }} + PUSH_DNF_RESULT: ${{ needs.push-dnf.result }} + PUSH_APT_RESULT: ${{ needs.push-apt.result }} + DNF_TS: ${{ needs.push-dnf.outputs.timestamp }} + APT_TS: ${{ needs.push-apt.outputs.timestamp }} + RPM_MATRIX: ${{ needs.detect-matrix.outputs.rpm_matrix }} + DEB_MATRIX: ${{ needs.detect-matrix.outputs.deb_matrix }} + run: | + set -euo pipefail + mkdir -p aggregated + + # Gate on push job result, not timestamp presence: the + # detect-matrix job emits dnf_ts / apt_ts at workflow + # start, before push-dnf / push-apt have a chance to fail. + # Emitting cells[] or backups[] for failed pushes would + # lie to downstream qa-certify / promote consumers. + dnf_backups='[]' + cells='[]' + if [ "${PUSH_DNF_RESULT}" = "success" ] && [ -n "${DNF_TS}" ]; then + # One backup path per OS major (arch dim is inside the + # RPM filename). But cells[] keeps the arch dim so + # promote.yml can verify each (os, arch) was published. + for os in $(echo "$RPM_MATRIX" | jq -r '[.include[].image | split(":")[1]] | unique | .[]'); do + path="${REPO_TYPE}/${COMPONENT_NAME}/${CV}/${DNF_TS}/dnf/${os}/" + dnf_backups="$(jq -c --arg p "${path}" '. + [$p]' <<<"${dnf_backups}")" + done + while IFS=$'\t' read -r image os arch; do + path="${REPO_TYPE}/${COMPONENT_NAME}/${CV}/${DNF_TS}/dnf/${os}/" + cell="$(jq -nc \ + --arg fam rpm --arg comp "$COMPONENT_NAME" --arg cv "$CV" \ + --arg img "${image}" --arg ver "${os}" \ + --arg arch "${arch}" \ + --arg bp "${path}" --arg ts "$DNF_TS" --arg rt "$REPO_TYPE" \ + '{family:$fam, component:$comp, component_version:$cv, + image:$img, arch:$arch, os_version:$ver, + backup_path:$bp, timestamp:$ts, repo_type:$rt}')" + cells="$(jq -c --argjson c "${cell}" '. + [$c]' <<<"${cells}")" + done < <(echo "$RPM_MATRIX" | jq -r '.include[] | [.image, (.image | split(":")[1]), .arch] | @tsv') + fi + + apt_backups='[]' + if [ "${PUSH_APT_RESULT}" = "success" ] && [ -n "${APT_TS}" ]; then + for distro in $(echo "$DEB_MATRIX" | jq -r '[.include[].image | split(":")[1]] | unique | .[]'); do + path="${REPO_TYPE}/${COMPONENT_NAME}/${CV}/${APT_TS}/apt/${distro}/" + apt_backups="$(jq -c --arg p "${path}" '. + [$p]' <<<"${apt_backups}")" + done + while IFS=$'\t' read -r image distro arch; do + path="${REPO_TYPE}/${COMPONENT_NAME}/${CV}/${APT_TS}/apt/${distro}/" + cell="$(jq -nc \ + --arg fam deb --arg comp "$COMPONENT_NAME" --arg cv "$CV" \ + --arg img "${image}" --arg distro "${distro}" \ + --arg arch "${arch}" \ + --arg bp "${path}" --arg ts "$APT_TS" --arg rt "$REPO_TYPE" \ + '{family:$fam, component:$comp, component_version:$cv, + image:$img, arch:$arch, distro:$distro, + backup_path:$bp, timestamp:$ts, repo_type:$rt}')" + cells="$(jq -c --argjson c "${cell}" '. + [$c]' <<<"${cells}")" + done < <(echo "$DEB_MATRIX" | jq -r '.include[] | [.image, (.image | split(":")[1]), .arch] | @tsv') + fi + + jq -n \ + --arg run_id "${GITHUB_RUN_ID}" \ + --arg run_attempt "${GITHUB_RUN_ATTEMPT}" \ + --arg repo "${GITHUB_REPOSITORY}" \ + --arg tag "${EFFECTIVE_TAG}" \ + --argjson simulated "${SIMULATED}" \ + --arg commit_sha "${GITHUB_SHA}" \ + --arg component_name "${COMPONENT_NAME}" \ + --arg component_version "${CV}" \ + --arg component_buildnum "${CBN}" \ + --arg repo_type "${REPO_TYPE}" \ + --arg dnf_timestamp "${DNF_TS}" \ + --arg apt_timestamp "${APT_TS}" \ + --arg push_dnf_outcome "${PUSH_DNF_RESULT}" \ + --arg push_apt_outcome "${PUSH_APT_RESULT}" \ + --argjson dnf_backups "${dnf_backups}" \ + --argjson apt_backups "${apt_backups}" \ + --argjson cells "${cells}" \ + '{run_id:$run_id, run_attempt:$run_attempt, + repo:$repo, tag:$tag, simulated:$simulated, + commit_sha:$commit_sha, + component_name:$component_name, + component_version:$component_version, + component_buildnum:$component_buildnum, + repo_type:$repo_type, + inputs:{component:$component_name, + component_version:$component_version, + component_buildnum:$component_buildnum}, + timestamps:{dnf:$dnf_timestamp, apt:$apt_timestamp}, + push_results:{dnf:$push_dnf_outcome, apt:$push_apt_outcome}, + backups:{dnf:$dnf_backups, apt:$apt_backups}, + cells:$cells}' \ + > aggregated/manifest.json + cat aggregated/manifest.json + + { + echo "manifest_json<<__EOF__" + jq -c '.' aggregated/manifest.json + echo "__EOF__" + } >> "$GITHUB_OUTPUT" + + - name: Write step summary + run: | + set -euo pipefail + { + echo "## Run \`${GITHUB_RUN_ID}\` attempt ${GITHUB_RUN_ATTEMPT}" + echo "" + jq -r ' + "**Tag:** `\(.tag)`" + (if .simulated then " *(simulated)*" else "" end) + " ", + "**Repo type:** `\(.repo_type)` ", + "**Component:** `\(.component_name) \(.component_version)-\(.component_buildnum)` ", + "**Push results:** dnf=`\(.push_results.dnf)`, apt=`\(.push_results.apt)` ", + "**Timestamps:** dnf=`\(.timestamps.dnf // "—")`, apt=`\(.timestamps.apt // "—")`" + ' aggregated/manifest.json + echo "" + echo "### Backup paths (DNF)" + jq -r '.backups.dnf[]? | "- `" + . + "`"' aggregated/manifest.json + if [ "$(jq -r '.backups.dnf | length' aggregated/manifest.json)" = "0" ]; then + echo "_(none — push-dnf skipped or no packages)_" + fi + echo "" + echo "### Backup paths (APT)" + jq -r '.backups.apt[]? | "- `" + . + "`"' aggregated/manifest.json + if [ "$(jq -r '.backups.apt | length' aggregated/manifest.json)" = "0" ]; then + echo "_(none — push-apt skipped or no packages)_" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload aggregated manifest to S3 (legacy _runs/ path) + if: github.ref_type == 'tag' + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + S3_BACKUP_BUCKET: ${{ secrets.S3_BACKUP_BUCKET_NAME }} + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + CV: ${{ needs.determine-repo-type.outputs.component-version }} + run: | + set -euo pipefail + key="_runs/${REPO_TYPE}/${COMPONENT_NAME}/${CV}/${GITHUB_RUN_ID}-r${GITHUB_RUN_ATTEMPT}.json" + aws s3 cp aggregated/manifest.json "s3://${S3_BACKUP_BUCKET}/${key}" \ + | sed "s|${S3_BACKUP_BUCKET}||g" + echo "Aggregated manifest uploaded (key=${key})" + + # pgedge-build-publisher now does both jobs in one step: + # * uploads the manifest to _pending/ for qa-certify (gated + # via skip_publish — true when nothing publishable) + # * sends the Slack notification (always, as long as the + # webhook secret is set) + # The old separate notify-slack job is gone — its Block Kit + # construction lives below as `compose_slack`, fed into the + # publisher's `fields_json` / `result_fields_json` / + # `code_sections_json` inputs. + - name: Checkout pgedge-build-publisher + env: + TOKEN: ${{ secrets.PGEDGE_BUILDER_TOKEN }} + run: | + set -euo pipefail + mkdir -p .github/actions + git clone --depth 1 \ + "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-build-publisher.git" \ + ".github/actions/pgedge-build-publisher" + + - name: Compose Slack fields + id: compose_slack + env: + COMPONENT_NAME: ${{ github.event.repository.name }} + CV: ${{ needs.determine-repo-type.outputs.component-version }} + CBN: ${{ needs.determine-repo-type.outputs.component-buildnum }} + EFFECTIVE_TAG: ${{ needs.determine-repo-type.outputs.effective-tag }} + SIMULATED: ${{ needs.determine-repo-type.outputs.simulated }} + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + PUSH_DNF_RESULT: ${{ needs.push-dnf.result }} + PUSH_APT_RESULT: ${{ needs.push-apt.result }} + # has_rpm / has_deb let the status logic distinguish + # "family was skipped because it doesn't exist" from + # "family was skipped because something upstream failed". + HAS_RPM: ${{ needs.detect-matrix.outputs.has_rpm }} + HAS_DEB: ${{ needs.detect-matrix.outputs.has_deb }} + ACTOR: ${{ github.actor }} + REPO_URL: ${{ github.server_url }}/${{ github.repository }} + run: | + set -euo pipefail + + unix_ts="$(date -u +%s)" + fallback_ts="$(date -u +'%Y-%m-%d %H:%M UTC')" + + # ---- subject (clickable tag inside outer-bold) --------- + # Publisher wraps subject with *...*; link tokens still + # render correctly inside the outer bold. For simulated + # runs we omit the link because the tag doesn't exist. + if [ "${SIMULATED}" = "true" ]; then + subject="${COMPONENT_NAME} ${EFFECTIVE_TAG} (simulated)" + else + subject="${COMPONENT_NAME} <${REPO_URL}/releases/tag/${EFFECTIVE_TAG}|${EFFECTIVE_TAG}>" + fi + + # ---- status (family-aware) ----------------------------- + # Count outcomes only for families this component actually + # builds (has_rpm / has_deb). A skipped push on a family + # the component doesn't ship is not a downgrade. We pass + # the resulting status to the publisher explicitly via + # its `status:` input — the publisher's auto-compute path + # requires build_*_result inputs that this workflow + # doesn't track separately. + if [ "${SIMULATED}" = "true" ]; then + status="Simulated" + else + appl_success=0; appl_failed=0; appl_skipped=0 + if [ "${HAS_RPM:-true}" = "true" ]; then + case "${PUSH_DNF_RESULT}" in + success) appl_success=$((appl_success+1)) ;; + skipped) appl_skipped=$((appl_skipped+1)) ;; + *) appl_failed=$((appl_failed+1)) ;; + esac + fi + if [ "${HAS_DEB:-true}" = "true" ]; then + case "${PUSH_APT_RESULT}" in + success) appl_success=$((appl_success+1)) ;; + skipped) appl_skipped=$((appl_skipped+1)) ;; + *) appl_failed=$((appl_failed+1)) ;; + esac + fi + total=$((appl_success + appl_failed + appl_skipped)) + if [ "$total" -eq 0 ]; then + status="Skipped" + elif [ "$appl_failed" -eq 0 ] && [ "$appl_skipped" -eq 0 ]; then + status="Success" + elif [ "$appl_success" -gt 0 ]; then + status="Partial" + elif [ "$appl_skipped" -eq "$total" ]; then + status="Skipped" + else + status="Failed" + fi + fi + + # ---- archs from the manifest's cells[] ----------------- + # cells[] is populated only for cells whose push family + # succeeded (manifest builder gates on PUSH_*_RESULT), so + # this reflects what actually shipped. Falls back to "—" + # for failed / simulated runs. + archs="—" + if [ -f aggregated/manifest.json ]; then + a=$(jq -r '(.cells // []) | map(.arch) | unique | join(", ")' aggregated/manifest.json) + [ -n "$a" ] && archs="$a" + fi + + fields=$(jq -nc \ + --arg channel "${REPO_TYPE}" \ + --arg version "${CV}" \ + --arg buildnum "${CBN}" \ + --arg archs "${archs}" \ + --arg actor "${ACTOR}" \ + --arg unix_ts "${unix_ts}" \ + --arg fallback_ts "${fallback_ts}" \ + '[ + {title:"Channel", value:("`" + $channel + "`")}, + {title:"Version", value:("`" + $version + "`")}, + {title:"Build #", value:("`" + $buildnum + "`")}, + {title:"Archs", value:("`" + $archs + "`")}, + {title:"Triggered by", value:$actor}, + {title:"When", value:("")} + ]') + + result_fields=$(jq -nc \ + --arg dnf "${PUSH_DNF_RESULT}" \ + --arg apt "${PUSH_APT_RESULT}" \ + '[ + {title:"Push DNF", value:("`" + $dnf + "`")}, + {title:"Push APT", value:("`" + $apt + "`")} + ]') + + # Full backup paths in code blocks (operationally useful + # for diagnostics and audit). Publisher elides empty + # sections automatically when push didn't run. + dnf_paths=$(jq -c '.backups.dnf // []' aggregated/manifest.json) + apt_paths=$(jq -c '.backups.apt // []' aggregated/manifest.json) + code_sections=$(jq -nc \ + --argjson dnf "${dnf_paths}" \ + --argjson apt "${apt_paths}" \ + '[ + {title:"DNF backup paths", lines:$dnf}, + {title:"APT backup paths", lines:$apt} + ]') + + { + echo "status=${status}" + echo "subject<<__SUB_EOF__" + echo "${subject}" + echo "__SUB_EOF__" + echo "fields<<__F_EOF__" + echo "${fields}" + echo "__F_EOF__" + echo "result_fields<<__RF_EOF__" + echo "${result_fields}" + echo "__RF_EOF__" + echo "code_sections<<__CS_EOF__" + echo "${code_sections}" + echo "__CS_EOF__" + } >> "$GITHUB_OUTPUT" + + - name: Publish manifest + Slack notification + # No outer `if:` — runs on every publish-manifest invocation + # (i.e., whenever determine-repo-type succeeded). The action + # internally decides whether to upload the manifest via the + # `skip_publish` flag, and the Slack step fires whenever + # `slack_webhook` is set. Simulated runs ship "Simulated" + # status; real-tag-with-failed-pushes ship Failed / Partial + # with no _pending/ enqueue. + uses: ./.github/actions/pgedge-build-publisher + with: + manifest_json: ${{ steps.build_manifest.outputs.manifest_json }} + s3_backup_bucket: ${{ secrets.S3_BACKUP_BUCKET_NAME }} + repo_type: ${{ needs.determine-repo-type.outputs.repo-type }} + component_name: ${{ env.COMPONENT_NAME }} + # Skip the _pending/ upload when there's nothing to QA: + # simulate_tag dispatches or both-pushes-failed runs. + skip_publish: ${{ needs.determine-repo-type.outputs.simulated == 'true' || (needs.push-dnf.result != 'success' && needs.push-apt.result != 'success') }} + # Status computed in compose_slack from push results + + # the simulated flag (mirrors the old notify-slack logic). + # We pass it explicitly because the publisher's auto- + # compute requires has_rpm/has_deb/build_*_result inputs + # that this workflow doesn't surface. + status: ${{ steps.compose_slack.outputs.status }} + push_dnf_result: ${{ needs.push-dnf.result }} + push_apt_result: ${{ needs.push-apt.result }} + slack_webhook: ${{ secrets.SLACK_CHANNEL_URL }} + subject: ${{ steps.compose_slack.outputs.subject }} + fields_json: ${{ steps.compose_slack.outputs.fields }} + result_fields_json: ${{ steps.compose_slack.outputs.result_fields }} + code_sections_json: ${{ steps.compose_slack.outputs.code_sections }} + tagline: "pgEdge release pipeline" + aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Upload aggregated manifest as workflow artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: aggregated-manifest + path: aggregated/manifest.json + retention-days: 90 diff --git a/common/build.sh b/common/build.sh new file mode 100644 index 0000000..42ea7e3 --- /dev/null +++ b/common/build.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Entry point for the pgEdge builder-action. Invoked as: +# common/build.sh +# where is a packaging dir under the repo root that holds a +# common.sh + build-rpm.sh / build-deb.sh (e.g. "packaging/pg_duckdb"). +COMPONENT_NAME=$1 +source "$(dirname "$0")/../${COMPONENT_NAME}/common.sh" + +# common-functions.sh is a committed copy of the shared pgEdge build helpers +# (from pgedge-enterprise-packages/common/). The builder-action mounts the repo +# and runs this script but does not supply it, so it must live in the repo — +# same pattern as ai-dba-workbench's packaging/scripts/common-functions.sh. +COMMON_FILE="$(dirname "$0")/common-functions.sh" +if [ -f "$COMMON_FILE" ]; then + source "$COMMON_FILE" +else + echo "Error: $COMMON_FILE not found!" >&2 + exit 1 +fi + +########### +# Main +########### +detect_os_type +prepare +build +post_build diff --git a/common/common-functions.sh b/common/common-functions.sh new file mode 100755 index 0000000..83414fb --- /dev/null +++ b/common/common-functions.sh @@ -0,0 +1,281 @@ +#!/bin/bash + +install_syft(){ + + # Install syft from its pinned release tarball and verify it against the + # published checksums before installing — avoids piping a remote installer + # script straight into a root shell. Override SYFT_VERSION to bump. + SYFT_VERSION="${SYFT_VERSION:-v1.45.1}" + local ver="${SYFT_VERSION#v}" arch + case "$(uname -m)" in + x86_64) arch=amd64 ;; + aarch64) arch=arm64 ;; + *) echo "unsupported arch for syft: $(uname -m)" >&2; return 1 ;; + esac + local tgz="syft_${ver}_linux_${arch}.tar.gz" + local base="https://github.com/anchore/syft/releases/download/v${ver}" + local tmp; tmp="$(mktemp -d)" + echo "Installing syft ${SYFT_VERSION} (${arch})..." + curl -sSfL "${base}/${tgz}" -o "${tmp}/${tgz}" + curl -sSfL "${base}/syft_${ver}_checksums.txt" -o "${tmp}/checksums.txt" + ( cd "${tmp}" && grep " ${tgz}\$" checksums.txt | sha256sum -c - ) + tar -xzf "${tmp}/${tgz}" -C "${tmp}" syft + sudo install -m 0755 "${tmp}/syft" /usr/local/bin/syft + rm -rf "${tmp}" + syft version +} + +setup_dnf_build_env(){ + + echo "Installing required packages..." + dnf groupinstall "Development Tools" -y + dnf install -y rpm-build rpmdevtools yum-utils tar wget git gnupg2 sudo + + echo "📦 Enabling additional repositories..." + dnf install -y epel-release + if [ "$RHEL" = "8" ]; then + dnf config-manager --set-enabled powertools + else + dnf config-manager --set-enabled crb + fi + + echo "Configuring pgEdge repository..." + configure_pgedge_dnf_repo $REPO_TYPE + + echo "Setting up RPM build environment..." + rpmdev-setuptree + + install_syft +} + +setup_apt_build_env(){ + + echo "Installing build tools and dependencies..." + sudo ln -fs /usr/share/zoneinfo/UTC /etc/localtime + + sudo apt-get update + sudo apt-get install -y devscripts build-essential pkg-config fakeroot git curl \ + ca-certificates debhelper dpkg-dev gnupg2 wget sudo lsb-release + + echo "Configuring pgEdge repository..." + configure_pgedge_apt_repo $REPO_TYPE + + install_syft +} + +rename_ddeb_packages(){ + # Rename any *.ddeb (debug-symbol packages) to *.deb. Uses find -print0 so it + # is safe under `set -euo pipefail` when there are NO .ddeb files (no grep + # non-zero exit) and tolerates filenames with spaces. + local build_dir="$1" f + find "$build_dir" -maxdepth 1 -type f -name '*.ddeb' -print0 | while IFS= read -r -d '' f; do + mv -- "$f" "${f%.ddeb}.deb" + done +} + +configure_pgedge_dnf_repo() { + local REPO_TYPE="${1:-daily}" # "daily" or "staging" + + sudo dnf install -y https://dnf.pgedge.com/reporpm/pgedge-release-latest.noarch.rpm + sudo sed -i "s|release|$REPO_TYPE|g" /etc/yum.repos.d/pgedge.repo + + echo "Repo configured at /etc/yum.repos.d/pgedge.repo" +} + +configure_pgedge_apt_repo(){ + local REPO_TYPE="${1:-daily}" # "daily" or "staging" + local REPO_PATH="repodeb" + + curl -sSL https://apt.pgedge.com/${REPO_PATH}/pgedge-release_latest_all.deb -o /tmp/pgedge-release.deb && sudo dpkg -i /tmp/pgedge-release.deb && rm -f /tmp/pgedge-release.deb || true + sed -i "s|release|$REPO_TYPE|g" /etc/apt/sources.list.d/pgedge.sources + apt-get update + + echo "Repo configured at /etc/apt/sources.list.d/pgedge.sources" +} + +detect_os_type(){ + if command -v dnf &>/dev/null || command -v yum &>/dev/null; then + echo "Detected RPM-based system" + source "$(dirname "$0")/../${COMPONENT_NAME}/build-rpm.sh" + elif command -v apt-get &>/dev/null; then + echo "Detected Debian-based system" + source "$(dirname "$0")/../${COMPONENT_NAME}/build-deb.sh" + else + echo "Unsupported platform: No known package manager found" >&2 + exit 1 + fi +} + +import_gpg_keys() { + if ! command -v rpm &>/dev/null || ! command -v gpg &>/dev/null; then + echo "Installing rpm or gpg" + if command -v dnf &>/dev/null; then + sudo dnf install -y rpm gnupg2 + elif command -v apt-get &>/dev/null; then + sudo apt-get install -y rpm gnupg2 + fi + if [ $? -ne 0 ]; then + echo "Error: Failed to install rpm or gnupg2" + return 1 + fi + fi + + PRI_FILE=$(dirname "$0")/public.key + PUB_FILE=$(dirname "$0")/private.key + + GPG_PUBLIC_KEY=$(cat $PRI_FILE) + GPG_PRIVATE_KEY=$(cat $PUB_FILE) + rm -f $PRI_FILE $PUB_FILE + + [ -z "$GPG_PUBLIC_KEY" ] && { echo "Error: GPG_PUBLIC_KEY is unset"; return 1; } + [ -z "$GPG_PRIVATE_KEY" ] && { echo "Error: GPG_PRIVATE_KEY is unset"; return 1; } + + PUBLIC_KEY_FILE=$(mktemp) + echo "$GPG_PUBLIC_KEY" > "$PUBLIC_KEY_FILE" + + gpg --import "$PUBLIC_KEY_FILE" || { + echo "Error: Failed to import public key" + rm -f "$PUBLIC_KEY_FILE" + return 1 + } + + rpm --import "$PUBLIC_KEY_FILE" || { + echo "Error: Failed to import public key to RPM" + rm -f "$PUBLIC_KEY_FILE" + return 1 + } + + PRIVATE_KEY_FILE=$(mktemp) + echo "$GPG_PRIVATE_KEY" > "$PRIVATE_KEY_FILE" + gpg --import "$PRIVATE_KEY_FILE" || { + echo "Error: Failed to import private key" + rm -f "$PRIVATE_KEY_FILE" + rm -f "$PUBLIC_KEY_FILE" + return 1 + } + rm -f "$PRIVATE_KEY_FILE" + rm -f "$PUBLIC_KEY_FILE" + return 0 +} + +sign_rpms() { + # Check if at least one file is provided + if [ $# -eq 0 ]; then + echo "Error: No files provided to sign." + return 1 + fi + + # Check if rpmsign and gpg are installed, install if not + if ! command -v rpmsign &>/dev/null; then + echo "rpmsign not found. Installing rpm-sign" + if command -v sudo &>/dev/null; then + sudo dnf install -y rpm-sign + else + dnf install -y rpm-sign + fi + if [ $? -ne 0 ]; then + echo "Error: Failed to install rpm-sign" + return 1 + fi + fi + + # Get the key ID of the imported private key + KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/{print $5}' | head -n 1) + if [ -z "$KEY_ID" ]; then + echo "Error: No private key found after import." + rm -f "$PRIVATE_KEY_FILE" + rm -rf "$GNUPGHOME" + return 1 + fi + + echo "=======================Signing RPMs=======================" + # Sign each RPM file + for file in "$@"; do + # Ensure the file has /output/ prefix if relative + if [[ ! "$file" = /* ]]; then + file="/output/$file" + fi + + if [ ! -f "$file" ]; then + echo "Error: File '$file' does not exist." + continue + fi + + # Check if the file is an RPM + if ! file "$file" | grep -q "RPM"; then + echo "Error: File '$file' is not an RPM file." + continue + fi + + # Sign the RPM using rpmsign, using passphrase if provided + rpmsign --define "_gpg_name $KEY_ID" --addsign "$file" >/dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo "Successfully signed '$file'." + else + echo "Error: Failed to sign '$file'." + fi + done + echo "=======================Signing Completes==================" + + # Clean up + rm -f "$PRIVATE_KEY_FILE" +} + +validate_signatures() { + + # Check if files are provided + if [ $# -eq 0 ]; then + echo "Error: No files provided to validate." + return 1 + fi + + # Install dependencies + if ! command -v rpm &>/dev/null; then + echo "Installing rpm" + if command -v sudo &>/dev/null; then + sudo dnf install -y rpm + else + dnf install -y rpm + fi + if [ $? -ne 0 ]; then + echo "Error: Failed to install rpm" + return 1 + fi + fi + + # Validate each RPM + local all_valid=0 + echo "=======================Starting validation=======================" + for file in "$@"; do + if [[ ! "$file" = /* ]]; then + file="/output/$file" + fi + + if [ ! -f "$file" ]; then + echo "Error: File '$file' does not exist." + all_valid=1 + continue + fi + + if ! file "$file" | grep -q "RPM"; then + echo "Error: File '$file' is not an RPM file." + all_valid=1 + continue + fi + + CHECKSIG_OUTPUT=$(rpm --checksig "$file" 2>&1) + echo "$CHECKSIG_OUTPUT" + if echo "$CHECKSIG_OUTPUT" | grep -q "digests signatures OK"; then + echo "Signature for '$file' is valid." + else + echo "Error: Signature for '$file' is invalid or missing." + all_valid=1 + fi + done + echo "=======================Validation completes======================" + # Clean up + rm -f "$PUBLIC_KEY_FILE" + + return $all_valid +} diff --git a/pkg/build-deb.sh b/pkg/build-deb.sh new file mode 100755 index 0000000..cc03d85 --- /dev/null +++ b/pkg/build-deb.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Environment variables +BUILD_DIR="/tmp/pg_deb_build" + +CWD="$(pwd)" + +export DEBIAN_FRONTEND=noninteractive +ARCH=$(uname -m) +if [ "$ARCH" = "aarch64" ]; then + ARCH="arm64" +fi + +# Release assets are named with the full tag version (e.g. 1.0.0-beta2). +# DOCLOADER_VERSION may carry a '~beta…' suffix for DEB ordering, so it must +# NOT be used to build the download URL — use the tag instead. +TAG_VERSION="${DOCLOADER_BRANCH#v}" +SRC_DIR="${BUILD_DIR}/pgedge-docloader-${TAG_VERSION}" +# The workflow stages the GoReleaser tarball + LICENCE here; prefer them over +# wget so simulate_tag runs (no published release) still build. +ARTIFACT_DIR="${ARTIFACT_DIR:-${CWD}/release-artifacts}" + +prepare() { + + setup_apt_build_env + + # This function is for debugging purpose if you have your own keys. GH workflow does not need it. + #import_gpg_keys + + echo "Cloning docloader Debian packaging repo..." + rm -rf "$SRC_DIR" + mkdir -p $SRC_DIR + + echo "Fetching docloader source code" + if [ -f "${ARTIFACT_DIR}/docloader.tar.gz" ]; then + echo "Using staged tarball ${ARTIFACT_DIR}/docloader.tar.gz" + cp "${ARTIFACT_DIR}/docloader.tar.gz" "${BUILD_DIR}/docloader.tar.gz" + else + echo "Downloading source tarball from release ${DOCLOADER_BRANCH}" + wget -q "https://github.com/pgEdge/pgedge-docloader/releases/download/${DOCLOADER_BRANCH}/pgedge-docloader_${TAG_VERSION}_Linux_${ARCH}.tar.gz" -O "${BUILD_DIR}/docloader.tar.gz" + fi + tar -C "$SRC_DIR" -xzf "${BUILD_DIR}/docloader.tar.gz" + + echo "Moving Debian packaging into source directory..." + cp -rp "${CWD}/${COMPONENT_NAME}/deb/debian" "$SRC_DIR/" + cp ${COMPONENT_NAME}/common/config.yml "$SRC_DIR/debian/" + + if [ -f "${ARTIFACT_DIR}/LICENCE.md" ]; then + cp "${ARTIFACT_DIR}/LICENCE.md" "$SRC_DIR/" + else + wget -q "https://raw.githubusercontent.com/pgEdge/pgedge-docloader/${DOCLOADER_BRANCH}/LICENCE.md" -O "$SRC_DIR/LICENCE.md" + fi + + echo "Installing build dependencies..." + cd "$SRC_DIR" + sudo apt-get update + sudo apt-get build-dep -y . +} + +build() { + + cd "$SRC_DIR" + echo "Building Debian package..." + DISTRO=$(lsb_release -cs) + rm -f debian/changelog +cat > debian/changelog < $(date -R) +EOF + + dpkg-buildpackage -us -uc -b +} + +post_build() { + echo "Copying .deb packages to output..." + sudo mkdir -p "/output" + # Rename .ddeb files to .deb files + rename_ddeb_packages $BUILD_DIR + sudo cp "$BUILD_DIR"/*.deb "/output" || echo "No .deb packages found." +} + diff --git a/pkg/build-rpm.sh b/pkg/build-rpm.sh new file mode 100755 index 0000000..ecba783 --- /dev/null +++ b/pkg/build-rpm.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -euo pipefail + +RHEL="$(rpm --eval %rhel)" +ARCH=$(uname -m) +if [ "$ARCH" = "aarch64" ]; then + ARCH="arm64" +fi + +# Release assets are named with the full tag version (e.g. 1.0.0-beta2) and +# live under the tag. DOCLOADER_VERSION is the plain RPM version (no +# pre-release suffix), so download by TAG, then stage the tarball under the +# spec's expected basename. Mirrors build-deb.sh; avoids a 404 on +# pre-release tags (e.g. v1.0.0-test1) where v${DOCLOADER_VERSION} wouldn't +# resolve. +TAG_VERSION="${DOCLOADER_BRANCH#v}" +# The workflow stages the GoReleaser tarball + LICENCE here; prefer them over +# wget so simulate_tag runs (no published release) still build. +ARTIFACT_DIR="${ARTIFACT_DIR:-$(pwd)/release-artifacts}" + +prepare() { + setup_dnf_build_env + + echo "Copying packaging files..." + cp ${COMPONENT_NAME}/rpm/docloader.spec ~/rpmbuild/SPECS/ + + echo "Staging source tarball and docs ..." + # Stage under the basename the spec's Source0 expects (docloader_version), + # regardless of the tag's pre-release suffix. Prefer the workflow-staged + # tarball (so simulate_tag builds work) and fall back to the GitHub release. + SRC_TARBALL=~/rpmbuild/SOURCES/pgedge-docloader_${DOCLOADER_VERSION}_Linux_${ARCH}.tar.gz + if [ -f "${ARTIFACT_DIR}/docloader.tar.gz" ]; then + echo "Using staged tarball ${ARTIFACT_DIR}/docloader.tar.gz" + cp "${ARTIFACT_DIR}/docloader.tar.gz" "${SRC_TARBALL}" + else + echo "Downloading source tarball from release ${DOCLOADER_BRANCH}" + wget -q "https://github.com/pgEdge/pgedge-docloader/releases/download/${DOCLOADER_BRANCH}/pgedge-docloader_${TAG_VERSION}_Linux_${ARCH}.tar.gz" -O "${SRC_TARBALL}" + fi + cp ${COMPONENT_NAME}/common/config.yml ~/rpmbuild/SOURCES/ + if [ -f "${ARTIFACT_DIR}/LICENCE.md" ]; then + cp "${ARTIFACT_DIR}/LICENCE.md" ~/rpmbuild/SOURCES/ + else + wget -q "https://raw.githubusercontent.com/pgEdge/pgedge-docloader/${DOCLOADER_BRANCH}/LICENCE.md" -O ~/rpmbuild/SOURCES/LICENCE.md + fi + + # This function is for debugging purpose if you have your own keys. GH workflow does not need it. + #import_gpg_keys + + echo "🔧 Installing RPM build dependencies..." + dnf builddep -y \ + --define "docloader_version ${DOCLOADER_VERSION}" \ + --define "docloader_buildnum ${DOCLOADER_BUILDNUM}" \ + --define "arch ${ARCH}" \ + ~/rpmbuild/SPECS/docloader.spec +} + +build() { + echo "Building RPM and SRPM..." + QA_RPATHS=$(( 0xffff )) rpmbuild -ba ~/rpmbuild/SPECS/docloader.spec \ + --define "docloader_version ${DOCLOADER_VERSION}" \ + --define "docloader_buildnum ${DOCLOADER_BUILDNUM}" \ + --define "arch ${ARCH}" +} + +post_build() { + echo "📤 Copying built RPMs to /output..." + mkdir -p /output + cp -v ~/rpmbuild/RPMS/*/*.rpm /output/ || echo "No binary RPMs found" + cp -v ~/rpmbuild/SRPMS/*.src.rpm /output/ || echo "No SRPM found" + + sign_rpms /output/*.rpm + validate_signatures /output/*.rpm +} + diff --git a/pkg/common.sh b/pkg/common.sh new file mode 100755 index 0000000..4ed6f45 --- /dev/null +++ b/pkg/common.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# common.sh - Common environment variables + +# Default PostgreSQL version and derived values +export PG_VERSION="${PG_VERSION:-17.7}" +export PG_MAJOR_VERSION="$(echo "$PG_VERSION" | cut -d. -f1)" + +export DOCLOADER_REPO="https://github.com/pgEdge/pgedge-docloader.git" +export DOCLOADER_BRANCH="${COMPONENT_BRANCH:-v1.0.0}" +export DOCLOADER_VERSION=${COMPONENT_VERSION:-1.0.0} +export DOCLOADER_BUILDNUM=${COMPONENT_BUILDNUM:-1} + +# DEB only: move a pre-release pretag (e.g. BUILDNUM='beta3_1') into the +# upstream VERSION with a leading '~' (1.0.0~beta3, BUILDNUM=1) so '~' sorts +# pre-releases BELOW stable in dpkg/reprepro. Downloads use the tag +# (DOCLOADER_BRANCH), not VERSION, so this never affects the source URL. +if command -v apt-get &>/dev/null; then + if [[ "$DOCLOADER_BUILDNUM" == *_* ]]; then + DOCLOADER_PRETAG="${DOCLOADER_BUILDNUM%%_*}" + export DOCLOADER_VERSION="${DOCLOADER_VERSION}~${DOCLOADER_PRETAG}" + DOCLOADER_BUILDNUM="${DOCLOADER_BUILDNUM#*_}" + fi +fi + +export REPO_TYPE="${REPO_TYPE:-daily}" diff --git a/pkg/common/config.yml b/pkg/common/config.yml new file mode 100644 index 0000000..ddfb767 --- /dev/null +++ b/pkg/common/config.yml @@ -0,0 +1,9 @@ +# Load Markdown files into PostgreSQL +source: "./docs" +db-host: localhost +db-name: mydb +db-user: myuser +db-table: documents +col-doc-content: content +col-file-name: filename +update: true diff --git a/pkg/deb/debian/control b/pkg/deb/debian/control new file mode 100644 index 0000000..2626db6 --- /dev/null +++ b/pkg/deb/debian/control @@ -0,0 +1,14 @@ +Source: pgedge-docloader +Section: net +Priority: optional +Maintainer: Muhammad Aqeel +Build-Depends: debhelper-compat (= 13) +Standards-Version: 4.6.2 +Homepage: https://github.com/pgEdge/pgedge-docloader +Rules-Requires-Root: no + +Package: pgedge-docloader +Architecture: any +Depends: ${misc:Depends}, ${shlibs:Depends} +Description: A tool for converting HTML and RST docs into Markdown, and loading them into PostgreSQL + diff --git a/pkg/deb/debian/docs b/pkg/deb/debian/docs new file mode 100644 index 0000000..bf789ea --- /dev/null +++ b/pkg/deb/debian/docs @@ -0,0 +1 @@ +LICENCE.md diff --git a/pkg/deb/debian/format b/pkg/deb/debian/format new file mode 100644 index 0000000..c3d9f24 --- /dev/null +++ b/pkg/deb/debian/format @@ -0,0 +1,2 @@ +3.0 (quilt) + diff --git a/pkg/deb/debian/rules b/pkg/deb/debian/rules new file mode 100644 index 0000000..f09f26c --- /dev/null +++ b/pkg/deb/debian/rules @@ -0,0 +1,23 @@ +#!/usr/bin/make -f + +export DH_VERBOSE = 1 + +%: + dh $@ + +override_dh_builddeb: + dh_builddeb -- -Zgzip + +override_dh_auto_install: + # --- SBOM generation and signing --- + mkdir -p debian/tmp/sbom + syft dir:$(CURDIR) -o cyclonedx-json > debian/tmp/sbom/pgedge-docloader-sbom.json || exit 1 + KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/{print $5}' | head -n 1); export KEY_ID + gpg --armor --detach-sign \ + --output debian/tmp/sbom/pgedge-docloader-sbom.json.asc \ + debian/tmp/sbom/pgedge-docloader-sbom.json || exit 1 + install -D -m 0755 pgedge-docloader debian/pgedge-docloader/usr/bin/pgedge-docloader + install -D -m 0644 debian/config.yml debian/pgedge-docloader/etc/pgedge/docloader/config.yml + install -D -m 0644 debian/tmp/sbom/pgedge-docloader-sbom.json debian/pgedge-docloader/usr/share/pgedge-docloader/pgedge-docloader-sbom.json + install -D -m 0644 debian/tmp/sbom/pgedge-docloader-sbom.json.asc debian/pgedge-docloader/usr/share/pgedge-docloader/pgedge-docloader-sbom.json.asc + rm -rf debian/tmp/sbom diff --git a/pkg/rpm/docloader.spec b/pkg/rpm/docloader.spec new file mode 100644 index 0000000..7f2746f --- /dev/null +++ b/pkg/rpm/docloader.spec @@ -0,0 +1,49 @@ +%global debug_package %{nil} +%global sname pgedge-docloader + +Name: %{sname} +Version: %{docloader_version} +Release: %{docloader_buildnum}%{?dist} +Summary: pgEdge docloader tool for converting HTML and RST docs into Markdown, and loading them into PostgreSQL + +License: PostgreSQL License +URL: https://github.com/pgEdge/%{sname} + +Source0: https://github.com/pgEdge/%{sname}/releases/download/v%{docloader_version}/%{sname}_%{docloader_version}_Linux_%{arch}.tar.gz +Source1: config.yml +Source2: LICENCE.md + +%description +A tool for converting HTML and RST docs into Markdown, and loading them into PostgreSQL + +%prep +%setup -q -c -n pgedge-docloader-%{version} +cp %{SOURCE2} . + +%build +syft dir:%{_builddir} -o cyclonedx-json > %{_builddir}/%{sname}-sbom.json || exit 1 + +KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec/{print $5}' | head -n 1); export KEY_ID +gpg --armor --detach-sign --output %{_builddir}/%{sname}-sbom.json.asc %{_builddir}/%{sname}-sbom.json || exit 1 + +%install +install -D -m 0755 %{sname} %{buildroot}/usr/bin/%{sname} +install -D -m 0644 %{SOURCE1} %{buildroot}%{_sysconfdir}/pgedge/docloader/config.yml +mkdir -p %{buildroot}%{_datadir}/%{sname} +install -p -m 0644 %{_builddir}/%{sname}-sbom.json %{buildroot}%{_datadir}/%{sname}/%{sname}-sbom.json +install -p -m 0644 %{_builddir}/%{sname}-sbom.json.asc %{buildroot}%{_datadir}/%{sname}/%{sname}-sbom.json.asc + +%files +%license LICENCE.md +%doc README.md +%{_bindir}/%{sname} +%config(noreplace) %{_sysconfdir}/pgedge/docloader/config.yml +%{_datadir}/%{sname}/%{sname}-sbom.json +%{_datadir}/%{sname}/%{sname}-sbom.json.asc + +%changelog +* Fri Mar 13 2026 Muhammad Aqeel - 1.0.0 +- Update RPM package of pgedge-docloader +* Mon Dec 15 2025 Muhammad Aqeel - 1.0.0-beta1 +- Initial RPM package of pgedge-docloader + From 92c91972a989a44e56b969e8b06bd7c0c51e2a19 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Fri, 10 Jul 2026 14:41:59 +0500 Subject: [PATCH 2/2] ci(release): add ubuntu:resolute to the DEB build matrix --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2bdbad1..6dfc496 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,8 @@ # release MUST exist before the RPM/DEB build cells run — hence the # package jobs depend on `goreleaser`. # 2. Build RPMs (almalinux 9/10, amd64+arm64) and DEBs (jammy / noble / -# bullseye / bookworm / trixie, amd64+arm64) via pgedge-builder-action. +# resolute / bullseye / bookworm / trixie, amd64+arm64) via +# pgedge-builder-action. # Each cell's build script downloads its own source tarball from the # release, so there is no artifact-staging step (unlike the Go-binary # pipelines that stage tarballs from workflow artifacts). @@ -101,7 +102,7 @@ jobs: # emits cells with pg_version="" (see pgedge-detect-build-matrix README). archs: '["amd64","arm64"]' rpm_images: '["almalinux:9","almalinux:10"]' - deb_images: '["ubuntu:jammy","ubuntu:noble","debian:bullseye","debian:bookworm","debian:trixie"]' + deb_images: '["ubuntu:jammy","ubuntu:noble","ubuntu:resolute","debian:bullseye","debian:bookworm","debian:trixie"]' # ========================================================================= # Tag parsing → (repo_type, component-version, component-buildnum). On