Skip to content

Prepare Or Publish Packages #14

Prepare Or Publish Packages

Prepare Or Publish Packages #14

Workflow file for this run

name: Prepare Or Publish Packages
on:
workflow_dispatch:
inputs:
mode:
description: Prepare builds before release, or publish prepared artifacts after release
required: true
type: choice
options:
- prepare
- publish
tag:
description: Intended or existing release tag in vX.Y.Z form
required: true
type: string
source_ref:
description: Full 40-character commit SHA for prepare mode
required: false
default: ""
type: string
prepare_run_id:
description: Successful prepare workflow run ID for publish mode
required: false
type: string
permissions:
contents: read
actions: read
env:
IMAGE_NAME: ghcr.io/evokoa/pggraph
jobs:
validate:
name: Validate release
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.release.outputs.mode }}
tag: ${{ steps.release.outputs.tag }}
version: ${{ steps.release.outputs.version }}
short_sha: ${{ steps.commit.outputs.short_sha }}
full_sha: ${{ steps.commit.outputs.full_sha }}
source_ref: ${{ steps.release.outputs.source_ref }}
steps:
- name: Resolve release input
id: release
run: |
mode="${{ inputs.mode }}"
tag="${{ inputs.tag }}"
source_ref="${{ inputs.source_ref }}"
if [[ "$mode" != "prepare" && "$mode" != "publish" ]]; then
echo "Mode must be prepare or publish: $mode" >&2
exit 1
fi
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Release tag must use vX.Y.Z form: $tag" >&2
exit 1
fi
if [[ "$mode" == "prepare" && -z "$source_ref" ]]; then
echo "Prepare mode requires source_ref" >&2
exit 1
fi
if [[ "$mode" == "prepare" && ! "$source_ref" =~ ^[0-9a-f]{40}$ ]]; then
echo "Prepare source_ref must be a full 40-character commit SHA" >&2
exit 1
fi
if [[ "$mode" == "publish" && -z "${{ inputs.prepare_run_id }}" ]]; then
echo "Publish mode requires prepare_run_id" >&2
exit 1
fi
if [[ "$mode" == "publish" && ! "${{ inputs.prepare_run_id }}" =~ ^[0-9]+$ ]]; then
echo "Publish prepare_run_id must be a numeric workflow run ID" >&2
exit 1
fi
echo "mode=$mode" >> "$GITHUB_OUTPUT"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
echo "source_ref=$source_ref" >> "$GITHUB_OUTPUT"
- name: Verify GitHub Release exists
if: steps.release.outputs.mode == 'publish'
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh release view "${{ steps.release.outputs.tag }}" >/dev/null
- name: Checkout release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ steps.release.outputs.mode == 'prepare' && steps.release.outputs.source_ref || steps.release.outputs.tag }}
fetch-depth: 0
- name: Validate preparation source
if: steps.release.outputs.mode == 'prepare'
run: |
commit="$(git rev-parse HEAD)"
test "${{ github.sha }}" = "$commit"
scripts/validate_release.py \
--version "${{ steps.release.outputs.version }}" \
--ref "$commit" --check-main --require-clean
- name: Validate signed publication tag
if: steps.release.outputs.mode == 'publish'
run: |
test "${{ github.sha }}" = "$(git rev-parse HEAD)"
scripts/validate_release.py \
--tag "${{ steps.release.outputs.tag }}" \
--check-main --require-clean --require-signed-tag
- name: Authenticate preparation workflow run
if: steps.release.outputs.mode == 'publish'
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
EXPECTED_SHA: ${{ github.sha }}
RUN_ID: ${{ inputs.prepare_run_id }}
run: |
gh api "repos/$GH_REPO/actions/runs/$RUN_ID" > /tmp/prepare-run.json
jq -e \
--arg repo "$GH_REPO" \
--arg sha "$EXPECTED_SHA" \
--arg workflow ".github/workflows/release.yml" \
'.repository.full_name == $repo and
.head_repository.full_name == $repo and
.event == "workflow_dispatch" and
.status == "completed" and
.conclusion == "success" and
.head_sha == $sha and
(.path | split("@")[0]) == $workflow' \
/tmp/prepare-run.json >/dev/null
- name: Capture release commit
id: commit
run: |
echo "short_sha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT"
echo "full_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
release-bundle:
name: Build source release bundle
if: needs.validate.outputs.mode == 'prepare'
needs: validate
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
steps:
- name: Checkout release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.validate.outputs.full_sha }}
fetch-depth: 0
- name: Build deterministic release bundle
run: |
scripts/prepare_release_bundle.py \
--version "${{ needs.validate.outputs.version }}" \
--ref "${{ needs.validate.outputs.full_sha }}" \
--out-dir dist
- name: Verify release bundle
run: |
scripts/verify_release_bundle.py dist \
--version "${{ needs.validate.outputs.version }}" \
--commit "${{ needs.validate.outputs.full_sha }}"
- name: Attest release bundle provenance
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
with:
subject-path: dist/*
- name: Attest source archive SBOM
uses: actions/attest-sbom@10926c72720ffc3f7b666661c8e55b1344e2a365 # v2
with:
subject-path: dist/pgGraph-${{ needs.validate.outputs.version }}.zip
sbom-path: dist/pgGraph-${{ needs.validate.outputs.version }}.spdx.json
- name: Upload workflow artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-bundle-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.short_sha }}
path: dist/
if-no-files-found: error
retention-days: 90
approve-publishing:
name: Approve package publishing
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
runs-on: ubuntu-latest
environment: release
steps:
- name: Approved
run: echo "Approved PGXN and Docker package publishing."
verify-prepared-artifacts:
name: Verify prepared source attestations
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- approve-publishing
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
attestations: read
steps:
- name: Checkout release tag
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.validate.outputs.tag }}
fetch-depth: 0
- name: Download prepared release bundle
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
run-id: ${{ inputs.prepare_run_id }}
pattern: release-bundle-${{ needs.validate.outputs.version }}-*
path: dist
merge-multiple: true
github-token: ${{ github.token }}
- name: Verify bundle and GitHub attestations
env:
GH_TOKEN: ${{ github.token }}
run: |
scripts/verify_release_bundle.py dist \
--version "${{ needs.validate.outputs.version }}" \
--commit "${{ needs.validate.outputs.full_sha }}"
for artifact in dist/*; do
gh attestation verify "$artifact" \
--repo "${{ github.repository }}" \
--signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \
--source-digest "${{ needs.validate.outputs.full_sha }}" \
--predicate-type https://slsa.dev/provenance/v1
done
gh attestation verify \
"dist/pgGraph-${{ needs.validate.outputs.version }}.zip" \
--repo "${{ github.repository }}" \
--signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \
--source-digest "${{ needs.validate.outputs.full_sha }}" \
--predicate-type https://spdx.dev/Document/v2.3
publish-pgxn:
name: Publish PGXN package
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- approve-publishing
- verify-prepared-artifacts
runs-on: ubuntu-latest
container: pgxn/pgxn-tools:v1@sha256:53e79ef1c8b9a0ee88b51e89b36fd30c46c9f2326aec0cde1d7f66e47ad719ff
permissions:
contents: read
actions: read
steps:
- name: Checkout release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.validate.outputs.full_sha }}
- name: Download prepared release bundle
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
run-id: ${{ inputs.prepare_run_id }}
pattern: release-bundle-${{ needs.validate.outputs.version }}-*
path: dist
merge-multiple: true
github-token: ${{ github.token }}
- name: Verify prepared release bundle
run: |
scripts/verify_release_bundle.py dist \
--version "${{ needs.validate.outputs.version }}" \
--commit "${{ needs.validate.outputs.full_sha }}"
- name: Publish to PGXN
env:
PGXN_USERNAME: ${{ secrets.PGXN_USERNAME }}
PGXN_PASSWORD: ${{ secrets.PGXN_PASSWORD }}
run: pgxn-release "dist/pgGraph-${{ needs.validate.outputs.version }}.zip"
pgxn-verify:
name: Verify PGXN package
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- publish-pgxn
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Download prepared release bundle
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
run-id: ${{ inputs.prepare_run_id }}
pattern: release-bundle-${{ needs.validate.outputs.version }}-*
path: dist
merge-multiple: true
github-token: ${{ github.token }}
- name: Verify PGXN metadata is visible
run: |
url="https://api.pgxn.org/dist/pggraph/${{ needs.validate.outputs.version }}/META.json"
for _ in {1..30}; do
if curl --fail --silent --show-error --location "$url" >/tmp/pgxn-meta.json; then
break
fi
sleep 10
done
test -s /tmp/pgxn-meta.json
- name: Verify PGXN version metadata
run: |
python3 - <<'PY'
import json
from pathlib import Path
expected = "${{ needs.validate.outputs.version }}"
meta = json.loads(Path("/tmp/pgxn-meta.json").read_text(encoding="utf-8"))
actual = meta.get("version")
if actual != expected:
raise SystemExit(f"Expected PGXN version {expected}, got {actual!r}")
PY
- name: Verify published PGXN archive bytes
run: |
url="https://api.pgxn.org/dist/pggraph/${{ needs.validate.outputs.version }}/pgGraph-${{ needs.validate.outputs.version }}.zip"
curl --fail --silent --show-error --location "$url" --output /tmp/pggraph-published.zip
expected="$(sha256sum "dist/pgGraph-${{ needs.validate.outputs.version }}.zip" | awk '{print $1}')"
actual="$(sha256sum /tmp/pggraph-published.zip | awk '{print $1}')"
test "$actual" = "$expected"
attach-pgxn-artifact:
name: Attach PGXN artifact to GitHub Release
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- approve-publishing
- verify-prepared-artifacts
- pgxn-verify
runs-on: ubuntu-latest
permissions:
contents: write
actions: read
steps:
- name: Checkout release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.validate.outputs.full_sha }}
fetch-depth: 0
- name: Download prepared release bundle
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
run-id: ${{ inputs.prepare_run_id }}
pattern: release-bundle-${{ needs.validate.outputs.version }}-*
path: dist
merge-multiple: true
github-token: ${{ github.token }}
- name: Verify prepared release bundle
run: |
scripts/verify_release_bundle.py dist \
--version "${{ needs.validate.outputs.version }}" \
--commit "${{ needs.validate.outputs.full_sha }}"
- name: Attach exact bundle to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
gh release upload "${{ needs.validate.outputs.tag }}" \
dist/* --clobber
docker:
name: Prepare Docker image PG${{ matrix.pg }} ${{ matrix.platform.name }}
if: needs.validate.outputs.mode == 'prepare'
needs:
- validate
runs-on: ${{ matrix.platform.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
pg: [14, 15, 16, 17, 18]
platform:
- name: linux/amd64
runner: ubuntu-24.04
pair: linux-amd64
- name: linux/arm64
runner: ubuntu-24.04-arm
pair: linux-arm64
steps:
- name: Checkout release source
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ needs.validate.outputs.full_sha }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=pg${{ matrix.pg }}-sha-${{ needs.validate.outputs.short_sha }}
type=raw,value=pg${{ matrix.pg }}-${{ needs.validate.outputs.tag }}-prepared
type=raw,value=pg${{ matrix.pg }}-${{ needs.validate.outputs.version }}-prepared
labels: |
org.opencontainers.image.version=${{ needs.validate.outputs.version }}
org.opencontainers.image.postgresql.major=${{ matrix.pg }}
org.opencontainers.image.revision=${{ needs.validate.outputs.short_sha }}
- name: Select immutable PostgreSQL base image
id: postgres-base
run: |
case "${{ matrix.pg }}" in
14) image="postgres:14-bookworm@sha256:9062043625415b4628f0ca44c2c65de93778afb7b1db33aff67877cace3b966f" ;;
15) image="postgres:15-bookworm@sha256:b0c5bab0fbba8e0c221f73b1dc6359ec35f8650074377e727299df248fc8ad51" ;;
16) image="postgres:16-bookworm@sha256:92620daddcd947f8d5ab5ba66e848702fe443d87fed30c4cea8e389fd78dfc55" ;;
17) image="postgres:17-bookworm@sha256:4f736ae292687621d4dbe0d499ffd024a36bd2ee7d8ca6f2ccd4c800f047b394" ;;
18) image="postgres:18-bookworm@sha256:1961f96e6029a02c3812d7cb329a3b03a3ac2bb067058dec17b0f5596aca9296" ;;
*) echo "Unsupported PostgreSQL major: ${{ matrix.pg }}" >&2; exit 2 ;;
esac
echo "image=$image" >> "$GITHUB_OUTPUT"
- name: Build and push
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: ${{ matrix.platform.name }}
build-args: |
PG_MAJOR=${{ matrix.pg }}
POSTGRES_IMAGE=${{ steps.postgres-base.outputs.image }}
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
tags: ${{ env.IMAGE_NAME }}
labels: ${{ steps.meta.outputs.labels }}
sbom: true
provenance: mode=max
cache-from: type=gha,scope=pggraph-pg${{ matrix.pg }}-${{ matrix.platform.pair }}
cache-to: type=gha,mode=max,scope=pggraph-pg${{ matrix.pg }}-${{ matrix.platform.pair }}
- name: Export digest
run: |
mkdir -p "${{ runner.temp }}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: digests-pg${{ matrix.pg }}-${{ matrix.platform.pair }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 90
docker-merge:
name: Merge prepared Docker image PG${{ matrix.pg }}
if: needs.validate.outputs.mode == 'prepare'
needs:
- validate
- docker
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
id-token: write
attestations: write
strategy:
fail-fast: false
matrix:
pg: [14, 15, 16, 17, 18]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with:
images: ${{ env.IMAGE_NAME }}
tags: |
type=raw,value=pg${{ matrix.pg }}-sha-${{ needs.validate.outputs.short_sha }}
type=raw,value=pg${{ matrix.pg }}-${{ needs.validate.outputs.tag }}-prepared
type=raw,value=pg${{ matrix.pg }}-${{ needs.validate.outputs.version }}-prepared
- name: Download digests
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: ${{ runner.temp }}/digests
pattern: digests-pg${{ matrix.pg }}-*
merge-multiple: true
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
env:
DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}
run: |
digest_count="$(find . -maxdepth 1 -type f | wc -l | tr -d ' ')"
if [[ "$digest_count" != "2" ]]; then
echo "Expected 2 platform digests for PG${{ matrix.pg }}, found $digest_count" >&2
find . -maxdepth 1 -type f -print >&2
exit 1
fi
docker buildx imagetools create \
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *)
- name: Resolve merged manifest digest
id: manifest
run: |
image="${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-${{ needs.validate.outputs.tag }}-prepared"
digest="$(docker buildx imagetools inspect "$image" | awk '/Digest:/ { print $2; exit }')"
echo "digest=$digest" >> "$GITHUB_OUTPUT"
- name: Record prepared manifest digest
run: |
mkdir -p "${{ runner.temp }}/manifest"
jq -n \
--arg image "${{ env.IMAGE_NAME }}" \
--arg pg "${{ matrix.pg }}" \
--arg tag "${{ needs.validate.outputs.tag }}" \
--arg commit "${{ needs.validate.outputs.full_sha }}" \
--arg digest "${{ steps.manifest.outputs.digest }}" \
'{image: $image, postgresql_major: ($pg | tonumber), intended_tag: $tag, source_commit: $commit, digest: $digest}' \
> "${{ runner.temp }}/manifest/pg${{ matrix.pg }}.json"
- name: Retain prepared manifest digest
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docker-manifest-pg${{ matrix.pg }}-${{ needs.validate.outputs.short_sha }}
path: ${{ runner.temp }}/manifest/pg${{ matrix.pg }}.json
if-no-files-found: error
retention-days: 90
- name: Attest image provenance
uses: actions/attest-build-provenance@96b4a1ef7235a096b17240c259729fdd70c83d45 # v2
with:
subject-name: ${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.manifest.outputs.digest }}
push-to-registry: true
docker-verify:
name: Verify prepared image PG${{ matrix.pg }} ${{ matrix.platform.name }}
if: needs.validate.outputs.mode == 'prepare'
needs:
- validate
- docker-merge
runs-on: ${{ matrix.platform.runner }}
permissions:
packages: read
strategy:
fail-fast: false
matrix:
pg: [14, 15, 16, 17, 18]
platform:
- name: linux/amd64
runner: ubuntu-24.04
pair: linux-amd64
- name: linux/arm64
runner: ubuntu-24.04-arm
pair: linux-arm64
steps:
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Pull and start release image
run: |
image="${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-${{ needs.validate.outputs.tag }}-prepared"
docker pull --platform "${{ matrix.platform.name }}" "$image"
docker run -d --rm \
--platform "${{ matrix.platform.name }}" \
--name pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
-e POSTGRES_PASSWORD=postgres \
-p 55432:5432 \
"$image"
- name: Wait for PostgreSQL
run: |
consecutive=0
for _ in {1..60}; do
if docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 -tAc "SELECT 1" >/dev/null 2>&1; then
consecutive=$((consecutive + 1))
if [[ "$consecutive" -ge 2 ]]; then
exit 0
fi
else
consecutive=0
fi
sleep 1
done
docker logs pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }}
exit 1
- name: Verify extensions
run: |
count=""
for _ in {1..60}; do
if count="$(docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-tAc "SELECT count(*) FROM pg_extension WHERE extname IN ('graph', 'pg_cron');")"; then
if [[ "$count" == "2" ]]; then
break
fi
else
count=""
fi
sleep 2
done
if [[ "$count" != "2" ]]; then
echo "Expected graph and pg_cron extensions, found $count" >&2
docker logs pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }}
docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-c "SELECT extname, extversion FROM pg_extension ORDER BY extname;"
exit 1
fi
server_version_num="$(docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-tAc "SHOW server_version_num;")"
if [[ "${server_version_num:0:2}" != "${{ matrix.pg }}" ]]; then
echo "Expected PostgreSQL ${{ matrix.pg }}, got server_version_num=$server_version_num" >&2
exit 1
fi
- name: Stop container
if: always()
run: docker rm -f pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} >/dev/null 2>&1 || true
docker-manifests:
name: Retain prepared Docker manifest digests
if: needs.validate.outputs.mode == 'prepare'
needs:
- validate
- docker-merge
- docker-verify
runs-on: ubuntu-latest
steps:
- name: Download prepared manifest records
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: docker-manifest-pg*-${{ needs.validate.outputs.short_sha }}
path: manifests
merge-multiple: true
- name: Build deterministic manifest inventory
run: jq -s 'sort_by(.postgresql_major)' manifests/*.json > docker-manifests.json
- name: Upload manifest inventory
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docker-manifests-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.short_sha }}
path: docker-manifests.json
if-no-files-found: error
retention-days: 90
publish-docker:
name: Publish Docker image PG${{ matrix.pg }}
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- approve-publishing
- verify-prepared-artifacts
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
actions: read
attestations: read
strategy:
fail-fast: false
matrix:
pg: [14, 15, 16, 17, 18]
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Download prepared manifest inventory
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
run-id: ${{ inputs.prepare_run_id }}
pattern: docker-manifests-${{ needs.validate.outputs.version }}-*
path: manifests
merge-multiple: true
github-token: ${{ github.token }}
- name: Verify and promote prepared image digest
env:
GH_TOKEN: ${{ github.token }}
run: |
source="${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-${{ needs.validate.outputs.tag }}-prepared"
sha_source="${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-sha-${{ needs.validate.outputs.short_sha }}"
docker buildx imagetools inspect "$source" >/dev/null
docker buildx imagetools inspect "$sha_source" >/dev/null
source_digest="$(docker buildx imagetools inspect "$source" | awk '/Digest:/ { print $2; exit }')"
sha_digest="$(docker buildx imagetools inspect "$sha_source" | awk '/Digest:/ { print $2; exit }')"
jq -e \
--arg image "${{ env.IMAGE_NAME }}" \
--arg tag "${{ needs.validate.outputs.tag }}" \
--arg commit "${{ needs.validate.outputs.full_sha }}" \
--argjson pg "${{ matrix.pg }}" \
'[.[] | select(
.image == $image and
.intended_tag == $tag and
.source_commit == $commit and
.postgresql_major == $pg and
(.digest | startswith("sha256:"))
)] | length == 1' manifests/docker-manifests.json >/dev/null
expected_digest="$(jq -r '.[] | select(.postgresql_major == ${{ matrix.pg }}) | .digest' manifests/docker-manifests.json)"
if [[ "$source_digest" != "$sha_digest" || "$source_digest" != "$expected_digest" ]]; then
echo "Prepared Docker digest does not match the retained candidate manifest" >&2
exit 1
fi
source_by_digest="${{ env.IMAGE_NAME }}@$expected_digest"
gh attestation verify "oci://$source_by_digest" \
--repo "${{ github.repository }}" \
--signer-workflow "${{ github.repository }}/.github/workflows/release.yml" \
--source-digest "${{ needs.validate.outputs.full_sha }}" \
--predicate-type https://slsa.dev/provenance/v1
docker buildx imagetools inspect "$source_by_digest" \
--format '{{ json .SBOM }}' | jq -e 'length > 0' >/dev/null
tags=(
-t "${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-${{ needs.validate.outputs.tag }}"
-t "${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-${{ needs.validate.outputs.version }}"
-t "${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}"
-t "${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-sha-${{ needs.validate.outputs.short_sha }}"
)
if [[ "${{ matrix.pg }}" == "17" ]]; then
tags+=(
-t "${{ env.IMAGE_NAME }}:${{ needs.validate.outputs.tag }}"
-t "${{ env.IMAGE_NAME }}:${{ needs.validate.outputs.version }}"
-t "${{ env.IMAGE_NAME }}:latest"
-t "${{ env.IMAGE_NAME }}:sha-${{ needs.validate.outputs.short_sha }}"
)
fi
docker buildx imagetools create "${tags[@]}" "$source_by_digest"
publish-docker-verify:
name: Verify published image PG${{ matrix.pg }} ${{ matrix.platform.name }}
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- publish-docker
runs-on: ${{ matrix.platform.runner }}
permissions:
packages: read
strategy:
fail-fast: false
matrix:
pg: [14, 15, 16, 17, 18]
platform:
- name: linux/amd64
runner: ubuntu-24.04
pair: linux-amd64
- name: linux/arm64
runner: ubuntu-24.04-arm
pair: linux-arm64
steps:
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Pull and start published image
run: |
image="${{ env.IMAGE_NAME }}:pg${{ matrix.pg }}-${{ needs.validate.outputs.tag }}"
docker pull --platform "${{ matrix.platform.name }}" "$image"
docker run -d --rm \
--platform "${{ matrix.platform.name }}" \
--name pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
-e POSTGRES_PASSWORD=postgres \
-p 55432:5432 \
"$image"
- name: Wait for PostgreSQL
run: |
consecutive=0
for _ in {1..60}; do
if docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 -tAc "SELECT 1" >/dev/null 2>&1; then
consecutive=$((consecutive + 1))
if [[ "$consecutive" -ge 2 ]]; then
exit 0
fi
else
consecutive=0
fi
sleep 1
done
docker logs pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }}
exit 1
- name: Verify extensions
run: |
count=""
for _ in {1..60}; do
if count="$(docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-tAc "SELECT count(*) FROM pg_extension WHERE extname IN ('graph', 'pg_cron');")"; then
if [[ "$count" == "2" ]]; then
break
fi
else
count=""
fi
sleep 2
done
if [[ "$count" != "2" ]]; then
echo "Expected graph and pg_cron extensions, found $count" >&2
docker logs pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }}
docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-c "SELECT extname, extversion FROM pg_extension ORDER BY extname;"
exit 1
fi
server_version_num="$(docker exec pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-tAc "SHOW server_version_num;")"
if [[ "${server_version_num:0:2}" != "${{ matrix.pg }}" ]]; then
echo "Expected PostgreSQL ${{ matrix.pg }}, got server_version_num=$server_version_num" >&2
exit 1
fi
- name: Stop container
if: always()
run: docker rm -f pggraph-release-verify-pg${{ matrix.pg }}-${{ matrix.platform.pair }} >/dev/null 2>&1 || true
docker-verify-default:
name: Verify default published image ${{ matrix.platform.name }}
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- publish-docker
runs-on: ${{ matrix.platform.runner }}
permissions:
packages: read
strategy:
fail-fast: false
matrix:
platform:
- name: linux/amd64
runner: ubuntu-24.04
pair: linux-amd64
- name: linux/arm64
runner: ubuntu-24.04-arm
pair: linux-arm64
steps:
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Pull and start default release image
run: |
image="${{ env.IMAGE_NAME }}:${{ needs.validate.outputs.tag }}"
docker pull --platform "${{ matrix.platform.name }}" "$image"
docker run -d --rm \
--platform "${{ matrix.platform.name }}" \
--name pggraph-release-verify-default-${{ matrix.platform.pair }} \
-e POSTGRES_PASSWORD=postgres \
-p 55432:5432 \
"$image"
- name: Wait for PostgreSQL
run: |
consecutive=0
for _ in {1..60}; do
if docker exec pggraph-release-verify-default-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 -tAc "SELECT 1" >/dev/null 2>&1; then
consecutive=$((consecutive + 1))
if [[ "$consecutive" -ge 2 ]]; then
exit 0
fi
else
consecutive=0
fi
sleep 1
done
docker logs pggraph-release-verify-default-${{ matrix.platform.pair }}
exit 1
- name: Verify extensions and default PostgreSQL major
run: |
count=""
for _ in {1..60}; do
if count="$(docker exec pggraph-release-verify-default-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-tAc "SELECT count(*) FROM pg_extension WHERE extname IN ('graph', 'pg_cron');")"; then
if [[ "$count" == "2" ]]; then
break
fi
else
count=""
fi
sleep 2
done
if [[ "$count" != "2" ]]; then
echo "Expected graph and pg_cron extensions, found $count" >&2
docker logs pggraph-release-verify-default-${{ matrix.platform.pair }}
docker exec pggraph-release-verify-default-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-c "SELECT extname, extversion FROM pg_extension ORDER BY extname;"
exit 1
fi
server_version_num="$(docker exec pggraph-release-verify-default-${{ matrix.platform.pair }} \
psql -U postgres -d graph -v ON_ERROR_STOP=1 \
-tAc "SHOW server_version_num;")"
if [[ "${server_version_num:0:2}" != "17" ]]; then
echo "Expected default PostgreSQL 17 image, got server_version_num=$server_version_num" >&2
exit 1
fi
- name: Stop container
if: always()
run: docker rm -f pggraph-release-verify-default-${{ matrix.platform.pair }} >/dev/null 2>&1 || true
publish-summary:
name: Publish summary
if: needs.validate.outputs.mode == 'publish'
needs:
- validate
- pgxn-verify
- attach-pgxn-artifact
- publish-docker-verify
- docker-verify-default
runs-on: ubuntu-latest
steps:
- name: Summarize published packages
run: |
{
echo "## Published packages"
echo
echo "- PGXN: pggraph ${{ needs.validate.outputs.version }}"
echo "- Docker: ${{ env.IMAGE_NAME }}:${{ needs.validate.outputs.tag }}"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg14-${{ needs.validate.outputs.tag }}"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg15-${{ needs.validate.outputs.tag }}"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg16-${{ needs.validate.outputs.tag }}"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg17-${{ needs.validate.outputs.tag }}"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg18-${{ needs.validate.outputs.tag }}"
} >> "$GITHUB_STEP_SUMMARY"
prepare-summary:
name: Prepare summary
if: needs.validate.outputs.mode == 'prepare'
needs:
- validate
- release-bundle
- docker-verify
- docker-manifests
runs-on: ubuntu-latest
steps:
- name: Summarize prepared packages
run: |
{
echo "## Prepared packages"
echo
echo "- Commit: ${{ needs.validate.outputs.short_sha }}"
echo "- Source bundle: release-bundle-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.short_sha }}"
echo "- Docker manifests: docker-manifests-${{ needs.validate.outputs.version }}-${{ needs.validate.outputs.short_sha }}"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg14-${{ needs.validate.outputs.tag }}-prepared"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg15-${{ needs.validate.outputs.tag }}-prepared"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg16-${{ needs.validate.outputs.tag }}-prepared"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg17-${{ needs.validate.outputs.tag }}-prepared"
echo "- Docker: ${{ env.IMAGE_NAME }}:pg18-${{ needs.validate.outputs.tag }}-prepared"
} >> "$GITHUB_STEP_SUMMARY"