Release v0.2.5 #9
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }} |