From b36ef15c6baf1e8441a267e339d1f323ee3bb1c7 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Fri, 31 Jul 2026 12:57:42 +0500 Subject: [PATCH 1/4] Add in-repo release workflow and lolor RPM/DEB packaging --- .github/workflows/release.yml | 880 ++++++++++++++++++ common/build.sh | 6 + pkg/build-deb.sh | 58 ++ pkg/build-rpm.sh | 42 + pkg/common.sh | 64 ++ pkg/deb/debian/control.in | 29 + pkg/deb/debian/docs | 1 + .../debian/pgedge-postgresql-lolor.install | 1 + pkg/deb/debian/rules | 19 + pkg/deb/debian/source/format | 1 + pkg/deb/debian/tests/control | 5 + pkg/deb/debian/tests/installcheck | 3 + pkg/deb/debian/watch | 2 + pkg/rpm/lolor.spec | 79 ++ pkg/scripts/build.sh | 31 + pkg/scripts/common-functions.sh | 272 ++++++ 16 files changed, 1493 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100755 common/build.sh create mode 100644 pkg/build-deb.sh create mode 100644 pkg/build-rpm.sh create mode 100644 pkg/common.sh create mode 100644 pkg/deb/debian/control.in create mode 100644 pkg/deb/debian/docs create mode 100644 pkg/deb/debian/pgedge-postgresql-lolor.install create mode 100755 pkg/deb/debian/rules create mode 100644 pkg/deb/debian/source/format create mode 100644 pkg/deb/debian/tests/control create mode 100755 pkg/deb/debian/tests/installcheck create mode 100644 pkg/deb/debian/watch create mode 100644 pkg/rpm/lolor.spec create mode 100755 pkg/scripts/build.sh create mode 100644 pkg/scripts/common-functions.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..793aec4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,880 @@ +# ------------------------------------------------------------------------- +# +# Lolor — release pipeline (in-repo packaging → pgedge-lolor packages) +# +# Mirrors the pgEdge component release pipelines (pgedge-postgres-mcp / +# pgedge-rag-server / spock), adapted for a PostgreSQL EXTENSION +# (PER_PG_VERSION=true → one build per PG major): +# 1. Resolve the build matrix with pgedge-detect-build-matrix. Lolor's +# pkg/common.sh sets PER_PG_VERSION=true, so cells fan out over +# `pg_versions` × image × arch. This repo has no postgresql-N/ dirs, so +# the PG list cannot be auto-discovered — it is passed explicitly. +# 2. Parse the tag → (repo_type, component-version, component-buildnum) and +# assert the tag agrees with lolor.control's default_version. +# 3. Build RPMs (almalinux 9/10) and DEBs (jammy / noble / resolute / +# bullseye / bookworm / trixie), amd64 + arm64, via pgedge-builder-action. +# Each cell stages the source as release-artifacts/lolor-.tar.gz +# straight from this run's checkout (`git archive`), so branch runs under +# simulate_tag build the exact commit under test with no network fetch. +# 4. Push RPMs to dnf.pgedge.com and DEBs to apt.pgedge.com (one batched +# multi-target invocation per family; cross-repo lock per (family, repo_type)). +# 5. Publish a manifest under _pending//lolor/ for QA to +# certify; promote.yml in pgedge-repo-manager picks it up from there. +# +# ------------------------------------------------------------------------- + +name: Release + +on: + push: + # Lolor ships a single series, so every v* tag belongs here. The + # version assert in determine-repo-type still makes a tag that + # disagrees with lolor.control fail loudly. + tags: + - 'v*' + workflow_dispatch: + inputs: + simulate_tag: + description: | + Pretend a v* tag was pushed (e.g. "v1.3.0-test1") so we can + exercise determine-repo-type / package-rpm / package-deb + without pushing to live repos. Leave empty for a normal + branch test (everything downstream is skipped). + required: false + default: '' + pg_versions: + description: | + PostgreSQL majors (or full versions) to build Lolor against, + comma-separated. This repo has no postgresql-N/ dirs, so the + list cannot be auto-discovered from the packaging repo. + required: false + default: '16,17,18' + force_push: + description: | + Publish RPMs/DEBs even if some matrix cells failed. Only + honored on real tag pushes. Default false. + required: false + default: 'false' + skip_rpm: + description: 'Skip the RPM family for this run (build only DEBs).' + required: false + default: 'false' + skip_deb: + description: 'Skip the DEB family for this run (build only RPMs).' + required: false + default: 'false' + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: false + +env: + # The dnf/apt package family, backup namespace and manifest component. + # Matches the component dir that used to build these packages in + # pgedge-enterprise-packages (here it happens to equal the repo name). + COMPONENT_NAME: lolor + # The in-repo packaging directory handed to pgedge-builder-action + # (`cd /build && bash ./common/build.sh pkg`) — a different thing from + # COMPONENT_NAME above. + PACKAGING_DIR: pkg + +jobs: + # ========================================================================= + # Build matrix — pgedge-detect-build-matrix reads PER_PG_VERSION=true from + # pkg/common.sh and fans cells out over pg_versions × image × arch. + # ========================================================================= + 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # No job uses the default GITHUB_TOKEN for git operations (the + # pgEdge action repos are cloned with an explicit token and + # git archive needs no credentials), and the workspace is + # mounted into the build containers — so don't leave a + # credential in .git/config. + persist-credentials: false + + - 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: ${{ env.PACKAGING_DIR }} + pg_versions: ${{ github.event.inputs.pg_versions || '16,17,18' }} + archs: '["amd64","arm64"]' + rpm_images: '["almalinux:9","almalinux:10"]' + deb_images: '["ubuntu:jammy","ubuntu:noble","ubuntu:resolute","debian:bullseye","debian:bookworm","debian:trixie"]' + skip_rpm: ${{ github.event.inputs.skip_rpm || 'false' }} + skip_deb: ${{ github.event.inputs.skip_deb || 'false' }} + + # ========================================================================= + # Tag parsing → (repo_type, component-version, component-buildnum), plus a + # guard that the tag agrees with the source it is built from. + # ========================================================================= + 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 code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # No job uses the default GITHUB_TOKEN for git operations (the + # pgEdge action repos are cloned with an explicit token and + # git archive needs no credentials), and the workspace is + # mounted into the build containers — so don't leave a + # credential in .git/config. + persist-credentials: false + + - 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: Assert tag matches lolor.control default_version + env: + CV: ${{ steps.parse.outputs.version }} + EFFECTIVE_TAG: ${{ steps.parse.outputs.effective_tag }} + run: | + set -euo pipefail + # Command substitution under `set -e`: a failing grep would kill + # the step with a confusing message, so tolerate it and check + # for an empty result ourselves. + src_ver="$(grep -E "^[[:space:]]*default_version[[:space:]]*=" lolor.control \ + | cut -d"'" -f2 || true)" + if [ -z "$src_ver" ]; then + echo "::error::Could not read default_version from lolor.control" + exit 1 + fi + echo "lolor.control default_version=$src_ver, tag version=$CV" + if [ "$src_ver" != "$CV" ]; then + echo "::error::Tag ${EFFECTIVE_TAG} implies version ${CV} but lolor.control says ${src_ver}. Bump default_version (and add the matching lolor--*.sql upgrade script) or fix the tag." + exit 1 + fi + + - name: Route suffix → repo_type + id: route + env: + SUFFIX: ${{ steps.parse.outputs.suffix }} + EFFECTIVE_TAG: ${{ steps.parse.outputs.effective_tag }} + run: | + set -euo pipefail + # Routing is policy and stays per-repo. + 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" + + # ========================================================================= + # RPM build matrix — one cell per (EL major, arch, PG major). + # ========================================================================= + package-rpm: + name: Package RPM (${{ matrix.image }} ${{ matrix.arch }} pg${{ matrix.pg_major }}) + needs: [detect-matrix, determine-repo-type] + if: | + always() && + needs.determine-repo-type.result == 'success' && + needs.detect-matrix.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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # No job uses the default GITHUB_TOKEN for git operations (the + # pgEdge action repos are cloned with an explicit token and + # git archive needs no credentials), and the workspace is + # mounted into the build containers — so don't leave a + # credential in .git/config. + persist-credentials: false + + - name: Derive os_version from image + id: cell + env: + IMAGE: ${{ matrix.image }} + run: | + set -euo pipefail + echo "os_version=${IMAGE##*:}" >> "$GITHUB_OUTPUT" + + - name: Stage source tarball for the build container + run: | + set -euo pipefail + # pkg/build-rpm.sh prefers ./release-artifacts/lolor-.tar.gz + # over cloning the tag, so the packages are built from exactly + # this commit (and branch/simulate_tag runs work at all). + mkdir -p release-artifacts + git archive --format=tar.gz \ + --prefix="lolor-${COMPONENT_VERSION}/" \ + -o "release-artifacts/lolor-${COMPONENT_VERSION}.tar.gz" HEAD + ls -la release-artifacts/ + + - 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 }} + pg_version: ${{ matrix.pg_version }} + component_name: ${{ env.PACKAGING_DIR }} + 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: + # pg major is part of the name: without it the PG cells of one + # (os, arch) would collide on a single artifact name. + name: rpm__el-${{ steps.cell.outputs.os_version }}__${{ matrix.arch }}__pg${{ matrix.pg_major }} + path: output/el${{ steps.cell.outputs.os_version }}-${{ matrix.arch }}/ + # A cell that produced no packages is a failure, not a warning: + # the default (warn) would let the push job proceed with an + # incomplete set. + if-no-files-found: error + retention-days: 7 + + # ========================================================================= + # DNF push — single batched multi-target invocation per family. + # ========================================================================= + push-dnf: + name: Push DNF (all EL majors × archs × PG majors, batched) + 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 + outputs: + timestamp: ${{ needs.detect-matrix.outputs.dnf_ts }} + env: + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + COMPONENT_VERSION: ${{ needs.determine-repo-type.outputs.component-version }} + RPM_MATRIX: ${{ needs.detect-matrix.outputs.rpm_matrix }} + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # No job uses the default GITHUB_TOKEN for git operations (the + # pgEdge action repos are cloned with an explicit token and + # git archive needs no credentials), and the workspace is + # mounted into the build containers — so don't leave a + # credential in .git/config. + persist-credentials: false + + - 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 + run: | + set -euo pipefail + # Group every (arch, PG major) cell of an EL major into one dir. + # Driven by the artifact dirs that actually arrived rather than + # by the matrix, so a skipped/failed cell just contributes nothing. + for d in rpm-artifacts/rpm__el-*; do + [ -d "$d" ] || continue + base="$(basename "$d")" + os="${base#rpm__el-}" + os="${os%%__*}" + mkdir -p "rpms-el${os}" + find "$d" -type f -name '*.rpm' -exec mv -v {} "rpms-el${os}/" \; + done + + # find on a missing dir exits non-zero; under `set -euo pipefail` + # that propagates out of the command substitution and kills the + # step. Guard it so an OS with no artifacts just reports 0. + for os in $(echo "$RPM_MATRIX" | jq -r '[.include[].image | split(":")[1]] | unique | .[]'); do + count=0 + if [ -d "rpms-el${os}" ]; then + count=$(find "rpms-el${os}" -maxdepth 1 -name '*.rpm' | wc -l) + fi + 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 }} + 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, PG major). + # ========================================================================= + package-deb: + name: Package DEB (${{ matrix.image }} ${{ matrix.arch }} pg${{ matrix.pg_major }}) + needs: [detect-matrix, determine-repo-type] + if: | + always() && + needs.determine-repo-type.result == 'success' && + needs.detect-matrix.result == 'success' && + needs.detect-matrix.outputs.has_deb == 'true' + 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # No job uses the default GITHUB_TOKEN for git operations (the + # pgEdge action repos are cloned with an explicit token and + # git archive needs no credentials), and the workspace is + # mounted into the build containers — so don't leave a + # credential in .git/config. + persist-credentials: false + + - name: Derive distro from image + id: cell + env: + IMAGE: ${{ matrix.image }} + run: | + set -euo pipefail + echo "distro=${IMAGE##*:}" >> "$GITHUB_OUTPUT" + + - name: Stage source tarball for the build container + run: | + set -euo pipefail + mkdir -p release-artifacts + git archive --format=tar.gz \ + --prefix="lolor-${COMPONENT_VERSION}/" \ + -o "release-artifacts/lolor-${COMPONENT_VERSION}.tar.gz" HEAD + ls -la release-artifacts/ + + - 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 }} + pg_version: ${{ matrix.pg_version }} + component_name: ${{ env.PACKAGING_DIR }} + 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 }}__pg${{ matrix.pg_major }} + path: output/${{ steps.cell.outputs.distro }}-${{ matrix.arch }}/ + # A cell that produced no packages is a failure, not a warning: + # the default (warn) would let the push job proceed with an + # incomplete set. + if-no-files-found: error + retention-days: 7 + + # ========================================================================= + # APT push — single batched multi-target invocation per family. + # ========================================================================= + push-apt: + name: Push APT (all distros × archs × PG majors, 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 + 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 }} + DEB_MATRIX: ${{ needs.detect-matrix.outputs.deb_matrix }} + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + # No job uses the default GITHUB_TOKEN for git operations (the + # pgEdge action repos are cloned with an explicit token and + # git archive needs no credentials), and the workspace is + # mounted into the build containers — so don't leave a + # credential in .git/config. + persist-credentials: false + + - 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 + run: | + set -euo pipefail + for d in deb-artifacts/deb__*; do + [ -d "$d" ] || continue + base="$(basename "$d")" + distro="${base#deb__}" + distro="${distro%%__*}" + mkdir -p "debs-${distro}" + find "$d" -type f -name '*.deb' -exec mv -v {} "debs-${distro}/" \; + done + + # Same set -e + find guard as the RPM side. + for distro in $(echo "$DEB_MATRIX" | jq -r '[.include[].image | split(":")[1]] | unique | .[]'); do + count=0 + if [ -d "debs-${distro}" ]; then + count=$(find "debs-${distro}" -maxdepth 1 -name '*.deb' | wc -l) + fi + echo "${distro}: ${count} DEBs" + done + rm -rf deb-artifacts + + - name: Build targets JSON for push + backup + id: targets + run: | + set -euo pipefail + 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 }} + 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 + Slack — _pending/ enqueue for qa-certify/promote. + # ========================================================================= + 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 + steps: + - 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 + + # One backup path per OS (PG majors share it — the repo layout + # has no PG dimension), but one cell per (os, arch, pg_major) + # so qa-certify/promote can see the full build surface. + dnf_backups='[]' + cells='[]' + if [ "${PUSH_DNF_RESULT}" = "success" ] && [ -n "${DNF_TS}" ]; then + 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 pg_version pg_major; 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 pgv "${pg_version}" --arg pgm "${pg_major}" \ + --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, + pg_version:$pgv, pg_major:$pgm, + 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, .pg_version, .pg_major] | @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 pg_version pg_major; 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 pgv "${pg_version}" --arg pgm "${pg_major}" \ + --arg bp "${path}" --arg ts "$APT_TS" --arg rt "$REPO_TYPE" \ + '{family:$fam, component:$comp, component_version:$cv, + image:$img, arch:$arch, distro:$distro, + pg_version:$pgv, pg_major:$pgm, + 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, .pg_version, .pg_major] | @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: 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})" + + - 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: + 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: ${{ 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')" + + if [ "${SIMULATED}" = "true" ]; then + subject="${COMPONENT_NAME} ${EFFECTIVE_TAG} (simulated)" + else + subject="${COMPONENT_NAME} <${REPO_URL}/releases/tag/${EFFECTIVE_TAG}|${EFFECTIVE_TAG}>" + fi + + 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="—" + pgs="—" + if [ -f aggregated/manifest.json ]; then + a=$(jq -r '(.cells // []) | map(.arch) | unique | join(", ")' aggregated/manifest.json) + [ -n "$a" ] && archs="$a" + p=$(jq -r '(.cells // []) | map(.pg_major) | unique | sort | join(", ")' aggregated/manifest.json) + [ -n "$p" ] && pgs="$p" + fi + + fields=$(jq -nc \ + --arg channel "${REPO_TYPE}" \ + --arg version "${CV}" \ + --arg buildnum "${CBN}" \ + --arg archs "${archs}" \ + --arg pgs "${pgs}" \ + --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:"PG majors", value:("`" + $pgs + "`")}, + {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 + "`")} + ]') + + 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 + 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_publish: ${{ needs.determine-repo-type.outputs.simulated == 'true' || (needs.push-dnf.result != 'success' && needs.push-apt.result != 'success') }} + 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 100755 index 0000000..245c04c --- /dev/null +++ b/common/build.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Bridge wrapper: pgedge-builder-action hardcodes +# `cd /build && bash ./common/build.sh $COMPONENT_NAME`. Our packaging lives +# under pkg/, so delegate to the real entrypoint there. The shared +# pgedge-builder-action must not be modified — this wrapper adapts to it. +exec "$(dirname "$0")/../pkg/scripts/build.sh" "$@" diff --git a/pkg/build-deb.sh b/pkg/build-deb.sh new file mode 100644 index 0000000..3ed56ce --- /dev/null +++ b/pkg/build-deb.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Environment variables +BUILD_DIR="/tmp/pg_deb_build" +SRC_DIR="${BUILD_DIR}/src" + +export DEBIAN_FRONTEND=noninteractive + +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 + + rm -rf "$SRC_DIR" + mkdir -p "$SRC_DIR" + + stage_source "${BUILD_DIR}/${SRC_TARBALL}" + tar -C "$BUILD_DIR" -xzf "${BUILD_DIR}/${SRC_TARBALL}" + + echo "Moving Debian packaging into source directory..." + cp -rp "${COMPONENT_DIR}/deb/debian" "$BUILD_DIR/lolor-${LOLOR_VERSION}/" + cp $BUILD_DIR/lolor-${LOLOR_VERSION}/debian/control.in $BUILD_DIR/lolor-${LOLOR_VERSION}/debian/control + sed -i "s|PG_MAJOR_VERSION|${PG_MAJOR_VERSION}|g" $BUILD_DIR/lolor-${LOLOR_VERSION}/debian/control + mv $BUILD_DIR/lolor-${LOLOR_VERSION}/debian/pgedge-postgresql-lolor.install $BUILD_DIR/lolor-${LOLOR_VERSION}/debian/pgedge-postgresql-${PG_MAJOR_VERSION}-lolor.install + sed -i "s|PG_MAJOR_VERSION|${PG_MAJOR_VERSION}|g" $BUILD_DIR/lolor-${LOLOR_VERSION}/debian/pgedge-postgresql-${PG_MAJOR_VERSION}-lolor.install + + echo "Installing build dependencies..." + cd "$BUILD_DIR/lolor-${LOLOR_VERSION}" + sudo apt-get update + sudo apt-get build-dep -y . +} + +build() { + + cd "$BUILD_DIR/lolor-${LOLOR_VERSION}" + echo "Building Debian package..." + DISTRO=$(lsb_release -cs) + # LOLOR_DEB_VERSION carries the '~' form for pre-releases so they + # sort below stable; it equals LOLOR_VERSION for a GA build. + rm -rf debian/changelog + echo "pgedge-lolor (${LOLOR_DEB_VERSION}-${LOLOR_BUILDNUM}.${DISTRO}) unstable; urgency=low" >> debian/changelog + echo " * Update Release." >> debian/changelog + echo " -- pgEdge Build Team $(date -R)" >> debian/changelog + dch -D "$DISTRO" --force-distribution -v "${LOLOR_DEB_VERSION}-${LOLOR_BUILDNUM}.${DISTRO}" "pgEdge Lolor $LOLOR_DEB_VERSION for $DISTRO" + + DEB_BUILD_OPTIONS=nocheck PATH=/usr/lib/postgresql/${PG_MAJOR_VERSION}/bin:$PATH USE_PGXS=1 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 100644 index 0000000..b87ede8 --- /dev/null +++ b/pkg/build-rpm.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -euo pipefail + +RHEL="$(rpm --eval %rhel)" + +prepare() { + setup_dnf_build_env + echo "Copying packaging files..." + cp ${COMPONENT_NAME}/rpm/lolor.spec ~/rpmbuild/SPECS/ + + # The spec's Source0 basename is v.tar.gz (a GitHub tag archive), + # while %setup expects the lolor-/ directory inside it — which is + # what release.yml's `git archive --prefix` produces. + stage_source ~/rpmbuild/SOURCES/v${LOLOR_VERSION}.tar.gz + + # This function is for debugging purpose if you have your own keys. GH workflow sets it + #import_gpg_keys + + echo "🔧 Installing RPM build dependencies..." + dnf builddep -y \ + --define "lolor_version ${LOLOR_VERSION}" \ + --define "lolor_buildnum ${LOLOR_BUILDNUM}" \ + --define "pgmajorversion ${PG_MAJOR_VERSION}" \ + ~/rpmbuild/SPECS/lolor.spec +} + +build() { + QA_RPATHS=$(( 0xffff )) rpmbuild -ba ~/rpmbuild/SPECS/lolor.spec \ + --define "lolor_version ${LOLOR_VERSION}" \ + --define "lolor_buildnum ${LOLOR_BUILDNUM}" \ + --define "pgmajorversion ${PG_MAJOR_VERSION}" +} + +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 100644 index 0000000..24ad509 --- /dev/null +++ b/pkg/common.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# common.sh - packaging environment for Lolor. +# +# Lolor is a PostgreSQL extension: one build per PG major. +# pgedge-detect-build-matrix reads this to fan the matrix out over pg_versions. +PER_PG_VERSION=true + +export PG_VERSION="${PG_VERSION:-17}" +export PG_MAJOR_VERSION="$(echo "$PG_VERSION" | cut -d. -f1)" + +export PG_LOLOR_REPO="https://github.com/pgEdge/lolor.git" +export LOLOR_BRANCH="${COMPONENT_BRANCH:-v1.3.0}" + +# Upstream version, suffix-stripped (e.g. 1.3.0). Names the source tarball's +# internal directory and the RPM Version. +export LOLOR_VERSION="${COMPONENT_VERSION:-1.3.0}" +export LOLOR_BUILDNUM=${COMPONENT_BUILDNUM:-1} + +export REPO_TYPE="${REPO_TYPE:-daily}" + +# DEB only: move a pre-release pretag (COMPONENT_BUILDNUM='rc1_1') into the +# upstream version with a leading '~' so pre-releases sort BELOW stable in +# dpkg/reprepro: 1.3.0~rc1-1.noble < 1.3.0-1.noble. +# +# The '~' form goes in a SEPARATE variable used only by the debian/changelog: +# LOLOR_VERSION itself must stay clean because it names the source tarball +# and its unpack directory (a '~' there would break %setup and the DEB extract). +export LOLOR_DEB_VERSION="${LOLOR_VERSION}" +if command -v apt-get &>/dev/null; then + if [[ "$LOLOR_BUILDNUM" == *_* ]]; then + LOLOR_PRETAG="${LOLOR_BUILDNUM%%_*}" + export LOLOR_DEB_VERSION="${LOLOR_VERSION}~${LOLOR_PRETAG}" + LOLOR_BUILDNUM="${LOLOR_BUILDNUM##*_}" + fi +fi + +# release.yml stages the source tarball built from THIS run's checkout here. +export ARTIFACT_DIR="${ARTIFACT_DIR:-$(pwd)/release-artifacts}" +export SRC_TARBALL="lolor-${LOLOR_VERSION}.tar.gz" + +# Prefer the workflow-staged tarball (so branch / simulate_tag runs build the +# exact commit under test and need no network). The LOLOR_BRANCH clone is an +# opt-in fallback for local builds: set LOLOR_ALLOW_CLONE_FALLBACK=1. +stage_source() { + local dest="$1" + if [ -f "${ARTIFACT_DIR}/${SRC_TARBALL}" ]; then + echo "Staging ${SRC_TARBALL} from ${ARTIFACT_DIR}" + cp "${ARTIFACT_DIR}/${SRC_TARBALL}" "${dest}" + elif [ -z "${LOLOR_ALLOW_CLONE_FALLBACK:-}" ]; then + # A staged tarball is required by default: cloning LOLOR_BRANCH instead + # would ship a package built from a different commit than COMPONENT_VERSION + # claims. + echo "::error::${ARTIFACT_DIR}/${SRC_TARBALL} not found. release.yml stages it with git archive; for a local build, stage it yourself or set LOLOR_ALLOW_CLONE_FALLBACK=1 to clone ${LOLOR_BRANCH} instead." >&2 + return 1 + else + echo "Fetching Lolor source code (${LOLOR_BRANCH})" + rm -rf "lolor-${LOLOR_VERSION}" + git clone --depth=1 --branch "$LOLOR_BRANCH" "$PG_LOLOR_REPO" "lolor-${LOLOR_VERSION}" + rm -rf "lolor-${LOLOR_VERSION}/.git" + tar -czf "${SRC_TARBALL}" "lolor-${LOLOR_VERSION}" + rm -rf "lolor-${LOLOR_VERSION}" + mv "${SRC_TARBALL}" "${dest}" + fi +} diff --git a/pkg/deb/debian/control.in b/pkg/deb/debian/control.in new file mode 100644 index 0000000..029a419 --- /dev/null +++ b/pkg/deb/debian/control.in @@ -0,0 +1,29 @@ +Source: pgedge-lolor +Section: database +Priority: optional +Maintainer: pgEdge Build Team +Build-Depends: + debhelper-compat (= 13), + pgedge-postgresql-all , + pgedge-postgresql-server-dev-PG_MAJOR_VERSION, + pgedge-postgresql-server-dev-all, +Standards-Version: 4.7.0 +Rules-Requires-Root: no +Homepage: https://github.com/pgEdge/lolor + +Package: pgedge-postgresql-PG_MAJOR_VERSION-lolor +Architecture: any +Depends: + ${misc:Depends}, + pgedge-postgresql-PG_MAJOR_VERSION, + ${shlibs:Depends}, +Conflicts: + postgresql-PG_MAJOR_VERSION-lolor, +Provides: + postgresql-PG_MAJOR_VERSION-lolor, +Replaces: + postgresql-PG_MAJOR_VERSION-lolor, +Breaks: + postgresql-PG_MAJOR_VERSION-lolor, +Description: lolor is a plugin in replacement for Postgres' Large Objects that + makes them compatible with Logical Replication. diff --git a/pkg/deb/debian/docs b/pkg/deb/debian/docs new file mode 100644 index 0000000..0e4b780 --- /dev/null +++ b/pkg/deb/debian/docs @@ -0,0 +1 @@ +LICENSE.md diff --git a/pkg/deb/debian/pgedge-postgresql-lolor.install b/pkg/deb/debian/pgedge-postgresql-lolor.install new file mode 100644 index 0000000..19f2a62 --- /dev/null +++ b/pkg/deb/debian/pgedge-postgresql-lolor.install @@ -0,0 +1 @@ +debian/tmp/sbom/* usr/lib/postgresql/PG_MAJOR_VERSION/sbom/ diff --git a/pkg/deb/debian/rules b/pkg/deb/debian/rules new file mode 100755 index 0000000..ed3469b --- /dev/null +++ b/pkg/deb/debian/rules @@ -0,0 +1,19 @@ +#!/usr/bin/make -f + +%: + dh $@ + +override_dh_builddeb: + dh_builddeb -- -Zgzip + +execute_before_dh_install: + # --- SBOM generation and signing --- + mkdir -p debian/tmp/sbom + syft dir:$(CURDIR) -o cyclonedx-json > debian/tmp/sbom/lolor-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/lolor-sbom.json.asc \ + debian/tmp/sbom/lolor-sbom.json || exit 1 + +override_dh_installdocs: + dh_installdocs --all README.* diff --git a/pkg/deb/debian/source/format b/pkg/deb/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/pkg/deb/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/pkg/deb/debian/tests/control b/pkg/deb/debian/tests/control new file mode 100644 index 0000000..74b0464 --- /dev/null +++ b/pkg/deb/debian/tests/control @@ -0,0 +1,5 @@ +Depends: + make, + @, +Tests: installcheck +Restrictions: allow-stderr diff --git a/pkg/deb/debian/tests/installcheck b/pkg/deb/debian/tests/installcheck new file mode 100755 index 0000000..5a20e78 --- /dev/null +++ b/pkg/deb/debian/tests/installcheck @@ -0,0 +1,3 @@ +#!/bin/sh + +pg_buildext installcheck diff --git a/pkg/deb/debian/watch b/pkg/deb/debian/watch new file mode 100644 index 0000000..1e3ddae --- /dev/null +++ b/pkg/deb/debian/watch @@ -0,0 +1,2 @@ +version=4 +https://github.com/pgEdge/lolor/tags .*/v(.*).tar.gz diff --git a/pkg/rpm/lolor.spec b/pkg/rpm/lolor.spec new file mode 100644 index 0000000..f342051 --- /dev/null +++ b/pkg/rpm/lolor.spec @@ -0,0 +1,79 @@ +%global pname lolor +%global sname pgedge-lolor +%global pginstdir /usr/pgsql-%{pgmajorversion} + +%{!?llvm:%global llvm 1} + +Name: %{sname}_%{pgmajorversion} +Version: %{lolor_version} +Release: %{lolor_buildnum}%{?dist} +Summary: Large Object LOgical Replication +License: PostgreSQL License +URL: https://github.com/pgEdge/%{pname}/ +Source0: https://github.com/pgEdge/%{pname}/archive/refs/tags/v%{version}.tar.gz + +BuildRequires: pgedge-postgresql%{pgmajorversion}-devel +Requires: pgedge-postgresql%{pgmajorversion}-server +Provides: %{pname}_%{pgmajorversion} + +%description +lolor is a plugin in replacement for Postgres' Large Objects that makes them +compatible with Logical Replication. + +%if %llvm +%package llvmjit +Summary: Just-in-time compilation support for lolor +Requires: %{name}%{?_isa} = %{version}-%{release} +%if 0%{?suse_version} >= 1500 +BuildRequires: llvm17-devel clang17-devel +Requires: llvm17 +%endif +%if 0%{?fedora} || 0%{?rhel} >= 8 +BuildRequires: llvm-devel >= 13.0 clang-devel >= 13.0 +Requires: llvm => 13.0 +Provides: %{pname}_%{pgmajorversion}-llvmjit +%endif + +%description llvmjit +This packages provides JIT support for lolor +%endif + +%prep +%setup -q -n %{pname}-%{version} + +%build +USE_PGXS=1 PATH=%{pginstdir}/bin:$PATH %{__make} #%{?_smp_mflags} +syft dir:%{_builddir}/%{pname}-%{version} -o cyclonedx-json > %{_builddir}/%{pname}-%{version}/%{pname}-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}/%{pname}-%{version}/%{pname}-sbom.json.asc %{_builddir}/%{pname}-%{version}/%{pname}-sbom.json || exit 1 + +%install +%{__rm} -rf %{buildroot} +USE_PGXS=1 PATH=%{pginstdir}/bin:$PATH %{__make} %{?_smp_mflags} install DESTDIR=%{buildroot} +mkdir -p %{buildroot}/%{pginstdir}/sbom +install -p -m 0644 %{_builddir}/%{pname}-%{version}/%{pname}-sbom.json %{buildroot}/%{pginstdir}/sbom/%{pname}-sbom.json +install -p -m 0644 %{_builddir}/%{pname}-%{version}/%{pname}-sbom.json.asc %{buildroot}/%{pginstdir}/sbom/%{pname}-sbom.json.asc + +%files +%doc README.md +%license LICENSE.md +%{pginstdir}/lib/%{pname}.so +%{pginstdir}/share/extension/%{pname}.control +%{pginstdir}/share/extension/%{pname}*sql +%{pginstdir}/sbom/%{pname}-sbom.json +%{pginstdir}/sbom/%{pname}-sbom.json.asc + +%if %llvm +%files llvmjit + %{pginstdir}/lib/bitcode/%{pname}*.bc + %{pginstdir}/lib/bitcode/%{pname}/src/*.bc +%endif + +%changelog +* Thu Dec 18 2025 Muhammad Aqeel - 1.2.2 +- Update lolor package to 1.2.2 +* Thu Sep 25 2025 Muhammad Aqeel - 1.2.1 +- Update lolor package to 1.2.1 +* Mon Jul 21 2025 Muhammad Aqeel - 1.2 +- Initial lolor package. diff --git a/pkg/scripts/build.sh b/pkg/scripts/build.sh new file mode 100755 index 0000000..47ac765 --- /dev/null +++ b/pkg/scripts/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +COMPONENT_NAME=$1 + +# Resolve absolute, canonical paths. The common/build.sh bridge wrapper +# exec's this script with a relative $0 (./common/../pkg/scripts/build.sh), +# so `$(dirname "$0")/../${COMPONENT_NAME}/` would yield non-canonical paths +# that resolve wrong. cd+pwd gives stable absolute paths regardless of caller. +# Exported so common-functions.sh (sourced below) reuses them. +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +COMPONENT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +export SCRIPT_DIR COMPONENT_DIR + +source "${COMPONENT_DIR}/common.sh" + +COMMON_FILE="${SCRIPT_DIR}/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/pkg/scripts/common-functions.sh b/pkg/scripts/common-functions.sh new file mode 100644 index 0000000..9b49252 --- /dev/null +++ b/pkg/scripts/common-functions.sh @@ -0,0 +1,272 @@ +#!/bin/bash + +install_syft(){ + + # Pin the installer to a tagged ref (not mutable main) AND pin the installed + # version, so release builds are reproducible and not exposed to upstream + # changes on syft's main branch. Override SYFT_VERSION to bump. + SYFT_VERSION="${SYFT_VERSION:-v1.45.1}" + echo "Installing syft ${SYFT_VERSION}..." + curl -sSfL "https://raw.githubusercontent.com/anchore/syft/${SYFT_VERSION}/install.sh" | sudo sh -s -- -b /usr/local/bin "${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 "${COMPONENT_DIR}/build-rpm.sh" + elif command -v apt-get &>/dev/null; then + echo "Detected Debian-based system" + source "${COMPONENT_DIR}/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="${SCRIPT_DIR}/public.key" + PUB_FILE="${SCRIPT_DIR}/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() { + local rc=0 + + # 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. Any per-file failure flips rc to 1 so the caller sees a + # non-zero exit; we keep iterating so the operator gets the full list of + # failures in one run rather than aborting on the first bad 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." + rc=1 + 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." + rc=1 + continue + fi + + # Sign the RPM using rpmsign, using passphrase if provided + if rpmsign --define "_gpg_name $KEY_ID" --addsign "$file" >/dev/null 2>&1; then + echo "Successfully signed '$file'." + else + echo "Error: Failed to sign '$file'." + rc=1 + fi + done + echo "=======================Signing Completes==================" + + # Clean up + rm -f "$PRIVATE_KEY_FILE" + return $rc +} + +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 +} From 293c40956e24b0ff37aede1460eecf0689c11495 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Fri, 31 Jul 2026 15:46:50 +0500 Subject: [PATCH 2/4] Add in-repo release workflow and lolor RPM/DEB packaging --- .github/workflows/release.yml | 36 ++++++++++++++++++++++++++++++++++- pkg/build-deb.sh | 14 ++++++++++---- pkg/build-rpm.sh | 2 +- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 793aec4..9cbaf1b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,6 +116,10 @@ jobs: git clone --depth 1 \ "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-detect-build-matrix.git" \ ".github/actions/pgedge-detect-build-matrix" + # The clone URL embeds the token, so git persists it in + # .git/config — inside a workspace that is mounted into the + # build containers. The action only needs its files. + rm -rf ".github/actions/pgedge-detect-build-matrix/.git" - name: Detect build matrix id: detect @@ -163,6 +167,10 @@ jobs: git clone --depth 1 \ "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-parse-release-tag.git" \ ".github/actions/pgedge-parse-release-tag" + # The clone URL embeds the token, so git persists it in + # .git/config — inside a workspace that is mounted into the + # build containers. The action only needs its files. + rm -rf ".github/actions/pgedge-parse-release-tag/.git" - name: Parse release tag id: parse @@ -273,6 +281,10 @@ jobs: git clone --depth 1 \ "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-builder-action.git" \ ".github/actions/pgedge-builder-action" + # The clone URL embeds the token, so git persists it in + # .git/config — inside a workspace that is mounted into the + # build containers. The action only needs its files. + rm -rf ".github/actions/pgedge-builder-action/.git" - name: Build RPM uses: ./.github/actions/pgedge-builder-action @@ -309,6 +321,7 @@ jobs: name: Push DNF (all EL majors × archs × PG majors, batched) if: | always() && github.ref_type == 'tag' && + needs.detect-matrix.outputs.has_rpm == 'true' && (needs.package-rpm.result == 'success' || (github.event.inputs.force_push == 'true' && needs.package-rpm.result != 'cancelled')) needs: [detect-matrix, determine-repo-type, package-rpm] @@ -387,6 +400,8 @@ jobs: git clone --depth 1 --branch multi-target \ "https://x-access-token:${TOKEN}@github.com/pgEdge/${repo}.git" \ ".github/actions/${repo}" + # Drop the token git persisted in .git/config. + rm -rf ".github/actions/${repo}/.git" done - name: Push RPMs to yum repo (all EL majors batched) @@ -469,6 +484,10 @@ jobs: git clone --depth 1 \ "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-builder-action.git" \ ".github/actions/pgedge-builder-action" + # The clone URL embeds the token, so git persists it in + # .git/config — inside a workspace that is mounted into the + # build containers. The action only needs its files. + rm -rf ".github/actions/pgedge-builder-action/.git" - name: Build DEB uses: ./.github/actions/pgedge-builder-action @@ -503,6 +522,7 @@ jobs: name: Push APT (all distros × archs × PG majors, batched) if: | always() && github.ref_type == 'tag' && + needs.detect-matrix.outputs.has_deb == 'true' && (needs.package-deb.result == 'success' || (github.event.inputs.force_push == 'true' && needs.package-deb.result != 'cancelled')) needs: [detect-matrix, determine-repo-type, package-deb] @@ -576,6 +596,8 @@ jobs: git clone --depth 1 --branch multi-target \ "https://x-access-token:${TOKEN}@github.com/pgEdge/${repo}.git" \ ".github/actions/${repo}" + # Drop the token git persisted in .git/config. + rm -rf ".github/actions/${repo}/.git" done - name: Push DEBs to apt repo (all distros batched) @@ -629,6 +651,14 @@ jobs: set -euo pipefail mkdir -p aggregated + # --argjson below needs literal true/false; an unset SIMULATED + # would make jq exit non-zero and lose the manifest after the + # packages have already been pushed. + case "${SIMULATED:-}" in + true|false) simulated_json="${SIMULATED}" ;; + *) simulated_json=false ;; + esac + # One backup path per OS (PG majors share it — the repo layout # has no PG dimension), but one cell per (os, arch, pg_major) # so qa-certify/promote can see the full build surface. @@ -680,7 +710,7 @@ jobs: --arg run_attempt "${GITHUB_RUN_ATTEMPT}" \ --arg repo "${GITHUB_REPOSITORY}" \ --arg tag "${EFFECTIVE_TAG}" \ - --argjson simulated "${SIMULATED}" \ + --argjson simulated "${simulated_json}" \ --arg commit_sha "${GITHUB_SHA}" \ --arg component_name "${COMPONENT_NAME}" \ --arg component_version "${CV}" \ @@ -740,6 +770,10 @@ jobs: git clone --depth 1 \ "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-build-publisher.git" \ ".github/actions/pgedge-build-publisher" + # The clone URL embeds the token, so git persists it in + # .git/config — inside a workspace that is mounted into the + # build containers. The action only needs its files. + rm -rf ".github/actions/pgedge-build-publisher/.git" - name: Compose Slack fields id: compose_slack diff --git a/pkg/build-deb.sh b/pkg/build-deb.sh index 3ed56ce..af58c28 100644 --- a/pkg/build-deb.sh +++ b/pkg/build-deb.sh @@ -40,11 +40,17 @@ build() { DISTRO=$(lsb_release -cs) # LOLOR_DEB_VERSION carries the '~' form for pre-releases so they # sort below stable; it equals LOLOR_VERSION for a GA build. + # A Debian changelog entry needs a blank line after the header and before the + # maintainer trailer. Written in final form, so no dch pass is needed — dch + # with this same version appended a duplicate entry instead of editing. rm -rf debian/changelog - echo "pgedge-lolor (${LOLOR_DEB_VERSION}-${LOLOR_BUILDNUM}.${DISTRO}) unstable; urgency=low" >> debian/changelog - echo " * Update Release." >> debian/changelog - echo " -- pgEdge Build Team $(date -R)" >> debian/changelog - dch -D "$DISTRO" --force-distribution -v "${LOLOR_DEB_VERSION}-${LOLOR_BUILDNUM}.${DISTRO}" "pgEdge Lolor $LOLOR_DEB_VERSION for $DISTRO" + { + echo "pgedge-lolor (${LOLOR_DEB_VERSION}-${LOLOR_BUILDNUM}.${DISTRO}) ${DISTRO}; urgency=low" + echo "" + echo " * Update Release." + echo "" + echo " -- pgEdge Build Team $(date -R)" + } > debian/changelog DEB_BUILD_OPTIONS=nocheck PATH=/usr/lib/postgresql/${PG_MAJOR_VERSION}/bin:$PATH USE_PGXS=1 dpkg-buildpackage -us -uc -b } diff --git a/pkg/build-rpm.sh b/pkg/build-rpm.sh index b87ede8..e85f3dd 100644 --- a/pkg/build-rpm.sh +++ b/pkg/build-rpm.sh @@ -6,7 +6,7 @@ RHEL="$(rpm --eval %rhel)" prepare() { setup_dnf_build_env echo "Copying packaging files..." - cp ${COMPONENT_NAME}/rpm/lolor.spec ~/rpmbuild/SPECS/ + cp "${COMPONENT_DIR}/rpm/lolor.spec" ~/rpmbuild/SPECS/ # The spec's Source0 basename is v.tar.gz (a GitHub tag archive), # while %setup expects the lolor-/ directory inside it — which is From 4cae18838668f1f47ec0607ddb92a666dc520d01 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Fri, 31 Jul 2026 16:32:14 +0500 Subject: [PATCH 3/4] Correct the force_push description: it applies only to tag dispatches --- .github/workflows/release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9cbaf1b..5bcf1ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,8 +51,11 @@ on: default: '16,17,18' force_push: description: | - Publish RPMs/DEBs even if some matrix cells failed. Only - honored on real tag pushes. Default false. + Publish RPMs/DEBs even if some matrix cells failed. Reachable + only by dispatching this workflow against a TAG ref + (`gh workflow run release.yml --ref -f force_push=true`): + on a plain tag push github.event.inputs is null, so the push + gates see it as false. Default false. required: false default: 'false' skip_rpm: From 781fbe8807be1fea19e646c1e63123cbb7579b89 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Fri, 31 Jul 2026 16:44:26 +0500 Subject: [PATCH 4/4] Set AWS_DEFAULT_REGION for the inline manifest upload --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bcf1ef..3b11fbe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -754,6 +754,10 @@ jobs: env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + # Every shared pgEdge action defaults its region input to + # us-east-1 and exports it; this inline aws call is the only + # one that relied on botocore's implicit fallback. + AWS_DEFAULT_REGION: us-east-1 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 }}