diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3cddaf4..21712ac 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,71 +1,1082 @@ +# ------------------------------------------------------------------------- +# +# pgEdge ACE +# +# Copyright (c) 2025 - 2026, pgEdge, Inc. +# This software is released under The PostgreSQL License +# +# Release pipeline: +# 1. GoReleaser builds the Go binaries. On a real tag it publishes the +# GitHub release with all tarballs; on simulate_tag it builds them in +# --snapshot mode (publishes nothing). Either way it uploads the +# tarballs as `release-artifacts`, which the packaging jobs stage — so +# packaging never needs a pre-existing release and pushes stay tag-only. +# 2. Publish the multi-arch Docker image to ghcr.io/pgedge/ace (tag-only). +# 3. Build RPMs (almalinux 9/10, amd64+arm64) and DEBs (jammy / noble / +# bullseye / bookworm / trixie, amd64+arm64) via pgedge-builder-action. +# Each cell stages its arch-specific tarball from `release-artifacts`. +# 4. 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)). +# 5. 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 - packages: write + # Least-privilege default — overridden per-job only where write is + # genuinely needed (goreleaser needs contents: write to publish the + # GitHub release; docker needs packages: write to push to GHCR). + contents: read + +concurrency: + group: release-${{ github.ref_name }} + cancel-in-progress: false jobs: - goreleaser: - runs-on: ubuntu-latest + # ========================================================================= + # 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. packaging/ 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: packaging + # ace has no postgresql-N/ siblings; packaging 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 ace'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. On a real tag it publishes the + # GitHub release with all tarballs; under 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). Keeps the container-based goreleaser + # mechanism the ace release workflow already used. + # ========================================================================= + goreleaser: + name: Build (GoReleaser) + # Runs on a real tag AND under simulate_tag branch testing. + 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@v4 + with: + fetch-depth: 0 + + - name: Set up QEMU (for cross-compiling) + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 + - name: Run GoReleaser + 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 }} + run: | + # --snapshot on a non-tag (simulate_tag) dispatch builds the + # tarballs without publishing a GitHub release. + docker run --rm \ + -v "$PWD":/workspace \ + -w /workspace \ + -e GITHUB_TOKEN \ + -e GORELEASER_CURRENT_TAG \ + goreleaser/goreleaser:v2.15.2 \ + release --clean ${{ (github.event_name == 'workflow_dispatch' && github.ref_type != 'tag') && '--snapshot' || '' }} - - name: Set up QEMU (for cross-compiling) - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + - name: Upload release artifacts (for packaging) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: release-artifacts + path: dist/*.tar.gz + retention-days: 1 - - name: Run GoReleaser + # ========================================================================= + # Docker image — multi-arch buildx push to ghcr.io/pgedge/ace. Tag-only: + # simulate_tag branch tests never push an image. `:latest` moves only on + # a final GA tag (no pre-release suffix); -test/-dev/-rc/-beta/-alpha tags + # publish their versioned tag but never move latest. + # ========================================================================= + docker: + name: Publish Docker Image + runs-on: ubuntu-latest + needs: goreleaser + # Strictly tag-only — never push an image on a simulate_tag dispatch. + if: github.ref_type == 'tag' + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up QEMU (for multi-arch) + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + + - name: Log in to GHCR + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + env: + # Pass the tag via env, never inline ${{ }}, so a crafted tag + # name cannot inject shell into this step. + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + # Always publish the exact-version tag. + tags=( --tag "ghcr.io/pgedge/ace:${REF_NAME}" ) + # Move :latest only for a final GA tag (no pre-release + # suffix). Any tag containing '-' (e.g. -test/-dev/-rc/ + # -beta/-alpha) publishes its versioned tag but leaves + # :latest pointing at the last GA release. + case "${REF_NAME}" in + *-*) echo "Pre-release tag ${REF_NAME}; not moving :latest" ;; + *) tags+=( --tag "ghcr.io/pgedge/ace:latest" ) ;; + esac + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + "${tags[@]}" \ + --build-arg "ACE_VERSION=${REF_NAME}" \ + --file Dockerfile \ + --push . + + # ========================================================================= + # RPM build matrix — one cell per (EL major, arch). Each cell stages its + # arch-specific source tarball from the `release-artifacts` uploaded by + # the goreleaser job and produces arch-specific pgedge-ace 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: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - docker run --rm \ - -v "$PWD":/workspace \ - -w /workspace \ - -e GITHUB_TOKEN \ - goreleaser/goreleaser:v2.15.2 \ - release --clean - - docker: - name: Publish Docker Image - runs-on: ubuntu-latest - needs: goreleaser - permissions: - contents: read - packages: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up QEMU (for multi-arch) - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - - name: Log in to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push image - run: | - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --tag ghcr.io/pgedge/ace:${{ github.ref_name }} \ - --tag ghcr.io/pgedge/ace:latest \ - --build-arg ACE_VERSION=${{ github.ref_name }} \ - --file Dockerfile \ - --push . + 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/ace.tar.gz over + # wget'ing the GitHub release, so simulate_tag builds work + # without a published release. The GoReleaser archive wraps + # its payload in ace-/, so we re-pack it + # under the plain RPM/DEB version (ace-${COMPONENT_VERSION}) + # that the spec's %setup / build-deb.sh expect. + case "${{ matrix.arch }}" in + amd64) tok=x86_64 ;; + arm64) tok=arm64 ;; + *) echo "::error::unexpected arch ${{ matrix.arch }}"; exit 1 ;; + esac + mkdir -p release-artifacts + tmp="$(mktemp -d)" + tar -C "$tmp" -xzf go-dist/ace_Linux_${tok}.tar.gz # internal dir ace-/ + src="$(cd "$tmp" && echo ace-*/)"; src="${src%/}" # e.g. ace-2.0.0-rc.1 + # On a GA tag the GoReleaser version already equals the plain + # COMPONENT_VERSION, so the dir is already correct; rename only + # when they differ (mv dir/ dir errors under set -e). + if [ "$src" != "ace-${COMPONENT_VERSION}" ]; then + mv "$tmp/$src" "$tmp/ace-${COMPONENT_VERSION}" # normalize to the plain RPM/DEB version + fi + ( cd "$tmp" && tar -czf - "ace-${COMPONENT_VERSION}" ) > release-artifacts/ace.tar.gz + cp NOTICE.txt release-artifacts/NOTICE.txt # not in the GoReleaser archive + rm -rf "$tmp" go-dist + 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 }} + component_name: packaging + 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: + 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 stages its + # arch-specific source tarball from the `release-artifacts` uploaded by + # the goreleaser job and produces arch-specific pgedge-ace 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/ace.tar.gz over + # wget'ing the GitHub release, so simulate_tag builds work + # without a published release. The GoReleaser archive wraps + # its payload in ace-/, so we re-pack it + # under the plain RPM/DEB version (ace-${COMPONENT_VERSION}) + # that build-deb.sh expects. + case "${{ matrix.arch }}" in + amd64) tok=x86_64 ;; + arm64) tok=arm64 ;; + *) echo "::error::unexpected arch ${{ matrix.arch }}"; exit 1 ;; + esac + mkdir -p release-artifacts + tmp="$(mktemp -d)" + tar -C "$tmp" -xzf go-dist/ace_Linux_${tok}.tar.gz # internal dir ace-/ + src="$(cd "$tmp" && echo ace-*/)"; src="${src%/}" # e.g. ace-2.0.0-rc.1 + # On a GA tag the GoReleaser version already equals the plain + # COMPONENT_VERSION, so the dir is already correct; rename only + # when they differ (mv dir/ dir errors under set -e). + if [ "$src" != "ace-${COMPONENT_VERSION}" ]; then + mv "$tmp/$src" "$tmp/ace-${COMPONENT_VERSION}" # normalize to the plain RPM/DEB version + fi + ( cd "$tmp" && tar -czf - "ace-${COMPONENT_VERSION}" ) > release-artifacts/ace.tar.gz + cp NOTICE.txt release-artifacts/NOTICE.txt # not in the GoReleaser archive + rm -rf "$tmp" go-dist + 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 }} + component_name: packaging + 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/packaging/build-deb.sh b/packaging/build-deb.sh new file mode 100755 index 0000000..ee3e273 --- /dev/null +++ b/packaging/build-deb.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Environment variables +BUILD_DIR="/tmp/pg_deb_build" +SRC_DIR="${BUILD_DIR}/src" + +CWD="$(pwd)" + +# Directory where the release pipeline stages the pre-built GoReleaser +# tarball (ace.tar.gz) and NOTICE.txt. When present, prepare() prefers +# these staged files over downloading from the GitHub release, so +# simulate_tag builds work without a published release. +ARTIFACT_DIR="${ARTIFACT_DIR:-${CWD}/release-artifacts}" + +export DEBIAN_FRONTEND=noninteractive +ARCH=$(uname -m) +if [ "$ARCH" = "aarch64" ]; then + ARCH="arm64" +fi + +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 ace Debian packaging repo..." + rm -rf "$SRC_DIR" + mkdir -p $SRC_DIR + + echo "Fetching ace source code" + if [ -f "${ARTIFACT_DIR}/ace.tar.gz" ]; then + echo "Using staged tarball ${ARTIFACT_DIR}/ace.tar.gz" + cp "${ARTIFACT_DIR}/ace.tar.gz" "${BUILD_DIR}/ace_Linux_${ARCH}.tar.gz" + else + echo "Downloading release tarball for ${ACE_BRANCH} (${ARCH})..." + wget -q "https://github.com/pgEdge/ace/releases/download/${ACE_BRANCH}/ace_Linux_${ARCH}.tar.gz" \ + -O "${BUILD_DIR}/ace_Linux_${ARCH}.tar.gz" + fi + tar -C "$BUILD_DIR" -xvzf "${BUILD_DIR}/ace_Linux_${ARCH}.tar.gz" + # The tarball contains exactly one top-level ace-<...> directory. Its version + # suffix may differ from ACE_VERSION: common.sh rewrites ACE_VERSION to + # X.Y.Z~pretag for DEB pre-releases, while the workflow normalises the staged + # dir to the plain X.Y.Z. Locate it by glob so the extract works regardless + # of the internal directory's suffix. + shopt -s nullglob + extracted_dirs=("$BUILD_DIR"/ace-*/) + shopt -u nullglob + mv "${extracted_dirs[0]}"* "$BUILD_DIR"/ + rm -rf "${extracted_dirs[0]}" + + echo "Moving Debian packaging into source directory..." + cp -rp "${CWD}/${COMPONENT_NAME}/deb/debian" "$BUILD_DIR/" + cp ${COMPONENT_NAME}/common/pgedge-ace.* $BUILD_DIR/debian/ + + if [ -f "${ARTIFACT_DIR}/NOTICE.txt" ]; then + echo "Using staged NOTICE.txt from ${ARTIFACT_DIR}" + cp "${ARTIFACT_DIR}/NOTICE.txt" "$BUILD_DIR/NOTICE.txt" + else + echo "Downloading NOTICE.txt from ace repo..." + curl -fsSL -o "$BUILD_DIR/NOTICE.txt" \ + https://raw.githubusercontent.com/pgEdge/ace/${ACE_BRANCH}/NOTICE.txt + fi + + echo "Installing build dependencies..." + cd "$BUILD_DIR/" + sudo apt-get update + sudo apt-get build-dep -y . +} + +build() { + + cd "$BUILD_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/packaging/build-rpm.sh b/packaging/build-rpm.sh new file mode 100755 index 0000000..807af06 --- /dev/null +++ b/packaging/build-rpm.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -euo pipefail + +RHEL="$(rpm --eval %rhel)" +ARCH=$(uname -m) +if [ "$ARCH" = "aarch64" ]; then + ARCH="arm64" +fi + +CWD="$(pwd)" + +# Directory where the release pipeline stages the pre-built GoReleaser +# tarball (ace.tar.gz) and NOTICE.txt. When present, prepare() prefers +# these staged files over downloading from the GitHub release, so +# simulate_tag builds work without a published release. +ARTIFACT_DIR="${ARTIFACT_DIR:-${CWD}/release-artifacts}" + +prepare() { + setup_dnf_build_env + + echo "Copying packaging files..." + cp ${COMPONENT_NAME}/rpm/ace.spec ~/rpmbuild/SPECS/ + + echo "Obtaining official source tarball and docs..." + if [ -f "${ARTIFACT_DIR}/ace.tar.gz" ]; then + echo "Using staged tarball ${ARTIFACT_DIR}/ace.tar.gz" + cp "${ARTIFACT_DIR}/ace.tar.gz" ~/rpmbuild/SOURCES/ace_Linux_${ARCH}.tar.gz + else + echo "Downloading release tarball for ${ACE_BRANCH} (${ARCH})..." + wget -q "https://github.com/pgEdge/ace/releases/download/${ACE_BRANCH}/ace_Linux_${ARCH}.tar.gz" \ + -O ~/rpmbuild/SOURCES/ace_Linux_${ARCH}.tar.gz + fi + cp ${COMPONENT_NAME}/common/pgedge-ace.* ~/rpmbuild/SOURCES/ + + if [ -f "${ARTIFACT_DIR}/NOTICE.txt" ]; then + echo "Using staged NOTICE.txt from ${ARTIFACT_DIR}" + cp "${ARTIFACT_DIR}/NOTICE.txt" ~/rpmbuild/SOURCES/NOTICE.txt + else + echo "Downloading NOTICE.txt from ace repo..." + curl -fsSL -o ~/rpmbuild/SOURCES/NOTICE.txt \ + https://raw.githubusercontent.com/pgEdge/ace/${ACE_BRANCH}/NOTICE.txt + 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 "ace_version ${ACE_VERSION}" \ + --define "ace_buildnum ${ACE_BUILDNUM}" \ + --define "arch ${ARCH}" \ + ~/rpmbuild/SPECS/ace.spec +} + +build() { + echo "Building RPM and SRPM..." + QA_RPATHS=$(( 0xffff )) rpmbuild -ba ~/rpmbuild/SPECS/ace.spec \ + --define "ace_version ${ACE_VERSION}" \ + --define "ace_buildnum ${ACE_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/packaging/common.sh b/packaging/common.sh new file mode 100755 index 0000000..8c017cc --- /dev/null +++ b/packaging/common.sh @@ -0,0 +1,26 @@ +#!/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 ACE_REPO="https://github.com/pgEdge/ace.git" +export ACE_BRANCH="${COMPONENT_BRANCH:-v2.0.0}" +export ACE_VERSION=${COMPONENT_VERSION:-2.0.0} +export ACE_BUILDNUM=${COMPONENT_BUILDNUM:-1} + +# DEB only: move a pre-release pretag (e.g. BUILDNUM='rc1_1') into the upstream +# VERSION with a leading '~' (2.0.0~rc1, BUILDNUM=1) so '~' sorts pre-releases +# BELOW stable in dpkg/reprepro. Gated on apt-get so RPM keeps the pretag in +# the Release field. pgedge-parse-release-tag already normalises the tag suffix +# (e.g. v2.0.0-test.1 -> buildnum 'test1_1'), so no dots reach here. +if command -v apt-get &>/dev/null; then + if [[ "$ACE_BUILDNUM" == *_* ]]; then + ACE_PRETAG="${ACE_BUILDNUM%%_*}" + export ACE_VERSION="${ACE_VERSION}~${ACE_PRETAG}" + ACE_BUILDNUM="${ACE_BUILDNUM#*_}" + fi +fi + +export REPO_TYPE="${REPO_TYPE:-daily}" diff --git a/packaging/common/pgedge-ace.logrotate b/packaging/common/pgedge-ace.logrotate new file mode 100644 index 0000000..aa90d11 --- /dev/null +++ b/packaging/common/pgedge-ace.logrotate @@ -0,0 +1,14 @@ +/var/log/pgedge/ace/*.log { + daily + missingok + rotate 7 + compress + delaycompress + notifempty + create 0640 pgedge pgedge + sharedscripts + postrotate + /bin/systemctl kill -s HUP pgedge-ace.service >/dev/null 2>&1 || true + endscript +} + diff --git a/packaging/common/pgedge-ace.service b/packaging/common/pgedge-ace.service new file mode 100644 index 0000000..0acd5f4 --- /dev/null +++ b/packaging/common/pgedge-ace.service @@ -0,0 +1,16 @@ +[Unit] +Description=Active Consistency Engine of pgEdge +After=network.target postgresql.service + +[Service] +User=pgedge +Group=pgedge +WorkingDirectory=/var/lib/pgedge/ace +ExecStart=/usr/bin/ace start +StandardOutput=append:/var/log/pgedge/ace/ace.log +StandardError=append:/var/log/pgedge/ace/ace.log +Restart=on-failure + +[Install] +WantedBy=multi-user.target + diff --git a/packaging/common/pgedge-ace.tmpfiles.conf b/packaging/common/pgedge-ace.tmpfiles.conf new file mode 100644 index 0000000..d104f86 --- /dev/null +++ b/packaging/common/pgedge-ace.tmpfiles.conf @@ -0,0 +1,3 @@ +d /var/lib/pgedge/ace 0755 pgedge pgedge - +d /var/log/pgedge/ace 0755 pgedge pgedge - + diff --git a/packaging/deb/debian/control b/packaging/deb/debian/control new file mode 100644 index 0000000..30778ea --- /dev/null +++ b/packaging/deb/debian/control @@ -0,0 +1,14 @@ +Source: pgedge-ace +Section: net +Priority: optional +Maintainer: Muhammad Aqeel +Build-Depends: debhelper-compat (= 13), systemd +Standards-Version: 4.6.2 +Homepage: https://github.com/pgEdge/ace +Rules-Requires-Root: no + +Package: pgedge-ace +Architecture: any +Depends: ${misc:Depends}, ${shlibs:Depends}, systemd, logrotate +Description: The Active Consistency Engine of pgEdge + diff --git a/packaging/deb/debian/docs b/packaging/deb/debian/docs new file mode 100644 index 0000000..ea8a46a --- /dev/null +++ b/packaging/deb/debian/docs @@ -0,0 +1,3 @@ +LICENSE +NOTICE.txt +README.md diff --git a/packaging/deb/debian/format b/packaging/deb/debian/format new file mode 100644 index 0000000..c3d9f24 --- /dev/null +++ b/packaging/deb/debian/format @@ -0,0 +1,2 @@ +3.0 (quilt) + diff --git a/packaging/deb/debian/postinst b/packaging/deb/debian/postinst new file mode 100644 index 0000000..adc7125 --- /dev/null +++ b/packaging/deb/debian/postinst @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +# Tmpfiles setup +if [ "$1" = "configure" ]; then + systemd-tmpfiles --create /usr/lib/tmpfiles.d/pgedge-ace.conf || true +fi + +#DEBHELPER# +exit 0 + diff --git a/packaging/deb/debian/preinst b/packaging/deb/debian/preinst new file mode 100644 index 0000000..f9a9459 --- /dev/null +++ b/packaging/deb/debian/preinst @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +case "$1" in + install|upgrade) + + # Create parent directory first + mkdir -p /var/lib/pgedge + + # Create pgedge user if it doesn't exist + if ! getent group pgedge >/dev/null; then + addgroup --system pgedge + fi + if ! getent passwd pgedge >/dev/null; then + adduser --system --home /var/lib/pgedge --no-create-home \ + --ingroup pgedge --disabled-password --shell /bin/false \ + --gecos "pgEdge Services" pgedge + fi + + # Create directories + mkdir -p /var/log/pgedge/ace + mkdir -p /var/lib/pgedge/ace + chown -R pgedge:pgedge /var/log/pgedge/ace + chown -R pgedge:pgedge /var/lib/pgedge/ace + ;; +esac + +#DEBHELPER# + +exit 0 diff --git a/packaging/deb/debian/rules b/packaging/deb/debian/rules new file mode 100644 index 0000000..0bbb1d7 --- /dev/null +++ b/packaging/deb/debian/rules @@ -0,0 +1,27 @@ +#!/usr/bin/make -f + +export DH_VERBOSE = 1 + +%: + dh $@ + +override_dh_builddeb: + dh_builddeb -- -Zgzip + +override_dh_compress: + dh_compress -XNOTICE.txt + +override_dh_auto_install: + # --- SBOM generation and signing --- + mkdir -p debian/tmp/sbom + syft dir:$(CURDIR) -o cyclonedx-json > debian/tmp/sbom/ace-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/ace-sbom.json.asc \ + debian/tmp/sbom/ace-sbom.json || exit 1 + install -D -m 0755 ace debian/pgedge-ace/usr/bin/ace + install -D -m 0644 debian/pgedge-ace.tmpfiles.conf debian/pgedge-ace/usr/lib/tmpfiles.d/pgedge-ace.conf + install -D -m 0644 debian/pgedge-ace.logrotate debian/pgedge-ace/etc/logrotate.d/pgedge-ace + install -D -m 0644 debian/tmp/sbom/ace-sbom.json debian/pgedge-ace/usr/share/pgedge-ace/ace-sbom.json + install -D -m 0644 debian/tmp/sbom/ace-sbom.json.asc debian/pgedge-ace/usr/share/pgedge-ace/ace-sbom.json.asc + rm -rf debian/tmp/sbom diff --git a/packaging/rpm/ace.spec b/packaging/rpm/ace.spec new file mode 100644 index 0000000..20fe99e --- /dev/null +++ b/packaging/rpm/ace.spec @@ -0,0 +1,84 @@ +%global debug_package %{nil} +%global sname ace + +Name: pgedge-%{sname} +Version: %{ace_version} +Release: %{ace_buildnum}%{?dist} +Summary: The Active Consistency Engine of pgEdge + +License: PostgreSQL License +URL: https://github.com/pgEdge/%{sname} + +Source0: https://github.com/pgEdge/%{sname}/releases/download/v%{version}/%{sname}_Linux_%{arch}.tar.gz +Source1: %{name}.service +Source2: %{name}.tmpfiles.conf +Source3: %{name}.logrotate +Source4: NOTICE.txt + +BuildRequires: systemd +Requires(post): systemd +Requires(preun): systemd +Requires(postun): systemd +Requires: systemd +Requires: logrotate + +%description +The Active Consistency Engine of pgEdge + +%prep +%setup -q -n %{sname}-%{version} +cp %{SOURCE4} . + +%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}%{_unitdir}/%{name}.service +install -D -m 0644 %{SOURCE2} %{buildroot}%{_tmpfilesdir}/%{name}.conf +install -D -m 0644 %{SOURCE3} %{buildroot}%{_sysconfdir}/logrotate.d/%{name} +install -d %{buildroot}/var/lib/pgedge/ace +install -d %{buildroot}/var/log/pgedge/ace +mkdir -p %{buildroot}%{_datadir}/pgedge-%{sname} +install -p -m 0644 %{_builddir}/%{sname}-sbom.json %{buildroot}%{_datadir}/pgedge-%{sname}/%{sname}-sbom.json +install -p -m 0644 %{_builddir}/%{sname}-sbom.json.asc %{buildroot}%{_datadir}/pgedge-%{sname}/%{sname}-sbom.json.asc + +%pre +# Ensure pgedge user/group exists +getent group pgedge >/dev/null || groupadd -r pgedge +getent passwd pgedge >/dev/null || \ + useradd -r -g pgedge -d /var/lib/pgedge -s /sbin/nologin \ + -c "pgEdge Services" pgedge +exit 0 + +%post +%systemd_post %{name}.service +%tmpfiles_create %{_tmpfilesdir}/%{name}.conf + +%preun +%systemd_preun %{name}.service + +%postun +%systemd_postun_with_restart %{name}.service + +%files +%license LICENSE NOTICE.txt +%doc README.md +%{_bindir}/%{sname} +%{_unitdir}/%{name}.service +%{_tmpfilesdir}/%{name}.conf +%config(noreplace) %{_sysconfdir}/logrotate.d/%{name} +%dir %attr(0755,pgedge,pgedge) /var/lib/pgedge +%dir %attr(0755,pgedge,pgedge) /var/lib/pgedge/ace +%dir %attr(0755,pgedge,pgedge) /var/log/pgedge +%dir %attr(0755,pgedge,pgedge) /var/log/pgedge/ace +%{_datadir}/pgedge-%{sname}/%{sname}-sbom.json +%{_datadir}/pgedge-%{sname}/%{sname}-sbom.json.asc + +%changelog +* Tue Apr 28 2026 Muhammad Aqeel - 2.0.0 +- Initial RPM package of ACE +