From 0db8553ac33ace003b84fc202296afe26018ebb3 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 23 Jun 2026 13:26:06 +0500 Subject: [PATCH 1/7] Add per-distro rpm/deb via nfpm and attach to the GitHub release; normalize pre-release version (rc.1->rc1) and pin deb gzip for reprepro --- .circleci/config.yml | 25 +++++++++++++++++++--- .goreleaser.yaml | 50 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f12d5144..3afe22b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,9 +128,28 @@ jobs: # initialize the ci buildx builder make buildx-init - # create the github release - GORELEASER_CURRENT_TAG=${CIRCLE_TAG} goreleaser release \ - --release-notes=${release_notes} + # pgEdge packages use the rcN/betaN convention (no dot), matching + # pgedge-parse-release-tag. goreleaser renders the tag's pre-release + # verbatim (v1.0.0-rc.1 -> 1.0.0~rc.1), so normalise it for the + # PACKAGES while keeping the GitHub release, binaries and docker + # image on the canonical (dotted) tag. + PKG_TAG=$(echo "${CIRCLE_TAG}" | sed -E 's/-(rc|beta|alpha|test|dev)\.([0-9]+)/-\1\2/') + + if [[ "${PKG_TAG}" == "${CIRCLE_TAG}" ]]; then + # GA or already-canonical tag — one run publishes everything, + # packages included. + GORELEASER_CURRENT_TAG=${CIRCLE_TAG} goreleaser release \ + --release-notes=${release_notes} + else + # Publish the release on the canonical tag WITHOUT the packages... + GORELEASER_CURRENT_TAG=${CIRCLE_TAG} goreleaser release \ + --release-notes=${release_notes} --skip=nfpm + # ...then rebuild only the rpm/deb with the normalised version and + # attach them to that same release (gh auth via $GITHUB_TOKEN). + GORELEASER_CURRENT_TAG=${PKG_TAG} goreleaser release \ + --clean --skip=publish,announce,validate,sbom + gh release upload "${CIRCLE_TAG}" dist/*.rpm dist/*.deb --clobber + fi # log into control plane ECR repo echo "${IMAGE_PUBLISH_TOKEN}" \ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index cff77a6d..4c4715a1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -26,17 +26,28 @@ sboms: documents: - '${artifact}.spdx.json' +# rpm/deb packages — one nfpm entry per target distro. The binary is static +# (CGO_ENABLED=0), so the SAME cross-compiled amd64/arm64 binaries are packaged +# for every distro; we only stamp a per-distro tag (rpm Release .el9/.el10; deb +# codename in the revision, e.g. 1.noble). Common fields live on the el9 anchor +# and merge into the rest; each entry overrides only id / formats / release. +# The VERSION comes from the (normalised) tag the CircleCI release job passes to +# goreleaser, e.g. 1.0.0~rc1 — both formats carry '~' for pre-releases. nfpms: - - package_name: pgedge-control-plane + - &nfpm_defaults + id: el9 + package_name: pgedge-control-plane vendor: pgEdge homepage: https://www.pgedge.com maintainer: pgEdge Support description: pgEdge Control Plane license: PostgreSQL License - formats: - - rpm - - deb + file_name_template: "{{ .ConventionalFileName }}" bindir: /usr/sbin + # Force gzip for the .deb members — reprepro can't read zstd; mirrors the + # enterprise debhelper convention `dh_builddeb -- -Zgzip`. + deb: + compression: gzip scripts: preremove: packaging/preremove.sh overrides: @@ -53,6 +64,37 @@ nfpms: - src: packaging/config.json dst: /etc/pgedge-control-plane/config.json type: config|noreplace + formats: [rpm] + release: "1.el9" + - <<: *nfpm_defaults + id: el10 + release: "1.el10" + # --- DEB (codename in the revision) --- + - <<: *nfpm_defaults + id: jammy + formats: [deb] + release: "1.jammy" + - <<: *nfpm_defaults + id: noble + formats: [deb] + release: "1.noble" + - <<: *nfpm_defaults + id: bullseye + formats: [deb] + release: "1.bullseye" + - <<: *nfpm_defaults + id: bookworm + formats: [deb] + release: "1.bookworm" + - <<: *nfpm_defaults + id: trixie + formats: [deb] + release: "1.trixie" + # Ubuntu 26.04 LTS — enable when targeted: + # - <<: *nfpm_defaults + # id: resolute + # formats: [deb] + # release: "1.resolute" release: github: From 3464475052a7a8a2b3d374007edd717abb1ee4e0 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 23 Jun 2026 13:53:48 +0500 Subject: [PATCH 2/7] Embed per-format-signed SBOM in packages: syft + gpg-sign with RPM/APT keys (base64 CircleCI secrets), via nfpms overrides --- .circleci/config.yml | 29 +++++++++++++++++++++++++++++ .goreleaser.yaml | 35 ++++++++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3afe22b7..d8dc0623 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -128,6 +128,35 @@ jobs: # initialize the ci buildx builder make buildx-init + # --- Generate + per-format-sign the SBOM embedded in every package. + # Signed once with the RPM key and once with the APT/DEB key; + # goreleaser embeds the matching .asc per format (see nfpms overrides + # in .goreleaser.yaml). + if [[ -z "${GPG_FIPS_RPM_PRIVATE_KEY:-}" || -z "${GPG_FIPS_DEB_PRIVATE_KEY:-}" ]]; then + echo "GPG_FIPS_RPM_PRIVATE_KEY and GPG_FIPS_DEB_PRIVATE_KEY must be set (control-plane-release context)" + exit 1 + fi + SYFT_VERSION=v1.45.1 + curl -sSfL "https://raw.githubusercontent.com/anchore/syft/${SYFT_VERSION}/install.sh" \ + | sudo sh -s -- -b /usr/local/bin "${SYFT_VERSION}" + # The two secrets hold the base64 of the armored private keys + # (single line — CircleCI mangles multi-line values), so decode them. + keyid() { # fingerprint of a base64 key blob, read in an isolated keyring + local d; d=$(mktemp -d) + echo "$1" | base64 -d | GNUPGHOME="$d" gpg --batch --import >/dev/null 2>&1 + GNUPGHOME="$d" gpg --batch --list-secret-keys --with-colons 2>/dev/null | awk -F: '/^sec/{print $5; exit}' + rm -rf "$d" + } + RPM_KEY_ID=$(keyid "${GPG_FIPS_RPM_PRIVATE_KEY}") + DEB_KEY_ID=$(keyid "${GPG_FIPS_DEB_PRIVATE_KEY}") + echo "${GPG_FIPS_RPM_PRIVATE_KEY}" | base64 -d | gpg --batch --import + echo "${GPG_FIPS_DEB_PRIVATE_KEY}" | base64 -d | gpg --batch --import + mkdir -p .sbom + syft dir:. -o cyclonedx-json > /tmp/cp-sbom.json + mv /tmp/cp-sbom.json .sbom/sbom.json + gpg --batch --yes --armor --detach-sign --local-user "${RPM_KEY_ID}" -o .sbom/sbom.rpm.asc .sbom/sbom.json + gpg --batch --yes --armor --detach-sign --local-user "${DEB_KEY_ID}" -o .sbom/sbom.deb.asc .sbom/sbom.json + # pgEdge packages use the rcN/betaN convention (no dot), matching # pgedge-parse-release-tag. goreleaser renders the tag's pre-release # verbatim (v1.0.0-rc.1 -> 1.0.0~rc.1), so normalise it for the diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4c4715a1..7d591a88 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -50,20 +50,41 @@ nfpms: compression: gzip scripts: preremove: packaging/preremove.sh + # goreleaser REPLACES (not merges) `contents` in a per-format override, so + # the full file list is repeated under rpm/deb. This is deliberate: it lets + # each format embed the SBOM signature made with ITS OWN key at the same + # path — RPM key for rpms, APT/DEB key for debs. The CircleCI release job + # generates .sbom/sbom.json and signs it twice (.sbom/sbom.rpm.asc, + # .sbom/sbom.deb.asc) before goreleaser runs. overrides: rpm: scripts: postinstall: packaging/rpm/postinstall.sh + contents: + - src: packaging/pgedge-control-plane.service + dst: /usr/lib/systemd/system/pgedge-control-plane.service + type: config + - src: packaging/config.json + dst: /etc/pgedge-control-plane/config.json + type: config|noreplace + - src: .sbom/sbom.json + dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json + - src: .sbom/sbom.rpm.asc + dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json.asc deb: scripts: postinstall: packaging/deb/postinstall.sh - contents: - - src: packaging/pgedge-control-plane.service - dst: /usr/lib/systemd/system/pgedge-control-plane.service - type: config - - src: packaging/config.json - dst: /etc/pgedge-control-plane/config.json - type: config|noreplace + contents: + - src: packaging/pgedge-control-plane.service + dst: /usr/lib/systemd/system/pgedge-control-plane.service + type: config + - src: packaging/config.json + dst: /etc/pgedge-control-plane/config.json + type: config|noreplace + - src: .sbom/sbom.json + dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json + - src: .sbom/sbom.deb.asc + dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json.asc formats: [rpm] release: "1.el9" - <<: *nfpm_defaults From bdfb0826e36ef1c00fc288ea8ddf231dba320d59 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 23 Jun 2026 14:08:54 +0500 Subject: [PATCH 3/7] Sign RPMs with the RPM key before release upload (Ubuntu __gpg fix) --- .circleci/config.yml | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d8dc0623..0fa7c0e6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -136,6 +136,8 @@ jobs: echo "GPG_FIPS_RPM_PRIVATE_KEY and GPG_FIPS_DEB_PRIVATE_KEY must be set (control-plane-release context)" exit 1 fi + # rpm provides rpmsign (used to sign the rpms below) + sudo apt-get update -qq && sudo apt-get install -y -qq rpm SYFT_VERSION=v1.45.1 curl -sSfL "https://raw.githubusercontent.com/anchore/syft/${SYFT_VERSION}/install.sh" \ | sudo sh -s -- -b /usr/local/bin "${SYFT_VERSION}" @@ -164,22 +166,32 @@ jobs: # image on the canonical (dotted) tag. PKG_TAG=$(echo "${CIRCLE_TAG}" | sed -E 's/-(rc|beta|alpha|test|dev)\.([0-9]+)/-\1\2/') - if [[ "${PKG_TAG}" == "${CIRCLE_TAG}" ]]; then - # GA or already-canonical tag — one run publishes everything, - # packages included. - GORELEASER_CURRENT_TAG=${CIRCLE_TAG} goreleaser release \ - --release-notes=${release_notes} - else - # Publish the release on the canonical tag WITHOUT the packages... - GORELEASER_CURRENT_TAG=${CIRCLE_TAG} goreleaser release \ - --release-notes=${release_notes} --skip=nfpm - # ...then rebuild only the rpm/deb with the normalised version and - # attach them to that same release (gh auth via $GITHUB_TOKEN). - GORELEASER_CURRENT_TAG=${PKG_TAG} goreleaser release \ - --clean --skip=publish,announce,validate,sbom - gh release upload "${CIRCLE_TAG}" dist/*.rpm dist/*.deb --clobber + # Publish the release on the canonical tag WITHOUT packages (binaries, + # archives, SBOM); then build the rpm/deb with the normalised version + # (PKG_TAG == CIRCLE_TAG for GA). Packages are built --skip=publish so + # the rpms can be signed before they are attached to the release. + GORELEASER_CURRENT_TAG=${CIRCLE_TAG} goreleaser release \ + --release-notes=${release_notes} --skip=nfpm + GORELEASER_CURRENT_TAG=${PKG_TAG} goreleaser release \ + --clean --skip=publish,announce,validate,sbom + + # Sign the rpms with the RPM key (debs are signed later at the apt + # repo by reprepro, not per-file). rpm's default %__gpg is gpg2, + # which is absent on Ubuntu — point it at the real gpg. + for r in dist/*.rpm; do + rpmsign --define "__gpg $(command -v gpg)" \ + --define "_gpg_name ${RPM_KEY_ID}" --addsign "$r" + done + # Verify the rpm signatures when the public key is provided. + if [[ -n "${GPG_FIPS_RPM_PUBLIC_KEY:-}" ]]; then + echo "${GPG_FIPS_RPM_PUBLIC_KEY}" | base64 -d > /tmp/rpm-pub.asc + sudo rpm --import /tmp/rpm-pub.asc + for r in dist/*.rpm; do rpm --checksig "$r"; done fi + # Attach the signed rpms + debs to the canonical-tag release. + gh release upload "${CIRCLE_TAG}" dist/*.rpm dist/*.deb --clobber + # log into control plane ECR repo echo "${IMAGE_PUBLISH_TOKEN}" \ | docker login ghcr.io --username "${IMAGE_PUBLISH_USER}" --password-stdin From 32d8c65386566c3b4348ad22ba633930376c3a97 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 23 Jun 2026 18:48:28 +0500 Subject: [PATCH 4/7] Dedupe nfpm package contents via YAML anchors (only the per-format SBOM signature differs) --- .goreleaser.yaml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 7d591a88..1ed15011 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -56,18 +56,25 @@ nfpms: # path — RPM key for rpms, APT/DEB key for debs. The CircleCI release job # generates .sbom/sbom.json and signs it twice (.sbom/sbom.rpm.asc, # .sbom/sbom.deb.asc) before goreleaser runs. + # The common files are defined once via YAML anchors (goreleaser REPLACES + # contents per format override, so it can't be shared at the top level — + # but anchors resolve at parse time, so each format still gets the full + # list). Only the SBOM signature differs: RPM key for rpms, APT key for debs. overrides: rpm: scripts: postinstall: packaging/rpm/postinstall.sh contents: - - src: packaging/pgedge-control-plane.service + - &svc_content + src: packaging/pgedge-control-plane.service dst: /usr/lib/systemd/system/pgedge-control-plane.service type: config - - src: packaging/config.json + - &cfg_content + src: packaging/config.json dst: /etc/pgedge-control-plane/config.json type: config|noreplace - - src: .sbom/sbom.json + - &sbom_content + src: .sbom/sbom.json dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json - src: .sbom/sbom.rpm.asc dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json.asc @@ -75,14 +82,9 @@ nfpms: scripts: postinstall: packaging/deb/postinstall.sh contents: - - src: packaging/pgedge-control-plane.service - dst: /usr/lib/systemd/system/pgedge-control-plane.service - type: config - - src: packaging/config.json - dst: /etc/pgedge-control-plane/config.json - type: config|noreplace - - src: .sbom/sbom.json - dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json + - *svc_content + - *cfg_content + - *sbom_content - src: .sbom/sbom.deb.asc dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json.asc formats: [rpm] From 95d3e6d359db664f8969253a83691d71ded8453a Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Tue, 23 Jun 2026 19:03:56 +0500 Subject: [PATCH 5/7] Share nfpm contents at top level; select per-format SBOM signature via {{ .Format }} --- .goreleaser.yaml | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 1ed15011..ded4ea3c 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -56,37 +56,29 @@ nfpms: # path — RPM key for rpms, APT/DEB key for debs. The CircleCI release job # generates .sbom/sbom.json and signs it twice (.sbom/sbom.rpm.asc, # .sbom/sbom.deb.asc) before goreleaser runs. - # The common files are defined once via YAML anchors (goreleaser REPLACES - # contents per format override, so it can't be shared at the top level — - # but anchors resolve at parse time, so each format still gets the full - # list). Only the SBOM signature differs: RPM key for rpms, APT key for debs. + # Contents are shared at the top level. The SBOM signature is selected per + # format via {{ .Format }} (rpm -> sbom.rpm.asc, deb -> sbom.deb.asc) — + # goreleaser templates contents.src, so no per-format contents override is + # needed. Only the postinstall script genuinely differs per format + # (rpm/ vs deb/), and that path can't be templated, so it stays in overrides. overrides: rpm: scripts: postinstall: packaging/rpm/postinstall.sh - contents: - - &svc_content - src: packaging/pgedge-control-plane.service - dst: /usr/lib/systemd/system/pgedge-control-plane.service - type: config - - &cfg_content - src: packaging/config.json - dst: /etc/pgedge-control-plane/config.json - type: config|noreplace - - &sbom_content - src: .sbom/sbom.json - dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json - - src: .sbom/sbom.rpm.asc - dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json.asc deb: scripts: postinstall: packaging/deb/postinstall.sh - contents: - - *svc_content - - *cfg_content - - *sbom_content - - src: .sbom/sbom.deb.asc - dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json.asc + contents: + - src: packaging/pgedge-control-plane.service + dst: /usr/lib/systemd/system/pgedge-control-plane.service + type: config + - src: packaging/config.json + dst: /etc/pgedge-control-plane/config.json + type: config|noreplace + - src: .sbom/sbom.json + dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json + - src: ".sbom/sbom.{{ .Format }}.asc" + dst: /usr/share/pgedge-control-plane/pgedge-control-plane-sbom.json.asc formats: [rpm] release: "1.el9" - <<: *nfpm_defaults From ff402fe0f4178f57dec17d524f21605a0969b799 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Wed, 24 Jun 2026 17:59:50 +0500 Subject: [PATCH 6/7] Add package publishing pipeline: draft release in CircleCI, publish to dnf/apt repos via GitHub workflow --- .circleci/config.yml | 6 +- .github/workflows/publish.yml | 389 ++++++++++++++++++++++++++++++++++ .goreleaser.yaml | 5 + 3 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/publish.yml diff --git a/.circleci/config.yml b/.circleci/config.yml index 0fa7c0e6..ab231f5b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -189,8 +189,12 @@ jobs: for r in dist/*.rpm; do rpm --checksig "$r"; done fi - # Attach the signed rpms + debs to the canonical-tag release. + # Attach the signed rpms + debs to the (draft) release, then publish + # it. Flipping draft -> published fires the 'release: published' + # event, which triggers .github/workflows/publish.yml with all the + # signed packages already present (no race). gh release upload "${CIRCLE_TAG}" dist/*.rpm dist/*.deb --clobber + gh release edit "${CIRCLE_TAG}" --draft=false # log into control plane ECR repo echo "${IMAGE_PUBLISH_TOKEN}" \ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..cc2e1be3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,389 @@ +# ------------------------------------------------------------------------- +# +# pgEdge Control Plane — package publishing +# +# control-plane builds + signs its rpm/deb packages in CircleCI and attaches +# them to the GitHub release. This workflow fires when that release is +# published, downloads the signed packages from the release, and runs the +# pgEdge publish pipeline (same building blocks as ai-kb / ai-dba-workbench / +# anonymizer): +# determine-repo-type → push-dnf → push-apt → publish-manifest (+ Slack) +# +# It does NOT build anything. Differences from the in-repo repos: +# * packages come from the published release (gh release download), not a +# same-run build artifact; +# * no pgedge-detect-build-matrix (that needs a component dir with common.sh) +# — the per-distro lists are derived from the downloaded filenames and the +# dnf_ts/apt_ts timestamps are generated inline. +# +# RPMs are already signed in CircleCI; DEBs are signed at the apt repo here +# (reprepro, DEB key). pgedge-dnf-repo-builder only runs createrepo_c. +# +# ------------------------------------------------------------------------- + +name: Publish + +on: + release: + types: [published] + workflow_dispatch: {} # manual re-publish — run against the release tag + +permissions: + contents: read + +concurrency: + group: publish-${{ github.ref_name }} + cancel-in-progress: false + +env: + TAG: ${{ github.event.release.tag_name || github.ref_name }} + +jobs: + # ========================================================================= + # Tag parsing → (repo_type, version, buildnum). parse-release-tag reads + # GITHUB_REF_NAME (the release tag) — no simulate_tag, so simulated=false. + # ========================================================================= + determine-repo-type: + name: Determine repo routing + runs-on: ubuntu-latest + 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 + - 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" + + # ========================================================================= + # Push RPMs → dnf.pgedge.com (createrepo_c + S3 + CloudFront). RPMs are + # already signed in CircleCI; this job adds no GPG key. + # ========================================================================= + push-dnf: + name: Push DNF (all EL majors × archs, batched) + needs: [determine-repo-type] + if: needs.determine-repo-type.result == 'success' + runs-on: ubuntu-latest + concurrency: + group: dnf-push-${{ needs.determine-repo-type.outputs.repo-type }} + cancel-in-progress: false + outputs: + timestamp: ${{ steps.ts.outputs.dnf_ts }} + cells: ${{ steps.targets.outputs.cells }} + backups: ${{ steps.targets.outputs.backups }} + 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 }} + steps: + - name: Timestamp + id: ts + run: echo "dnf_ts=$(date -u +'%Y%m%d-%H%M%S')" >> "$GITHUB_OUTPUT" + - name: Download RPMs from the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + mkdir -p rpm-dl + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.rpm' --dir rpm-dl + ls -la rpm-dl + - name: Organize RPMs by EL major + build targets/cells + id: targets + env: + DNF_TS: ${{ steps.ts.outputs.dnf_ts }} + run: | + set -euo pipefail + push='[]'; backups='[]'; cells='[]' + # Group the downloaded rpms by their .elN. dist tag. + for el in $(ls rpm-dl/*.rpm | grep -oE 'el[0-9]+' | sort -u); do + osv="${el#el}"; dest="rpms-${el}"; mkdir -p "$dest" + mv rpm-dl/*."${el}".*.rpm "$dest"/ + echo "EL${osv}: $(ls "$dest" | wc -l) rpms" + push="$(jq -c --arg v "$osv" --arg d "$dest" '. + [{"os-version":$v,"rpm-dir":$d}]' <<<"$push")" + backups="$(jq -c --arg v "$osv" --arg d "$dest" '. + [{"os-version":$v,"output-dir":$d}]' <<<"$backups")" + bp="${REPO_TYPE}/${COMPONENT_NAME}/${COMPONENT_VERSION}/${DNF_TS}/dnf/${osv}/" + for f in "$dest"/*.rpm; do + arch="$(basename "$f" | sed -E 's/.*\.([^.]+)\.rpm$/\1/')" + cells="$(jq -c --arg comp "$COMPONENT_NAME" --arg cv "$COMPONENT_VERSION" --arg img "almalinux:${osv}" --arg arch "$arch" --arg osv "$osv" --arg bp "$bp" --arg ts "$DNF_TS" --arg rt "$REPO_TYPE" \ + '. + [{family:"rpm",component:$comp,component_version:$cv,image:$img,arch:$arch,os_version:$osv,backup_path:$bp,timestamp:$ts,repo_type:$rt}]' <<<"$cells")" + done + done + rm -rf rpm-dl + echo "push=$push" >> "$GITHUB_OUTPUT" + echo "backups=$backups" >> "$GITHUB_OUTPUT" + echo "cells=$cells" >> "$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: ${{ steps.ts.outputs.dnf_ts }} + targets: ${{ steps.targets.outputs.backups }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + # ========================================================================= + # Push DEBs → apt.pgedge.com (reprepro signs the repo with the DEB key). + # ========================================================================= + push-apt: + name: Push APT (all distros × archs, batched) + needs: [determine-repo-type] + if: needs.determine-repo-type.result == 'success' + runs-on: ubuntu-latest + concurrency: + group: apt-push-${{ needs.determine-repo-type.outputs.repo-type }} + cancel-in-progress: false + outputs: + timestamp: ${{ steps.ts.outputs.apt_ts }} + cells: ${{ steps.targets.outputs.cells }} + backups: ${{ steps.targets.outputs.backups }} + 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 }} + steps: + - name: Timestamp + id: ts + run: echo "apt_ts=$(date -u +'%Y%m%d-%H%M%S')" >> "$GITHUB_OUTPUT" + - name: Download DEBs from the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + mkdir -p deb-dl + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.deb' --dir deb-dl + ls -la deb-dl + - name: Organize DEBs by codename + build targets/cells + id: targets + env: + APT_TS: ${{ steps.ts.outputs.apt_ts }} + run: | + set -euo pipefail + push='[]'; backups='[]'; cells='[]' + # codename lives in the deb revision: ..._-1._.deb + for code in $(ls deb-dl/*.deb | sed -E 's/.*-1\.([a-z]+)_[a-z0-9]+\.deb$/\1/' | sort -u); do + case "$code" in + jammy|noble) image="ubuntu:${code}" ;; + bullseye|bookworm|trixie) image="debian:${code}" ;; + *) echo "::error::unknown codename '${code}'"; exit 1 ;; + esac + dest="debs-${code}"; mkdir -p "$dest" + mv deb-dl/*-1."${code}"_*.deb "$dest"/ + echo "${code}: $(ls "$dest" | wc -l) debs" + push="$(jq -c --arg n "$image" --arg d "$dest" '. + [{"os-name":$n,"deb-dir":$d}]' <<<"$push")" + backups="$(jq -c --arg v "$code" --arg d "$dest" '. + [{"os-version":$v,"output-dir":$d}]' <<<"$backups")" + bp="${REPO_TYPE}/${COMPONENT_NAME}/${COMPONENT_VERSION}/${APT_TS}/apt/${code}/" + for f in "$dest"/*.deb; do + arch="$(basename "$f" | sed -E 's/.*_([a-z0-9]+)\.deb$/\1/')" + cells="$(jq -c --arg comp "$COMPONENT_NAME" --arg cv "$COMPONENT_VERSION" --arg img "$image" --arg arch "$arch" --arg distro "$code" --arg bp "$bp" --arg ts "$APT_TS" --arg rt "$REPO_TYPE" \ + '. + [{family:"deb",component:$comp,component_version:$cv,image:$img,arch:$arch,distro:$distro,backup_path:$bp,timestamp:$ts,repo_type:$rt}]' <<<"$cells")" + done + done + rm -rf deb-dl + echo "push=$push" >> "$GITHUB_OUTPUT" + echo "backups=$backups" >> "$GITHUB_OUTPUT" + echo "cells=$cells" >> "$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: ${{ steps.ts.outputs.apt_ts }} + targets: ${{ steps.targets.outputs.backups }} + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + # ========================================================================= + # Aggregated manifest (cells[] for qa-certify/promote) + Slack. + # ========================================================================= + publish-manifest: + name: Publish aggregated backup manifest + needs: [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 }} + DNF_CELLS: ${{ needs.push-dnf.outputs.cells }} + APT_CELLS: ${{ needs.push-apt.outputs.cells }} + run: | + set -euo pipefail + mkdir -p aggregated + cells='[]'; dnf_backups='[]'; apt_backups='[]' + if [ "${PUSH_DNF_RESULT}" = "success" ] && [ -n "${DNF_CELLS:-}" ]; then + cells="$(jq -c --argjson a "$cells" --argjson b "${DNF_CELLS}" -n '$a + $b')" + dnf_backups="$(jq -c '[.[].backup_path] | unique' <<<"${DNF_CELLS}")" + fi + if [ "${PUSH_APT_RESULT}" = "success" ] && [ -n "${APT_CELLS:-}" ]; then + cells="$(jq -c --argjson a "$cells" --argjson b "${APT_CELLS}" -n '$a + $b')" + apt_backups="$(jq -c '[.[].backup_path] | unique' <<<"${APT_CELLS}")" + 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: 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 }} + REPO_TYPE: ${{ needs.determine-repo-type.outputs.repo-type }} + PUSH_DNF_RESULT: ${{ needs.push-dnf.result }} + PUSH_APT_RESULT: ${{ needs.push-apt.result }} + 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="${COMPONENT_NAME} <${REPO_URL}/releases/tag/${EFFECTIVE_TAG}|${EFFECTIVE_TAG}>" + s=0; f=0; k=0 + for r in "${PUSH_DNF_RESULT}" "${PUSH_APT_RESULT}"; do + case "$r" in success) s=$((s+1));; skipped) k=$((k+1));; *) f=$((f+1));; esac + done + total=$((s+f+k)) + if [ "$f" -eq 0 ] && [ "$k" -eq 0 ]; then status="Success"; elif [ "$s" -gt 0 ]; then status="Partial"; elif [ "$k" -eq "$total" ]; then status="Skipped"; else status="Failed"; fi + 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+"`")}]') + 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<<__S__"; echo "${subject}"; echo "__S__"; echo "fields<<__F__"; echo "${fields}"; echo "__F__"; echo "result_fields<<__RF__"; echo "${result_fields}"; echo "__RF__"; echo "code_sections<<__CS__"; echo "${code_sections}"; echo "__CS__"; } >> "$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.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/.goreleaser.yaml b/.goreleaser.yaml index ded4ea3c..51b41a34 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -116,6 +116,11 @@ release: owner: pgEdge name: control-plane prerelease: auto + # Create the release as a DRAFT so the 'release: published' event (which + # triggers .github/workflows/publish.yml) does NOT fire until the signed + # rpm/deb packages are attached. CircleCI flips it to published at the end + # (gh release edit --draft=false), after the packages are uploaded. + draft: true footer: | Get the Docker image for this release from: From c8458eeebd446e909611d8380c81438859888c90 Mon Sep 17 00:00:00 2001 From: Muhammad Aqeel Date: Fri, 3 Jul 2026 19:19:08 +0500 Subject: [PATCH 7/7] Address PR review: verify syft checksum, pass clone token via header, fail publish on empty targets --- .circleci/config.yml | 17 ++++++++++++--- .github/workflows/publish.yml | 41 ++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ab231f5b..bb0a91fd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -138,9 +138,20 @@ jobs: fi # rpm provides rpmsign (used to sign the rpms below) sudo apt-get update -qq && sudo apt-get install -y -qq rpm - SYFT_VERSION=v1.45.1 - curl -sSfL "https://raw.githubusercontent.com/anchore/syft/${SYFT_VERSION}/install.sh" \ - | sudo sh -s -- -b /usr/local/bin "${SYFT_VERSION}" + # Install syft from the pinned release tarball and verify it against + # the published checksums file before installing (avoids piping a + # remote installer script straight into a shell). + SYFT_VERSION=1.45.1 + SYFT_TGZ="syft_${SYFT_VERSION}_linux_amd64.tar.gz" + SYFT_BASE="https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}" + syft_tmp="$(mktemp -d)" + curl -sSfL "${SYFT_BASE}/${SYFT_TGZ}" -o "${syft_tmp}/${SYFT_TGZ}" + curl -sSfL "${SYFT_BASE}/syft_${SYFT_VERSION}_checksums.txt" -o "${syft_tmp}/checksums.txt" + ( cd "${syft_tmp}" && grep " ${SYFT_TGZ}\$" checksums.txt | sha256sum -c - ) + tar -xzf "${syft_tmp}/${SYFT_TGZ}" -C "${syft_tmp}" syft + sudo install -m 0755 "${syft_tmp}/syft" /usr/local/bin/syft + rm -rf "${syft_tmp}" + syft version # The two secrets hold the base64 of the armored private keys # (single line — CircleCI mangles multi-line values), so decode them. keyid() { # fingerprint of a base64 key blob, read in an isolated keyring diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cc2e1be3..d5affc39 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -59,9 +59,14 @@ jobs: 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" \ + # Pass the token via a transient auth header (kept out of the + # remote URL) and drop .git after clone, so the credential does + # not persist in the workspace for later steps to read. + auth="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')" + git -c "http.extraheader=${auth}" clone --depth 1 \ + "https://github.com/pgEdge/pgedge-parse-release-tag.git" \ ".github/actions/pgedge-parse-release-tag" + rm -rf ".github/actions/pgedge-parse-release-tag/.git" - name: Parse release tag id: parse uses: ./.github/actions/pgedge-parse-release-tag @@ -138,6 +143,10 @@ jobs: done done rm -rf rpm-dl + if [ "$(jq 'length' <<<"$push")" -eq 0 ]; then + echo "::error::no RPM publish targets derived from the release assets (no *.rpm attached, or none matched the .elN. naming)" + exit 1 + fi echo "push=$push" >> "$GITHUB_OUTPUT" echo "backups=$backups" >> "$GITHUB_OUTPUT" echo "cells=$cells" >> "$GITHUB_OUTPUT" @@ -147,9 +156,13 @@ jobs: run: | set -euo pipefail mkdir -p .github/actions + # Token via transient auth header; drop .git after clone so the + # credential does not persist in the workspace. + auth="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')" 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}" + git -c "http.extraheader=${auth}" clone --depth 1 --branch multi-target \ + "https://github.com/pgEdge/${repo}.git" ".github/actions/${repo}" + rm -rf ".github/actions/${repo}/.git" done - name: Push RPMs to yum repo (all EL majors batched) uses: ./.github/actions/pgedge-dnf-repo-builder @@ -231,6 +244,10 @@ jobs: done done rm -rf deb-dl + if [ "$(jq 'length' <<<"$push")" -eq 0 ]; then + echo "::error::no DEB publish targets derived from the release assets (no *.deb attached, or none matched the -1._ naming)" + exit 1 + fi echo "push=$push" >> "$GITHUB_OUTPUT" echo "backups=$backups" >> "$GITHUB_OUTPUT" echo "cells=$cells" >> "$GITHUB_OUTPUT" @@ -240,9 +257,13 @@ jobs: run: | set -euo pipefail mkdir -p .github/actions + # Token via transient auth header; drop .git after clone so the + # credential does not persist in the workspace. + auth="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')" 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}" + git -c "http.extraheader=${auth}" clone --depth 1 --branch multi-target \ + "https://github.com/pgEdge/${repo}.git" ".github/actions/${repo}" + rm -rf ".github/actions/${repo}/.git" done - name: Push DEBs to apt repo (all distros batched) uses: ./.github/actions/pgedge-apt-repo-builder @@ -331,9 +352,13 @@ jobs: run: | set -euo pipefail mkdir -p .github/actions - git clone --depth 1 \ - "https://x-access-token:${TOKEN}@github.com/pgEdge/pgedge-build-publisher.git" \ + # Token via transient auth header; drop .git after clone so the + # credential does not persist in the workspace. + auth="AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')" + git -c "http.extraheader=${auth}" clone --depth 1 \ + "https://github.com/pgEdge/pgedge-build-publisher.git" \ ".github/actions/pgedge-build-publisher" + rm -rf ".github/actions/pgedge-build-publisher/.git" - name: Compose Slack fields id: compose_slack env: