name: Release PostgreSQL 14-18 Linux artifacts

on:
  workflow_run:
    workflows: ["CI"]
    branches: ["master"]
    types: [completed]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: pg_local_cache-release
  cancel-in-progress: false

jobs:
  metadata:
    if: >-
      github.event_name == 'workflow_dispatch' ||
      (
        github.event.workflow_run.conclusion == 'success' &&
        (
          github.event.workflow_run.event == 'push' ||
          github.event.workflow_run.event == 'workflow_dispatch'
        ) &&
        github.event.workflow_run.head_branch == 'master' &&
        github.event.workflow_run.head_repository.full_name == github.repository
      )
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    permissions:
      contents: read
      actions: read
    outputs:
      release_ready: ${{ steps.meta.outputs.release_ready }}
      version: ${{ steps.meta.outputs.version }}
      stable_tag: ${{ steps.meta.outputs.stable_tag }}
      commit_tag: ${{ steps.meta.outputs.commit_tag }}
      sha: ${{ steps.meta.outputs.sha }}
      short_sha: ${{ steps.meta.outputs.short_sha }}
      stable_publishable: ${{ steps.meta.outputs.stable_publishable }}
      workflow_run_id: ${{ steps.meta.outputs.workflow_run_id }}
    env:
      TARGET_SHA: >-
        ${{ github.event_name == 'workflow_run'
            && github.event.workflow_run.head_sha
            || github.sha }}
      TRIGGER_RUN_ID: >-
        ${{ github.event_name == 'workflow_run'
            && github.event.workflow_run.id
            || '' }}
      GH_TOKEN: ${{ github.token }}
    steps:
      - name: Check out the exact successful commit
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ env.TARGET_SHA }}
          fetch-depth: 0
          persist-credentials: false

      - name: Validate release metadata and immutable tag policy
        id: meta
        shell: bash
        run: |
          set -Eeuo pipefail

          sha="$(git rev-parse HEAD)"
          [[ "$sha" == "$TARGET_SHA" ]]
          master_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/master" --jq .sha)"
          if [[ "$sha" != "$master_sha" ]]; then
            echo "::notice::Skipping ${sha}; master advanced to ${master_sha}"
            {
              echo "release_ready=false"
              echo "sha=${sha}"
              echo "workflow_run_id=${TRIGGER_RUN_ID}"
            } >> "$GITHUB_OUTPUT"
            exit 0
          fi

          plan="$(python3 scripts/auto_version.py --json)"
          action="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["action"])' <<<"$plan")"
          reason="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["reason"])' <<<"$plan")"
          if [[ "$action" != "release" ]]; then
            echo "::notice::No release from ${sha}: ${reason}"
            {
              echo "release_ready=false"
              echo "sha=${sha}"
              echo "workflow_run_id=${TRIGGER_RUN_ID}"
            } >> "$GITHUB_OUTPUT"
            exit 0
          fi

          mapfile -t versions < <(
            sed -n \
              "s/^default_version = '\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)'$/\1/p" \
              pg_local_cache.control
          )
          [[ "${#versions[@]}" == 1 ]] || {
            echo "::error::control file needs one strict X.Y.Z default_version"
            exit 1
          }

          version="${versions[0]}"
          stable_tag="v${version}"
          short_sha="${sha:0:12}"
          commit_tag="master-${short_sha}"
          test -f "sql/pg_local_cache--${version}.sql"
          grep -Fq 'pg_local_cache--' Dockerfile
          grep -Fq "pg_local_cache_version:${version}" src/pg_local_cache_worker.c
          python3 scripts/validate_pgxn_meta.py

          if stable_sha="$(
            gh api "repos/${GITHUB_REPOSITORY}/commits/${stable_tag}" \
              --jq .sha 2>/dev/null
          )"; then
            echo "::error::${stable_tag} already points to ${stable_sha}; refusing version reuse"
            exit 1
          fi

          workflow_run_id="$TRIGGER_RUN_ID"
          if [[ -z "$workflow_run_id" ]]; then
            workflow_run_id="$(
              gh api \
                "repos/${GITHUB_REPOSITORY}/actions/workflows/ci.yml/runs?branch=master&head_sha=${sha}&status=success&per_page=100" \
                --jq '[
                  .workflow_runs[]
                  | select(
                      .event == "push" or
                      .event == "workflow_dispatch"
                    )
                ][0].id // empty'
            )"
          fi

          [[ -n "$workflow_run_id" ]] || {
            echo "::error::No successful master CI run exists for ${sha}"
            exit 1
          }

          {
            echo "release_ready=true"
            echo "version=${version}"
            echo "stable_tag=${stable_tag}"
            echo "commit_tag=${commit_tag}"
            echo "sha=${sha}"
            echo "short_sha=${short_sha}"
            echo "stable_publishable=true"
            echo "workflow_run_id=${workflow_run_id}"
          } >> "$GITHUB_OUTPUT"

  binary:
    needs: metadata
    if: needs.metadata.outputs.release_ready == 'true'
    runs-on: ubuntu-24.04
    timeout-minutes: 25
    strategy:
      fail-fast: false
      matrix:
        postgres_major: [14, 15, 16, 17, 18]
        target:
          - variant: bookworm
            libc: glibc
          - variant: alpine3.23
            libc: musl
    env:
      VERSION: ${{ needs.metadata.outputs.version }}
      RELEASE_SHA: ${{ needs.metadata.outputs.sha }}
      POSTGRES_MAJOR: ${{ matrix.postgres_major }}
      POSTGRES_VARIANT: ${{ matrix.target.variant }}
      LIBC: ${{ matrix.target.libc }}
    steps:
      - name: Check out exact source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ env.RELEASE_SHA }}
          fetch-depth: 0
          persist-credentials: false

      - name: Build and verify binary
        shell: bash
        run: |
          set -Eeuo pipefail
          image="pg_local_cache-release:pg${POSTGRES_MAJOR}-${LIBC}-${RELEASE_SHA}"
          docker build \
            --platform linux/amd64 \
            --target extension \
            --build-arg "POSTGRES_MAJOR=${POSTGRES_MAJOR}" \
            --build-arg "POSTGRES_VARIANT=${POSTGRES_VARIANT}" \
            --tag "$image" \
            .

          pkglibdir="$(
            docker run --rm \
              --entrypoint /bin/sh \
              "$image" \
              -ec 'pg_config --pkglibdir'
          )"

          reported="$(
            docker run --rm \
              --env LD_BIND_NOW=1 \
              --env "LD_PRELOAD=${pkglibdir}/pg_local_cache.so" \
              --entrypoint postgres \
              "$image" \
              --version
          )"
          [[ "$reported" == *" ${POSTGRES_MAJOR}."* ]]

      - name: Assemble deterministic binary archive
        shell: bash
        run: |
          set -Eeuo pipefail
          image="pg_local_cache-release:pg${POSTGRES_MAJOR}-${LIBC}-${RELEASE_SHA}"
          asset="pg_local_cache-pg${POSTGRES_MAJOR}-linux-${LIBC}-amd64"
          root="release-stage/pg_local_cache-${VERSION}-pg${POSTGRES_MAJOR}-linux-${LIBC}-amd64"
          install -d "$root/lib" "$root/share/extension" "$root/docs" dist extracted

          pkglibdir="$(docker run --rm --entrypoint /bin/sh "$image" -ec 'pg_config --pkglibdir')"
          sharedir="$(docker run --rm --entrypoint /bin/sh "$image" -ec 'pg_config --sharedir')"
          container="$(docker create "$image")"
          trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
          docker cp "${container}:${pkglibdir}/pg_local_cache.so" "$root/lib/"
          docker cp "${container}:${sharedir}/extension/." extracted/
          install -m 0644 extracted/pg_local_cache.control "$root/share/extension/"
          find extracted -maxdepth 1 -type f -name 'pg_local_cache--*.sql' \
            -exec install -m 0644 {} "$root/share/extension/" \;

          install -m 0755 scripts/install-existing.sh "$root/install.sh"
          install -m 0644 README.md LICENSE "$root/"
          install -m 0644 \
            docs/INSTALL_EXISTING.md docs/BENCHMARKS.md \
            docs/MONITORING.md docs/TECHNICAL.md "$root/docs/"
          {
            printf 'format=1\n'
            printf 'version=%s\n' "$VERSION"
            printf 'postgres_major=%s\n' "$POSTGRES_MAJOR"
            printf 'os=linux\n'
            printf 'libc=%s\n' "$LIBC"
            printf 'architecture=amd64\n'
            printf 'commit=%s\n' "$RELEASE_SHA"
            printf 'build_image=postgres:%s-%s\n' "$POSTGRES_MAJOR" "$POSTGRES_VARIANT"
          } > "$root/RELEASE-METADATA"

          epoch="$(git show -s --format=%ct "$RELEASE_SHA")"
          find release-stage -exec touch -h -d "@${epoch}" {} +
          tar \
            --sort=name --format=posix \
            --pax-option=delete=atime,delete=ctime \
            --owner=0 --group=0 --numeric-owner --mtime="@${epoch}" \
            -C release-stage -cf - "$(basename "$root")" |
            gzip -n -9 > "dist/${asset}.tar.gz"

      - name: Upload binary lane
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: binary-pg${{ matrix.postgres_major }}-${{ matrix.target.libc }}
          path: dist/*.tar.gz
          if-no-files-found: error
          retention-days: 1

  package:
    needs: [metadata, binary]
    runs-on: ubuntu-24.04
    timeout-minutes: 20
    permissions:
      contents: read
      actions: read
    env:
      VERSION: ${{ needs.metadata.outputs.version }}
      RELEASE_SHA: ${{ needs.metadata.outputs.sha }}
      SHORT_SHA: ${{ needs.metadata.outputs.short_sha }}
      TRIGGER_RUN_ID: ${{ needs.metadata.outputs.workflow_run_id }}
      GH_TOKEN: ${{ github.token }}
    steps:
      - name: Check out exact source
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          ref: ${{ env.RELEASE_SHA }}
          fetch-depth: 0
          persist-credentials: false

      - name: Download all binary lanes
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          pattern: binary-*
          path: dist
          merge-multiple: true

      - name: Validate source and create source archive
        shell: bash
        run: |
          set -Eeuo pipefail
          test "$(find dist -maxdepth 1 -name 'pg_local_cache-pg*-linux-*-amd64.tar.gz' | wc -l)" -eq 10
          test -x scripts/install-existing.sh
          bash -n scripts/install-existing.sh
          make source-test source-sanitize benchmark-test

          source_root="pg_local_cache-${VERSION}-source"
          git archive --format=tar --prefix="${source_root}/" "$RELEASE_SHA" |
            gzip -n -9 > dist/pg_local_cache-source.tar.gz

      - name: Preserve benchmark evidence from successful CI
        shell: bash
        run: |
          set -Eeuo pipefail
          install -d ci-evidence/comparison ci-evidence/sql-only
          gh run download "$TRIGGER_RUN_ID" --repo "$GITHUB_REPOSITORY" \
            --name comparison-smoke --dir ci-evidence/comparison
          gh run download "$TRIGGER_RUN_ID" --repo "$GITHUB_REPOSITORY" \
            --name sql-only-benchmark-smoke --dir ci-evidence/sql-only
          python3 scripts/validate_benchmark_evidence.py \
            --revision "$RELEASE_SHA" \
            --whole ci-evidence/comparison/whole-row.json \
            --sql-only ci-evidence/sql-only/sql-only.json

          epoch="$(git show -s --format=%ct "$RELEASE_SHA")"
          find ci-evidence -exec touch -h -d "@${epoch}" {} +
          tar \
            --sort=name --format=posix \
            --pax-option=delete=atime,delete=ctime \
            --owner=0 --group=0 --numeric-owner --mtime="@${epoch}" \
            -C ci-evidence -cf - . |
            gzip -n -9 > dist/pg_local_cache-ci-benchmarks.tar.gz

      - name: Create and verify checksums
        shell: bash
        run: |
          set -Eeuo pipefail
          (
            cd dist
            find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\0' |
              LC_ALL=C sort -z | xargs -0 sha256sum > SHA256SUMS
            sha256sum --check SHA256SUMS
          )

      - name: Upload per-commit downloadable artifact
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: pg_local_cache-${{ needs.metadata.outputs.version }}-${{ needs.metadata.outputs.short_sha }}
          path: dist/
          if-no-files-found: error
          retention-days: 90

  publish:
    needs: [metadata, package]
    runs-on: ubuntu-24.04
    timeout-minutes: 15
    permissions:
      contents: write
      actions: read
    env:
      GH_TOKEN: ${{ github.token }}
      GH_REPO: ${{ github.repository }}
      VERSION: ${{ needs.metadata.outputs.version }}
      STABLE_TAG: ${{ needs.metadata.outputs.stable_tag }}
      COMMIT_TAG: ${{ needs.metadata.outputs.commit_tag }}
      RELEASE_SHA: ${{ needs.metadata.outputs.sha }}
      STABLE_PUBLISHABLE: ${{ needs.metadata.outputs.stable_publishable }}
    steps:
      - name: Download verified packages
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: pg_local_cache-${{ needs.metadata.outputs.version }}-${{ needs.metadata.outputs.short_sha }}
          path: dist

      - name: Verify checksums
        working-directory: dist
        run: sha256sum --check SHA256SUMS

      - name: Publish immutable commit prerelease and optional stable release
        shell: bash
        run: |
          set -Eeuo pipefail

          create_ref() {
            local tag="$1" existing
            existing=""
            if existing="$(gh api "repos/${GH_REPO}/commits/${tag}" --jq .sha 2>/dev/null)"; then
              [[ "$existing" == "$RELEASE_SHA" ]] || {
                echo "::error::Refusing to move immutable tag ${tag}"
                return 1
              }
              return
            fi
            gh api --method POST "repos/${GH_REPO}/git/refs" \
              -f "ref=refs/tags/${tag}" -f "sha=${RELEASE_SHA}" >/dev/null
          }

          upload_missing_assets() {
            local tag="$1" existing_assets temporary
            existing_assets="$(gh release view "$tag" --json assets --jq '.assets[].name')"
            temporary="$(mktemp -d)"
            for file in dist/*; do
              local name
              name="$(basename "$file")"
              if grep -Fxq "$name" <<<"$existing_assets"; then
                mkdir -p "$temporary/$name"
                gh release download "$tag" --pattern "$name" --dir "$temporary/$name"
                cmp --silent "$file" "$temporary/$name/$name" || {
                  echo "::error::Existing ${tag}/${name} differs; refusing overwrite"
                  return 1
                }
              else
                gh release upload "$tag" "$file"
              fi
            done
          }

          create_ref "$COMMIT_TAG"
          if ! gh release view "$COMMIT_TAG" >/dev/null 2>&1; then
            gh release create "$COMMIT_TAG" \
              --target "$RELEASE_SHA" --draft --prerelease \
              --title "pg_local_cache ${VERSION} — master ${RELEASE_SHA:0:12}" \
              --notes "Successful master build ${RELEASE_SHA}. Includes PostgreSQL 14–18 Linux glibc/musl binaries, checksums, installer and CI benchmark evidence."
          fi
          upload_missing_assets "$COMMIT_TAG"
          gh release edit "$COMMIT_TAG" --draft=false --prerelease

          if [[ "$STABLE_PUBLISHABLE" == "true" ]]; then
            create_ref "$STABLE_TAG"
            if ! gh release view "$STABLE_TAG" >/dev/null 2>&1; then
              gh release create "$STABLE_TAG" \
                --target "$RELEASE_SHA" --draft \
                --title "pg_local_cache ${VERSION} — PostgreSQL 14–18" \
                --notes "Immutable pg_local_cache ${VERSION} release for PostgreSQL 14–18 on Linux amd64 (glibc and musl). Read docs/INSTALL_EXISTING.md before installing on an existing cluster."
            fi
            upload_missing_assets "$STABLE_TAG"
            gh release edit "$STABLE_TAG" --draft=false --prerelease=false
          fi
