Skip to content

Release v0.2.5

Release v0.2.5 #9

Workflow file for this run

name: Docker Publish
# Publishes the public pg_durable image to GHCR by installing a released Debian
# package on top of the official PostgreSQL image. No source compilation, so
# this is fast and amd64-only (matching the released .deb architecture).
#
# Multi-arch (arm64) is intentionally not built here: there is no arm64 .deb yet.
# When arm64 packages are published, add the arch to this workflow.
#
# The image is intended for evaluating and learning pg_durable only — NOT for
# production. Its HTTP egress policy is whatever the released package was built
# with (http-allow-azure-domains).
on:
release:
types: [published]
workflow_dispatch:
inputs:
ref:
description: >
Release tag to publish an image for, e.g. v0.2.2. The matching .deb
assets must already be attached to that GitHub release.
required: true
type: string
dry_run:
description: Build and smoke-test only — do not push images to GHCR.
required: false
# Manual runs default to a safe dry run; set false to actually push.
default: true
type: boolean
overwrite:
description: >
Allow overwriting immutable X.Y.Z-pg<major> tags that already exist in
the registry. Floating tags (pg<major>, latest) always move regardless.
required: false
default: false
type: boolean
permissions:
contents: read
packages: write
env:
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/pg_durable
# PG major that also receives the unqualified `latest` tag. Lets future PG
# versions (e.g. pg18) be published without fighting over `latest`.
DEFAULT_PG_MAJOR: '17'
concurrency:
group: docker-publish-${{ github.event.release.tag_name || github.event.inputs.ref }}
cancel-in-progress: false
jobs:
publish:
name: Publish PG${{ matrix.pg_major }} image
runs-on: ubuntu-latest
# Don't run (and push images) on forks.
if: github.repository == 'microsoft/pg_durable'
strategy:
fail-fast: false
matrix:
pg_major: ['17', '18']
steps:
# Check out the tooling (Dockerfile.release) from where the workflow runs:
# the default branch for release events, or the dispatched branch for
# workflow_dispatch. The release tag itself may predate this file.
- name: Checkout tooling
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'release' && github.event.repository.default_branch || github.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve version and download package
id: pkg
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
INPUT_REF: ${{ github.event.inputs.ref }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
PG_MAJOR: ${{ matrix.pg_major }}
run: |
set -euo pipefail
if [[ "${EVENT_NAME}" == "release" ]]; then
tag="${RELEASE_TAG}"
else
tag="${INPUT_REF}"
fi
if [[ "${tag}" != v* ]]; then
echo "::error::Expected a version tag starting with 'v' (e.g. v0.2.2), got '${tag}'."
exit 1
fi
version="${tag#v}"
mkdir -p dist
echo "Downloading PG${PG_MAJOR} package for ${tag}..."
gh release download "${tag}" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "pg-durable-postgresql-${PG_MAJOR}_*_amd64.deb" \
--dir dist
ls -l dist
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Compute image tags
id: meta
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ steps.pkg.outputs.version }}
PG_MAJOR: ${{ matrix.pg_major }}
run: |
set -euo pipefail
tags=(
"${IMAGE_NAME}:${VERSION}-pg${PG_MAJOR}"
"${IMAGE_NAME}:v${VERSION}-pg${PG_MAJOR}"
)
# A version containing a hyphen is a semver pre-release (e.g. 0.2.2-rc1):
# publish immutable tags only, never move floating tags.
if [[ "${VERSION}" == *-* ]]; then
echo "Pre-release ${VERSION}: immutable tags only (no floating pg${PG_MAJOR}/latest)."
else
# Floating tags only move forward: only the highest stable published
# release may claim pg${PG_MAJOR} (and latest, for the default major).
highest="$(gh release list --repo "${GITHUB_REPOSITORY}" \
--limit 1000 \
--json tagName,isPrerelease,isDraft \
--jq '[.[] | select(.isPrerelease == false and .isDraft == false) | .tagName] | .[]' \
| sed 's/^v//' | sort -V | tail -n1)"
if [[ "${VERSION}" == "${highest}" ]]; then
tags+=("${IMAGE_NAME}:pg${PG_MAJOR}")
if [[ "${PG_MAJOR}" == "${DEFAULT_PG_MAJOR}" ]]; then
tags+=("${IMAGE_NAME}:latest")
fi
else
echo "Version ${VERSION} is not the highest stable release (${highest}); not moving floating tags."
fi
fi
printf 'Image tags:\n%s\n' "${tags[*]}"
{
echo "tags<<EOF"
printf '%s\n' "${tags[@]}"
echo "EOF"
echo "smoke=${IMAGE_NAME}:${VERSION}-pg${PG_MAJOR}"
} >> "$GITHUB_OUTPUT"
# Build once and load into the local daemon (all final tags applied), so the
# exact image we smoke-test is the one we publish. The publish step below
# rebuilds from this builder's cache (a content-identical, near-instant
# cache hit) so it can attach provenance + SBOM attestations, which the
# `--load` daemon image store cannot carry.
- name: Build image (amd64)
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.release
platforms: linux/amd64
load: true
push: false
build-args: |
PG_MAJOR=${{ matrix.pg_major }}
tags: ${{ steps.meta.outputs.tags }}
labels: |
org.opencontainers.image.title=pg_durable
org.opencontainers.image.description=Durable SQL function execution for PostgreSQL — test/learning image, not for production
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.url=https://github.com/${{ github.repository }}
org.opencontainers.image.licenses=PostgreSQL
org.opencontainers.image.version=${{ steps.pkg.outputs.version }}
- name: Smoke test — container starts and extension loads
shell: bash
env:
IMAGE: ${{ steps.meta.outputs.smoke }}
run: |
set -euo pipefail
CONTAINER="pg_durable_smoke_${{ matrix.pg_major }}"
cleanup() { docker rm -f "$CONTAINER" >/dev/null 2>&1 || true; }
trap cleanup EXIT
docker run -d \
--name "$CONTAINER" \
-e POSTGRES_HOST_AUTH_METHOD=trust \
"$IMAGE"
echo -n "Waiting for PostgreSQL"
READY_COUNT=0
for _ in $(seq 1 90); do
if docker exec "$CONTAINER" pg_isready -U postgres >/dev/null 2>&1; then
READY_COUNT=$((READY_COUNT + 1))
# Require 3 consecutive ready checks to clear the post-init restart.
if [ "$READY_COUNT" -ge 3 ]; then
echo " ready!"
break
fi
else
READY_COUNT=0
fi
echo -n "."
sleep 1
done
if [ "$READY_COUNT" -lt 3 ]; then
echo " TIMEOUT"
docker logs "$CONTAINER"
exit 1
fi
# Allow the background worker to initialize.
sleep 2
echo "Creating extension..."
docker exec "$CONTAINER" psql -U postgres -c "CREATE EXTENSION IF NOT EXISTS pg_durable;"
echo "Verifying extension version..."
VERSION="$(docker exec "$CONTAINER" psql -U postgres -t -c "SELECT df.version();")"
echo "pg_durable version: $(echo "$VERSION" | tr -d ' \n')"
# Run a durable function end-to-end to prove the background worker is
# actually executing (catches worker/database misconfiguration that a
# plain CREATE EXTENSION would not).
echo "Starting a durable function..."
INSTANCE="$(docker exec "$CONTAINER" psql -U postgres -t -A -d postgres \
-c "SELECT df.start('SELECT 1');" | tr -d ' \n')"
echo "Instance: $INSTANCE"
echo -n "Waiting for completion"
STATUS=""
for _ in $(seq 1 60); do
STATUS="$(docker exec "$CONTAINER" psql -U postgres -t -A -d postgres \
-c "SELECT s FROM df.status('$INSTANCE') s;" | tr -d ' \n' | tr '[:upper:]' '[:lower:]')"
case "$STATUS" in
completed) echo " done!"; break ;;
failed|cancelled)
echo " FAILED ($STATUS)"; docker logs "$CONTAINER"; exit 1 ;;
esac
echo -n "."
sleep 1
done
if [ "$STATUS" != "completed" ]; then
echo " TIMEOUT (last status: ${STATUS:-none})"
docker logs "$CONTAINER"
exit 1
fi
- name: Resolve tags to publish
id: push
if: ${{ github.event.inputs.dry_run != 'true' }}
shell: bash
env:
TAGS: ${{ steps.meta.outputs.tags }}
OVERWRITE: ${{ github.event.inputs.overwrite }}
run: |
set -euo pipefail
final=()
while IFS= read -r tag; do
[ -n "$tag" ] || continue
# Floating tags (pg<major>, latest) are meant to move forward; only
# immutable X.Y.Z-pg<major> tags are protected from being replaced.
if [[ ! "$tag" =~ :(pg[0-9]+|latest)$ ]] \
&& [[ "${OVERWRITE:-false}" != "true" ]] \
&& docker manifest inspect "$tag" >/dev/null 2>&1; then
echo "Immutable tag $tag already exists; skipping (set overwrite=true to replace)."
continue
fi
final+=("$tag")
done <<< "$TAGS"
if [ "${#final[@]}" -eq 0 ]; then
echo "No tags to publish."
echo "has_tags=false" >> "$GITHUB_OUTPUT"
else
printf 'Publishing tags:\n%s\n' "${final[*]}"
{
echo "tags<<EOF"
printf '%s\n' "${final[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "has_tags=true" >> "$GITHUB_OUTPUT"
fi
# Re-run the build with push enabled. Inputs are identical to the load build
# above, so this is a full cache hit on the same builder — the published
# bytes match what was smoke-tested. push (not --load) lets buildx attach
# provenance + SBOM attestations to the image in the registry.
- name: Publish image with attestations (amd64)
if: ${{ github.event.inputs.dry_run != 'true' && steps.push.outputs.has_tags == 'true' }}
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile.release
platforms: linux/amd64
push: true
provenance: true
sbom: true
build-args: |
PG_MAJOR=${{ matrix.pg_major }}
tags: ${{ steps.push.outputs.tags }}
labels: |
org.opencontainers.image.title=pg_durable
org.opencontainers.image.description=Durable SQL function execution for PostgreSQL — test/learning image, not for production
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.url=https://github.com/${{ github.repository }}
org.opencontainers.image.licenses=PostgreSQL
org.opencontainers.image.version=${{ steps.pkg.outputs.version }}