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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          ref: ${{ github.event_name == 'release' && github.event.repository.default_branch || github.ref }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

      - name: Log in to GHCR
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        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 }}
